added trade-hub role and files

This commit is contained in:
boxswapper
2026-11-13 12:47:35 -07:00
parent 6c9c48dd19
commit 2b72f23ea9
46 changed files with 316582 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
"""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)