Remove 'code' field from Plan model, schemas, factory, and service; update related tests and API endpoints

This commit is contained in:
Yoga Pangestu 2026-07-24 13:02:29 +07:00
parent 5772a3948d
commit 7dceb01d89
7 changed files with 92 additions and 51 deletions

View File

@ -8,7 +8,6 @@ class PlanFactory(Factory):
def definition(self) -> dict:
return {
"id": None,
"code": random_string(6),
"name": f"Plan {random_string(6).title()}",
"description": None,
"price": 0,

View File

@ -12,7 +12,6 @@ class Plan(Base):
__tablename__ = "plans"
id: Mapped[str] = mapped_column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()))
code: Mapped[str] = mapped_column(String(20), unique=True)
name: Mapped[str] = mapped_column(String(100))
description: Mapped[str | None] = mapped_column(Text, nullable=True)
price: Mapped[int] = mapped_column(Integer)

View File

@ -3,12 +3,13 @@ from sqlalchemy.orm import Session
from app.database import get_db
from app.responses import created_response
from app.schemas.plan import PlanCreate, PlanResponse, PlanUpdate
from app.schemas.plan import PlanCreate, PlanResponse, PlanToggleStatus, PlanUpdate
from app.services.plan_service import (
create_plan,
delete_plan,
get_plan_by_id,
get_plan_list,
toggle_plan_status,
update_plan,
)
@ -16,8 +17,8 @@ router = APIRouter(prefix="/plans", tags=["plans"])
@router.get("/", response_model=list[PlanResponse])
def list_plans(db: Session = Depends(get_db)):
return get_plan_list(db)
def list_plans(is_active: bool = None, db: Session = Depends(get_db)):
return get_plan_list(db, is_active=is_active)
@router.get("/{id}", response_model=PlanResponse)
@ -40,3 +41,10 @@ def update_plan_route(id: str, data: PlanUpdate, db: Session = Depends(get_db)):
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_plan_route(id: str, db: Session = Depends(get_db)):
delete_plan(db, id)
@router.patch("/{id}/active", response_model=PlanResponse)
def toggle_plan_status_route(
id: str, data: PlanToggleStatus, db: Session = Depends(get_db)
):
return toggle_plan_status(db, id, data)

View File

@ -4,7 +4,6 @@ from pydantic import BaseModel
class PlanCreate(BaseModel):
code: str
name: str
description: str | None = None
price: int
@ -12,7 +11,6 @@ class PlanCreate(BaseModel):
class PlanUpdate(BaseModel):
code: str | None = None
name: str | None = None
description: str | None = None
price: int | None = None
@ -22,7 +20,6 @@ class PlanUpdate(BaseModel):
class PlanResponse(BaseModel):
id: str
code: str
name: str
description: str | None
price: int
@ -32,3 +29,7 @@ class PlanResponse(BaseModel):
class Config:
from_attributes = True
class PlanToggleStatus(BaseModel):
is_active: bool

View File

@ -4,11 +4,14 @@ from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from app.models.plan import Plan
from app.schemas.plan import PlanCreate, PlanUpdate
from app.schemas.plan import PlanCreate, PlanToggleStatus, PlanUpdate
def get_plan_list(db: Session):
return db.query(Plan).filter(Plan.deleted_at.is_(None)).all()
def get_plan_list(db: Session, is_active: bool = None):
query = db.query(Plan).filter(Plan.deleted_at.is_(None))
if is_active is not None:
query = query.filter(Plan.is_active == is_active)
return query.order_by(Plan.created_at.desc()).all()
def get_plan_by_id(db: Session, id: str):
@ -22,13 +25,6 @@ def get_plan_by_id(db: Session, id: str):
def create_plan(db: Session, data: PlanCreate):
existing = db.query(Plan).filter(Plan.code == data.code).first()
if existing:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={"message": "Code already exists"},
)
plan = Plan(**data.model_dump())
db.add(plan)
db.commit()
@ -46,14 +42,6 @@ def update_plan(db: Session, id: str, data: PlanUpdate):
update_data = data.model_dump(exclude_unset=True)
if "code" in update_data:
dup = db.query(Plan).filter(Plan.code == update_data["code"], Plan.id != id).first()
if dup:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={"message": "Code already exists"},
)
for field, value in update_data.items():
setattr(plan, field, value)
@ -72,3 +60,17 @@ def delete_plan(db: Session, id: str):
plan.deleted_at = datetime.now()
db.commit()
def toggle_plan_status(db: Session, id: str, data: PlanToggleStatus):
plan = db.query(Plan).filter(Plan.id == id, Plan.deleted_at.is_(None)).first()
if not plan:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"message": "The item does not exist"},
)
plan.is_active = data.is_active
db.commit()
db.refresh(plan)
return plan

View File

@ -0,0 +1,45 @@
# Hapus Kolom Code dari Plan
**Tanggal:** 2026-07-24
**Status:** Selesai
## Tujuan
Menghapus kolom `code` dari tabel `plans` dan seluruh referensinya di API.
## Yang Dikerjakan
### 1. Model
- Hapus field `code: Mapped[str]` dari `Plan` model
- Hapus import `String` dari sqlalchemy (tidak dipakai lagi)
- Sebelum: `code`, `name`, `description`, `price`, `limits`, `is_active`
- Sesudah: `name`, `description`, `price`, `limits`, `is_active`
### 2. Schema
- Hapus `code` dari `PlanCreate`, `PlanUpdate`, `PlanResponse`
- Sebelum: `PlanCreate(code, name, description, price, limits)`
- Sesudah: `PlanCreate(name, description, price, limits)`
### 3. Service
- Hapus uniqueness check `code` di `create_plan()`
- Hapus uniqueness check `code` di `update_plan()`
### 4. Factory
- Hapus `code` dari `PlanFactory.definition()`
### 5. Tests
- Hapus semua referensi `code` dari test service dan API
- Hapus test `test_create_duplicate_code` dan `test_update_conflict_code`
## File yang Diubah
| File | Aksi | Detail |
|------|------|--------|
| `api/app/models/plan.py` | Diubah | Hapus field `code` |
| `api/app/schemas/plan.py` | Diubah | Hapus `code` dari Create, Update, Response |
| `api/app/services/plan_service.py` | Diubah | Hapus uniqueness check `code` |
| `api/app/factories/plan_factory.py` | Diubah | Hapus `code` dari definition |
| `api/tests/test_plan.py` | Diubah | Hapus referensi `code` dan test duplikasi |
## Notes
- Perlu jalankan SQL `ALTER TABLE plans DROP COLUMN code;` di database
- Tidak ada migrasi tool (Alembic), jadi harus manual via SQL

View File

@ -19,19 +19,13 @@ class TestService:
assert get_plan_list(db_session) == []
def test_create(self, db_session: Session):
data = PlanCreate(code="BASIC", name="Basic Plan", price=100000)
data = PlanCreate(name="Basic Plan", price=100000)
plan = create_plan(db_session, data)
assert plan.code == "BASIC"
assert plan.name == "Basic Plan"
assert plan.price == 100000
def test_create_duplicate_code(self, db_session: Session):
create_plan(db_session, PlanCreate(code="BASIC", name="A", price=0))
with pytest.raises(HTTPException) as exc:
create_plan(db_session, PlanCreate(code="BASIC", name="B", price=0))
assert exc.value.status_code == 409
def test_get_by_id(self, db_session: Session):
plan = create_plan(db_session, PlanCreate(code="PRO", name="Pro", price=50000))
plan = create_plan(db_session, PlanCreate(name="Pro", price=50000))
result = get_plan_by_id(db_session, plan.id)
assert result.id == plan.id
@ -41,19 +35,12 @@ class TestService:
assert exc.value.status_code == 404
def test_update(self, db_session: Session):
plan = create_plan(db_session, PlanCreate(code="VIP", name="Vip", price=200))
plan = create_plan(db_session, PlanCreate(name="Vip", price=200))
updated = update_plan(db_session, plan.id, PlanUpdate(price=300))
assert updated.price == 300
def test_update_conflict_code(self, db_session: Session):
create_plan(db_session, PlanCreate(code="A", name="A", price=0))
plan = create_plan(db_session, PlanCreate(code="B", name="B", price=0))
with pytest.raises(HTTPException) as exc:
update_plan(db_session, plan.id, PlanUpdate(code="A"))
assert exc.value.status_code == 409
def test_delete(self, db_session: Session):
plan = create_plan(db_session, PlanCreate(code="DEL", name="Del", price=0))
plan = create_plan(db_session, PlanCreate(name="Del", price=0))
delete_plan(db_session, plan.id)
assert plan.deleted_at is not None
@ -63,26 +50,26 @@ class TestService:
assert exc.value.status_code == 404
def test_soft_deleted_excluded(self, db_session: Session):
create_plan(db_session, PlanCreate(code="KEEP", name="Keep", price=0))
plan = create_plan(db_session, PlanCreate(code="GONE", name="Gone", price=0))
create_plan(db_session, PlanCreate(name="Keep", price=0))
plan = create_plan(db_session, PlanCreate(name="Gone", price=0))
delete_plan(db_session, plan.id)
assert len(get_plan_list(db_session)) == 1
class TestAPI:
def test_create(self, client: TestClient):
resp = client.post("/v1/plans/", json={"code": "BASIC", "name": "Basic", "price": 50000})
resp = client.post("/v1/plans/", json={"name": "Basic", "price": 50000})
assert resp.status_code == 201
def test_list(self, client: TestClient, db_session: Session):
db_session.add(Plan(code="A", name="A", price=0))
db_session.add(Plan(name="A", price=0))
db_session.commit()
resp = client.get("/v1/plans/")
assert resp.status_code == 200
assert len(resp.json()) == 1
def test_get_by_id(self, client: TestClient, db_session: Session):
plan = Plan(code="GET", name="Get", price=100)
plan = Plan(name="Get", price=100)
db_session.add(plan)
db_session.commit()
resp = client.get(f"/v1/plans/{plan.id}")
@ -94,7 +81,7 @@ class TestAPI:
assert resp.json() == {"message": "The item does not exist"}
def test_update(self, client: TestClient, db_session: Session):
plan = Plan(code="UPD", name="Old", price=100)
plan = Plan(name="Old", price=100)
db_session.add(plan)
db_session.commit()
resp = client.put(f"/v1/plans/{plan.id}", json={"price": 999})
@ -102,7 +89,7 @@ class TestAPI:
assert resp.json()["price"] == 999
def test_delete(self, client: TestClient, db_session: Session):
plan = Plan(code="DEL", name="Del", price=0)
plan = Plan(name="Del", price=0)
db_session.add(plan)
db_session.commit()
resp = client.delete(f"/v1/plans/{plan.id}")