210 lines
6.4 KiB
Python
210 lines
6.4 KiB
Python
"""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)
|