"""Public order board. Any logged-in user can place an order (a request) for a catalog item. Every order is visible to everyone, with the requester's display name, quantity, notes, and — for quality-applicable ores — the requested quality. Orders are read-only for the public; only the owner can edit or remove their own order. Like inventory rows, orders reference catalog items by `class_name` only, so a dataset refresh cannot break them. """ import math from fastapi import APIRouter, Depends, HTTPException, Query, Response from sqlalchemy import select from sqlalchemy.orm import Session as DbSession from app import catalog from app.db import get_db from app.deps import get_current_user, require_same_origin from app.inventory import SORT_KEYS from app.models import Order, User from app.schemas import OrderCreateRequest, OrderOut, OrderPage, OrderPatchRequest router = APIRouter(prefix="/api/v1/orders", tags=["orders"]) MAX_PAGE_SIZE = 100 def enrich_order(order: Order, username: str) -> dict: item = catalog.store.by_class_name.get(order.class_name) if item is None: item_data = { "name": order.class_name.replace("_", " "), "category": None, "type": None, "size": None, "grade": None, "manufacturer": None, "quality_applicable": False, "available": False, } else: item_data = { "name": item["name"], "category": item["category"], "type": item["type"], "size": item["size"], "grade": item["grade"], "manufacturer": item["manufacturer"], "quality_applicable": item.get("quality_applicable", False), "available": True, } return { "id": order.id, "class_name": order.class_name, **item_data, "quantity": order.quantity, "notes": order.notes, "quality": order.quality, "username": username, "status": order.status, "created_at": order.created_at, "updated_at": order.updated_at, } def _validate_quality(item: dict, quality: int | None) -> None: if quality is not None and not item.get("quality_applicable", False): raise HTTPException( status_code=422, detail=f"Quality does not apply to '{item['name']}'", ) @router.get("", response_model=OrderPage) def list_orders( category: str | None = None, search: str | None = None, status: str | None = Query(default=None, pattern="^(open|fulfilled)$"), sort: str = Query(default="updated", pattern="^(name|category|quantity|updated)$"), page: int = Query(default=1, ge=1), page_size: int = Query(default=25, ge=1), db: DbSession = Depends(get_db), ): page_size = min(page_size, MAX_PAGE_SIZE) rows = db.execute( select(Order, User.username).join(User, User.id == Order.user_id) ).all() items = [enrich_order(order, username) for order, username in rows] if status is not None: items = [i for i in items if i["status"] == status] if category is not None and category.strip(): wanted = category.strip() items = [i for i in items if i["category"] == wanted] if search is not None and search.strip(): needle = search.strip().lower() items = [ i for i in items if needle in i["name"].lower() or needle in i["class_name"].lower() or needle in i["username"].lower() ] items.sort(key=SORT_KEYS[sort]) total = len(items) total_pages = max(1, math.ceil(total / page_size)) start = (page - 1) * page_size return { "total": total, "page": page, "page_size": page_size, "total_pages": total_pages, "items": items[start : start + page_size], } @router.post( "", response_model=OrderOut, status_code=201, dependencies=[Depends(require_same_origin)], ) def create_order( payload: OrderCreateRequest, user: User = Depends(get_current_user), db: DbSession = Depends(get_db), ): class_name = payload.class_name.strip() item = catalog.store.by_class_name.get(class_name) if item is None: raise HTTPException( status_code=404, detail=f"Catalog item '{class_name}' not found" ) _validate_quality(item, payload.quality) order = Order( user_id=user.id, class_name=class_name, quantity=payload.quantity, notes=payload.notes, quality=payload.quality, ) db.add(order) db.commit() db.refresh(order) return enrich_order(order, user.username) def _own_order(db: DbSession, user: User, order_id: int) -> Order: order = db.get(Order, order_id) if order is None or order.user_id != user.id: # Do not reveal whether somebody else's order exists. raise HTTPException(status_code=404, detail="Order not found") return order @router.patch( "/{order_id}", response_model=OrderOut, dependencies=[Depends(require_same_origin)], ) def update_order( order_id: int, payload: OrderPatchRequest, user: User = Depends(get_current_user), db: DbSession = Depends(get_db), ): order = _own_order(db, user, order_id) if payload.quantity is not None: order.quantity = payload.quantity if payload.notes is not None: order.notes = payload.notes if payload.status is not None: order.status = payload.status if "quality" in payload.model_fields_set: # Explicit null clears the value; omitted keeps the existing one. if payload.quality is not None: item = catalog.store.by_class_name.get(order.class_name) if item is None: raise HTTPException( status_code=422, detail=f"Quality does not apply to '{order.class_name}'", ) _validate_quality(item, payload.quality) order.quality = payload.quality db.commit() db.refresh(order) return enrich_order(order, user.username) @router.delete( "/{order_id}", status_code=204, dependencies=[Depends(require_same_origin)], ) def delete_order( order_id: int, user: User = Depends(get_current_user), db: DbSession = Depends(get_db), ): order = _own_order(db, user, order_id) db.delete(order) db.commit() return Response(status_code=204)