Remove 'code' field from Plan model, schemas, factory, and service; update related tests and API endpoints
This commit is contained in:
parent
5772a3948d
commit
7dceb01d89
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
45
docs/history/2026-07-24-hapus-kode-plan.md
Normal file
45
docs/history/2026-07-24-hapus-kode-plan.md
Normal 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
|
||||||
@ -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}")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user