feat: initial commit
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user