diff --git a/app/factories/plan_factory.py b/app/factories/plan_factory.py index 40a43eb..70934a5 100644 --- a/app/factories/plan_factory.py +++ b/app/factories/plan_factory.py @@ -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, diff --git a/app/models/plan.py b/app/models/plan.py index f0e76c3..402c17b 100644 --- a/app/models/plan.py +++ b/app/models/plan.py @@ -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) diff --git a/app/routers/plans.py b/app/routers/plans.py index d3be617..98d002b 100644 --- a/app/routers/plans.py +++ b/app/routers/plans.py @@ -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) diff --git a/app/schemas/plan.py b/app/schemas/plan.py index 8187e37..b7bc822 100644 --- a/app/schemas/plan.py +++ b/app/schemas/plan.py @@ -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 diff --git a/app/services/plan_service.py b/app/services/plan_service.py index 1217fb0..f7c5ee6 100644 --- a/app/services/plan_service.py +++ b/app/services/plan_service.py @@ -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 diff --git a/docs/history/2026-07-24-hapus-kode-plan.md b/docs/history/2026-07-24-hapus-kode-plan.md new file mode 100644 index 0000000..3361db9 --- /dev/null +++ b/docs/history/2026-07-24-hapus-kode-plan.md @@ -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 diff --git a/tests/test_plan.py b/tests/test_plan.py index 3c3ffad..45b1f3c 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -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}")