order button
This commit is contained in:
209
trade-hub/backend/app/orders.py
Normal file
209
trade-hub/backend/app/orders.py
Normal 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)
|
||||
Reference in New Issue
Block a user