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: def definition(self) -> dict:
return { return {
"id": None, "id": None,
"code": random_string(6),
"name": f"Plan {random_string(6).title()}", "name": f"Plan {random_string(6).title()}",
"description": None, "description": None,
"price": 0, "price": 0,

View File

@ -12,7 +12,6 @@ class Plan(Base):
__tablename__ = "plans" __tablename__ = "plans"
id: Mapped[str] = mapped_column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4())) 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)) name: Mapped[str] = mapped_column(String(100))
description: Mapped[str | None] = mapped_column(Text, nullable=True) description: Mapped[str | None] = mapped_column(Text, nullable=True)
price: Mapped[int] = mapped_column(Integer) 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.database import get_db
from app.responses import created_response 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 ( from app.services.plan_service import (
create_plan, create_plan,
delete_plan, delete_plan,
get_plan_by_id, get_plan_by_id,
get_plan_list, get_plan_list,
toggle_plan_status,
update_plan, update_plan,
) )
@ -16,8 +17,8 @@ router = APIRouter(prefix="/plans", tags=["plans"])
@router.get("/", response_model=list[PlanResponse]) @router.get("/", response_model=list[PlanResponse])
def list_plans(db: Session = Depends(get_db)): def list_plans(is_active: bool = None, db: Session = Depends(get_db)):
return get_plan_list(db) return get_plan_list(db, is_active=is_active)
@router.get("/{id}", response_model=PlanResponse) @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) @router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_plan_route(id: str, db: Session = Depends(get_db)): def delete_plan_route(id: str, db: Session = Depends(get_db)):
delete_plan(db, id) 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): class PlanCreate(BaseModel):
code: str
name: str name: str
description: str | None = None description: str | None = None
price: int price: int
@ -12,7 +11,6 @@ class PlanCreate(BaseModel):
class PlanUpdate(BaseModel): class PlanUpdate(BaseModel):
code: str | None = None
name: str | None = None name: str | None = None
description: str | None = None description: str | None = None
price: int | None = None price: int | None = None
@ -22,7 +20,6 @@ class PlanUpdate(BaseModel):
class PlanResponse(BaseModel): class PlanResponse(BaseModel):
id: str id: str
code: str
name: str name: str
description: str | None description: str | None
price: int price: int
@ -32,3 +29,7 @@ class PlanResponse(BaseModel):
class Config: class Config:
from_attributes = True 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 sqlalchemy.orm import Session
from app.models.plan import Plan 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): def get_plan_list(db: Session, is_active: bool = None):
return db.query(Plan).filter(Plan.deleted_at.is_(None)).all() 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): 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): 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()) plan = Plan(**data.model_dump())
db.add(plan) db.add(plan)
db.commit() db.commit()
@ -46,14 +42,6 @@ def update_plan(db: Session, id: str, data: PlanUpdate):
update_data = data.model_dump(exclude_unset=True) 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(): for field, value in update_data.items():
setattr(plan, field, value) setattr(plan, field, value)
@ -72,3 +60,17 @@ def delete_plan(db: Session, id: str):
plan.deleted_at = datetime.now() plan.deleted_at = datetime.now()
db.commit() 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) == [] assert get_plan_list(db_session) == []
def test_create(self, db_session: 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) plan = create_plan(db_session, data)
assert plan.code == "BASIC" assert plan.name == "Basic Plan"
assert plan.price == 100000 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): 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) result = get_plan_by_id(db_session, plan.id)
assert result.id == plan.id assert result.id == plan.id
@ -41,19 +35,12 @@ class TestService:
assert exc.value.status_code == 404 assert exc.value.status_code == 404
def test_update(self, db_session: Session): 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)) updated = update_plan(db_session, plan.id, PlanUpdate(price=300))
assert updated.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): 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) delete_plan(db_session, plan.id)
assert plan.deleted_at is not None assert plan.deleted_at is not None
@ -63,26 +50,26 @@ class TestService:
assert exc.value.status_code == 404 assert exc.value.status_code == 404
def test_soft_deleted_excluded(self, db_session: Session): def test_soft_deleted_excluded(self, db_session: Session):
create_plan(db_session, PlanCreate(code="KEEP", name="Keep", price=0)) create_plan(db_session, PlanCreate(name="Keep", price=0))
plan = create_plan(db_session, PlanCreate(code="GONE", name="Gone", price=0)) plan = create_plan(db_session, PlanCreate(name="Gone", price=0))
delete_plan(db_session, plan.id) delete_plan(db_session, plan.id)
assert len(get_plan_list(db_session)) == 1 assert len(get_plan_list(db_session)) == 1
class TestAPI: class TestAPI:
def test_create(self, client: TestClient): 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 assert resp.status_code == 201
def test_list(self, client: TestClient, db_session: Session): 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() db_session.commit()
resp = client.get("/v1/plans/") resp = client.get("/v1/plans/")
assert resp.status_code == 200 assert resp.status_code == 200
assert len(resp.json()) == 1 assert len(resp.json()) == 1
def test_get_by_id(self, client: TestClient, db_session: Session): 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.add(plan)
db_session.commit() db_session.commit()
resp = client.get(f"/v1/plans/{plan.id}") resp = client.get(f"/v1/plans/{plan.id}")
@ -94,7 +81,7 @@ class TestAPI:
assert resp.json() == {"message": "The item does not exist"} assert resp.json() == {"message": "The item does not exist"}
def test_update(self, client: TestClient, db_session: Session): 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.add(plan)
db_session.commit() db_session.commit()
resp = client.put(f"/v1/plans/{plan.id}", json={"price": 999}) resp = client.put(f"/v1/plans/{plan.id}", json={"price": 999})
@ -102,7 +89,7 @@ class TestAPI:
assert resp.json()["price"] == 999 assert resp.json()["price"] == 999
def test_delete(self, client: TestClient, db_session: Session): 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.add(plan)
db_session.commit() db_session.commit()
resp = client.delete(f"/v1/plans/{plan.id}") resp = client.delete(f"/v1/plans/{plan.id}")