"""Pricing tiers router — CRUD for category × quality-level price configuration.""" from datetime import datetime from decimal import Decimal from typing import Optional import uuid from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from sqlalchemy import select, update as sql_update from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models.pricing_tier import PricingTier from app.models.user import User from app.utils.auth import require_admin_or_pm, get_current_user router = APIRouter(prefix="/pricing", tags=["pricing"]) # ── Schemas ──────────────────────────────────────────────────────────────────── class PricingTierOut(BaseModel): id: int category_key: str quality_level: str price_per_item: float description: Optional[str] is_active: bool created_at: datetime updated_at: datetime model_config = {"from_attributes": True} class PricingTierCreate(BaseModel): category_key: str quality_level: str = "Normal" price_per_item: Decimal description: Optional[str] = None is_active: bool = True class PricingTierPatch(BaseModel): category_key: Optional[str] = None quality_level: Optional[str] = None price_per_item: Optional[Decimal] = None description: Optional[str] = None is_active: Optional[bool] = None # ── Endpoints ────────────────────────────────────────────────────────────────── @router.get("", response_model=list[PricingTierOut]) async def list_pricing_tiers( _user: User = Depends(require_admin_or_pm), db: AsyncSession = Depends(get_db), ) -> list[PricingTierOut]: result = await db.execute( select(PricingTier).order_by(PricingTier.category_key, PricingTier.quality_level) ) return result.scalars().all() @router.post("", response_model=PricingTierOut, status_code=status.HTTP_201_CREATED) async def create_pricing_tier( body: PricingTierCreate, _user: User = Depends(require_admin_or_pm), db: AsyncSession = Depends(get_db), ) -> PricingTierOut: tier = PricingTier( category_key=body.category_key, quality_level=body.quality_level, price_per_item=body.price_per_item, description=body.description, is_active=body.is_active, ) db.add(tier) try: await db.commit() await db.refresh(tier) except IntegrityError: await db.rollback() raise HTTPException( status_code=409, detail=f"Pricing tier for '{body.category_key}' / '{body.quality_level}' already exists", ) return tier @router.patch("/{tier_id}", response_model=PricingTierOut) async def update_pricing_tier( tier_id: int, body: PricingTierPatch, _user: User = Depends(require_admin_or_pm), db: AsyncSession = Depends(get_db), ) -> PricingTierOut: result = await db.execute(select(PricingTier).where(PricingTier.id == tier_id)) tier = result.scalar_one_or_none() if tier is None: raise HTTPException(status_code=404, detail="Pricing tier not found") patch = body.model_dump(exclude_unset=True) if patch: patch["updated_at"] = datetime.utcnow() await db.execute( sql_update(PricingTier).where(PricingTier.id == tier_id).values(**patch) ) await db.commit() result = await db.execute(select(PricingTier).where(PricingTier.id == tier_id)) tier = result.scalar_one() return tier @router.delete("/{tier_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_pricing_tier( tier_id: int, _user: User = Depends(require_admin_or_pm), db: AsyncSession = Depends(get_db), ) -> None: result = await db.execute(select(PricingTier).where(PricingTier.id == tier_id)) tier = result.scalar_one_or_none() if tier is None: raise HTTPException(status_code=404, detail="Pricing tier not found") await db.delete(tier) await db.commit() # ── Price Estimation ────────────────────────────────────────────────────────── class EstimateLineInput(BaseModel): product_id: uuid.UUID output_type_id: uuid.UUID | None = None class EstimateRequest(BaseModel): lines: list[EstimateLineInput] @router.post("/estimate") async def estimate_price( body: EstimateRequest, _user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Estimate the total price for a set of prospective order lines. Open to all authenticated users (read-only, needed by wizard). """ from app.services.pricing_service import estimate_order_price lines_dicts = [{"product_id": str(l.product_id), "output_type_id": str(l.output_type_id) if l.output_type_id else None} for l in body.lines] return await estimate_order_price(db, lines_dicts)