"""Vape API (datamodel-health-vape.md §8.4) — thin routing layer. Static sub-paths (/mixes/calculator, /coils/stats, /coils/change) are declared before their `{id}` siblings so FastAPI never routes them to the item handler. """ from datetime import date from typing import Annotated from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from app.core.database import get_db from app.core.dependencies import get_current_user from app.core.pagination import Page, PageParams from app.modules.auth.models import User from app.modules.vape import service from app.modules.vape.models import LiquidEntryKind, ProductKind from app.modules.vape.schemas import ( CoilChangeCreate, CoilChangeCreated, CoilChangeRead, CoilChangeUpdate, LiquidEntryCreate, LiquidEntryRead, LiquidEntryUpdate, MilestoneRead, MixCalculatorRequest, MixCalculatorResult, MixCreate, MixRead, MixUpdate, ProductCreate, ProductRead, ProductUpdate, PurchaseCreate, PurchaseRead, PurchaseUpdate, StatsResponse, VapeDashboard, VapeSettingsPut, VapeSettingsRead, ) router = APIRouter(prefix="/vape", tags=["vape"]) DbDep = Annotated[Session, Depends(get_db)] UserDep = Annotated[User, Depends(get_current_user)] PageDep = Annotated[PageParams, Depends()] FromQuery = Annotated[date | None, Query(alias="from")] ToQuery = Annotated[date | None, Query()] TzQuery = Annotated[str | None, Query()] SortQuery = Annotated[str | None, Query()] # --------------------------------------------------------------------- settings @router.get("/settings", response_model=VapeSettingsRead) def read_settings(db: DbDep, user: UserDep) -> VapeSettingsRead: return VapeSettingsRead.model_validate(service.get_settings(db, user.id)) @router.put("/settings", response_model=VapeSettingsRead) def write_settings( payload: VapeSettingsPut, db: DbDep, user: UserDep ) -> VapeSettingsRead: return VapeSettingsRead.model_validate(service.put_settings(db, user.id, payload)) # --------------------------------------------------------------------- products @router.get("/products", response_model=Page[ProductRead]) def list_products( db: DbDep, user: UserDep, params: PageDep, kind: Annotated[ProductKind | None, Query()] = None, include_archived: Annotated[bool, Query()] = False, sort: SortQuery = None, ) -> Page[ProductRead]: return service.list_products(db, user.id, params, kind, include_archived, sort) @router.post("/products", response_model=ProductRead, status_code=201) def create_product(payload: ProductCreate, db: DbDep, user: UserDep) -> ProductRead: return service.create_product(db, user.id, payload) @router.patch("/products/{product_id}", response_model=ProductRead) def update_product( product_id: int, payload: ProductUpdate, db: DbDep, user: UserDep ) -> ProductRead: return service.update_product(db, user.id, product_id, payload) @router.delete("/products/{product_id}", status_code=204) def delete_product(product_id: int, db: DbDep, user: UserDep) -> None: service.delete_product(db, user.id, product_id) # ------------------------------------------------------------------------ mixes @router.get("/mixes", response_model=Page[MixRead]) def list_mixes( db: DbDep, user: UserDep, params: PageDep, include_archived: Annotated[bool, Query()] = False, sort: SortQuery = None, ) -> Page[MixRead]: return service.list_mixes(db, user.id, params, include_archived, sort) @router.post("/mixes", response_model=MixRead, status_code=201) def create_mix(payload: MixCreate, db: DbDep, user: UserDep) -> MixRead: return service.create_mix(db, user.id, payload) @router.post("/mixes/calculator", response_model=MixCalculatorResult) def mix_calculator( payload: MixCalculatorRequest, db: DbDep, user: UserDep ) -> MixCalculatorResult: """Stateless recipe assistant — persists nothing (§6.3).""" return service.mix_calculator(db, user.id, payload) @router.post("/mixes/{mix_id}/activate", response_model=MixRead) def activate_mix(mix_id: int, db: DbDep, user: UserDep) -> MixRead: return service.activate_mix(db, user.id, mix_id) @router.patch("/mixes/{mix_id}", response_model=MixRead) def update_mix(mix_id: int, payload: MixUpdate, db: DbDep, user: UserDep) -> MixRead: return service.update_mix(db, user.id, mix_id, payload) @router.delete("/mixes/{mix_id}", status_code=204) def delete_mix(mix_id: int, db: DbDep, user: UserDep) -> None: service.delete_mix(db, user.id, mix_id) # ---------------------------------------------------------------------- liquids @router.get("/liquids", response_model=Page[LiquidEntryRead]) def list_liquids( db: DbDep, user: UserDep, params: PageDep, from_: FromQuery = None, to: ToQuery = None, kind: Annotated[LiquidEntryKind | None, Query()] = None, sort: SortQuery = None, ) -> Page[LiquidEntryRead]: return service.list_liquids(db, user.id, params, from_, to, kind, sort) @router.post("/liquids", response_model=LiquidEntryRead, status_code=201) def create_liquid( payload: LiquidEntryCreate, db: DbDep, user: UserDep ) -> LiquidEntryRead: return service.create_liquid(db, user.id, payload) @router.patch("/liquids/{entry_id}", response_model=LiquidEntryRead) def update_liquid( entry_id: int, payload: LiquidEntryUpdate, db: DbDep, user: UserDep ) -> LiquidEntryRead: return service.update_liquid(db, user.id, entry_id, payload) @router.delete("/liquids/{entry_id}", status_code=204) def delete_liquid(entry_id: int, db: DbDep, user: UserDep) -> None: service.delete_liquid(db, user.id, entry_id) # ------------------------------------------------------------------------ coils @router.get("/coils", response_model=Page[CoilChangeRead]) def list_coils( db: DbDep, user: UserDep, params: PageDep, from_: FromQuery = None, to: ToQuery = None, sort: SortQuery = None, tz: TzQuery = None, ) -> Page[CoilChangeRead]: return service.list_coils(db, user.id, params, from_, to, sort, tz) @router.get("/coils/stats", response_model=StatsResponse) def coil_stats(db: DbDep, user: UserDep, tz: TzQuery = None) -> StatsResponse: return service.coil_stats(db, user.id, tz) @router.post("/coils/change", response_model=CoilChangeCreated, status_code=201) def quick_coil_change( db: DbDep, user: UserDep, payload: CoilChangeCreate | None = None, tz: TzQuery = None, ) -> CoilChangeCreated: """One-click « Résistance changée » : registers a change at `now` (§12.6).""" return service.create_coil(db, user.id, payload or CoilChangeCreate(), tz) @router.post("/coils", response_model=CoilChangeCreated, status_code=201) def create_coil( payload: CoilChangeCreate, db: DbDep, user: UserDep, tz: TzQuery = None ) -> CoilChangeCreated: return service.create_coil(db, user.id, payload, tz) @router.patch("/coils/{coil_id}", response_model=CoilChangeRead) def update_coil( coil_id: int, payload: CoilChangeUpdate, db: DbDep, user: UserDep, tz: TzQuery = None, ) -> CoilChangeRead: return service.update_coil(db, user.id, coil_id, payload, tz) @router.delete("/coils/{coil_id}", status_code=204) def delete_coil(coil_id: int, db: DbDep, user: UserDep) -> None: service.delete_coil(db, user.id, coil_id) # -------------------------------------------------------------------- purchases @router.get("/purchases", response_model=Page[PurchaseRead]) def list_purchases( db: DbDep, user: UserDep, params: PageDep, from_: FromQuery = None, to: ToQuery = None, product_id: Annotated[int | None, Query()] = None, sort: SortQuery = None, ) -> Page[PurchaseRead]: return service.list_purchases(db, user.id, params, from_, to, product_id, sort) @router.post("/purchases", response_model=PurchaseRead, status_code=201) def create_purchase(payload: PurchaseCreate, db: DbDep, user: UserDep) -> PurchaseRead: return service.create_purchase(db, user.id, payload) @router.patch("/purchases/{purchase_id}", response_model=PurchaseRead) def update_purchase( purchase_id: int, payload: PurchaseUpdate, db: DbDep, user: UserDep ) -> PurchaseRead: return service.update_purchase(db, user.id, purchase_id, payload) @router.delete("/purchases/{purchase_id}", status_code=204) def delete_purchase(purchase_id: int, db: DbDep, user: UserDep) -> None: service.delete_purchase(db, user.id, purchase_id) # ------------------------------------------------------------- stats & overview @router.get("/stats/consumption", response_model=StatsResponse) def consumption_stats( db: DbDep, user: UserDep, from_: FromQuery = None, to: ToQuery = None, tz: TzQuery = None, ) -> StatsResponse: return service.consumption_stats(db, user.id, from_, to, tz) @router.get("/stats/nicotine", response_model=StatsResponse) def nicotine_stats( db: DbDep, user: UserDep, from_: FromQuery = None, to: ToQuery = None, tz: TzQuery = None, ) -> StatsResponse: return service.nicotine_stats(db, user.id, from_, to, tz) @router.get("/stats/costs", response_model=StatsResponse) def cost_stats( db: DbDep, user: UserDep, from_: FromQuery = None, to: ToQuery = None, tz: TzQuery = None, ) -> StatsResponse: return service.cost_stats(db, user.id, from_, to, tz) @router.get("/stats/savings", response_model=StatsResponse) def savings_stats( db: DbDep, user: UserDep, from_: FromQuery = None, to: ToQuery = None, tz: TzQuery = None, ) -> StatsResponse: return service.savings_stats(db, user.id, from_, to, tz) @router.get("/milestones", response_model=list[MilestoneRead]) def milestones(db: DbDep, user: UserDep, tz: TzQuery = None) -> list[MilestoneRead]: return service.milestones(db, user.id, tz) @router.get("/dashboard", response_model=VapeDashboard) def dashboard(db: DbDep, user: UserDep, tz: TzQuery = None) -> VapeDashboard: return service.dashboard(db, user.id, tz)