feat: initial commit

This commit is contained in:
2026-03-05 22:12:38 +01:00
commit bce762a783
380 changed files with 51955 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
"""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)