Files
HartOMat/backend/app/api/routers/pricing.py
T
2026-03-05 22:12:38 +01:00

153 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)