import json from fastapi import APIRouter, Depends, HTTPException, Header, status from passlib.context import CryptContext from sqlalchemy.orm import Session from database import get_db from models.site import Site from models.online_order import OnlineOrder from schemas.online_order import ( OnlineOrderCreate, OnlineOrderCreated, OrderStatusOut, PendingOrderOut, OrderSyncedRequest, OrderStatusUpdateRequest, ) from auth_utils import get_current_admin # manager JWT checked separately in manager_auth router = APIRouter() _pwd = CryptContext(schemes=["bcrypt"], deprecated="auto") # Valid status transitions _TRANSITIONS: dict[str, set[str]] = { "pending_acceptance": {"accepted", "rejected"}, "accepted": {"preparing"}, "preparing": {"ready", "out_for_delivery"}, "ready": {"delivered"}, "out_for_delivery": {"delivered"}, } def _require_site( x_site_id: str = Header(..., alias="X-Site-ID"), x_site_key: str = Header(..., alias="X-Site-Key"), db: Session = Depends(get_db), ) -> Site: site = db.query(Site).filter(Site.site_id == x_site_id).first() if not site or not _pwd.verify(x_site_key, site.secret_key_hash): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid site credentials") return site # ── Public endpoints ────────────────────────────────────────────────────────── @router.post("/{site_slug}", response_model=OnlineOrderCreated, status_code=status.HTTP_201_CREATED) def create_order(site_slug: str, body: OnlineOrderCreate, db: Session = Depends(get_db)): """Customer submits an order from the public menu SPA.""" site = db.query(Site).filter(Site.site_id == site_slug).first() if not site: raise HTTPException(status_code=404, detail="Site not found") # Atomically increment counter and generate public_ref site.order_counter = (site.order_counter or 0) + 1 public_ref = f"ORD-{site.order_counter:04d}" order = OnlineOrder( site_id=site.id, public_ref=public_ref, order_type=body.order_type, customer_name=body.customer_name, customer_phone=body.customer_phone, customer_address=body.customer_address, customer_notes=body.customer_notes, items_json=json.dumps([item.model_dump() for item in body.items]), subtotal=body.subtotal, delivery_fee=body.delivery_fee, total=body.total, status="pending_acceptance", ) db.add(order) db.commit() db.refresh(order) return OnlineOrderCreated(order_id=order.id, public_ref=order.public_ref, status=order.status) @router.get("/status/{public_ref}", response_model=OrderStatusOut) def get_order_status(public_ref: str, db: Session = Depends(get_db)): """Customer polls this to track their order.""" order = db.query(OnlineOrder).filter(OnlineOrder.public_ref == public_ref).first() if not order: raise HTTPException(status_code=404, detail="Order not found") return OrderStatusOut( public_ref=order.public_ref, status=order.status, rejection_reason=order.rejection_reason, ) # ── Internal endpoints (site API key) ───────────────────────────────────────── @router.get("/pending/{site_id}", response_model=list[PendingOrderOut]) def get_pending_orders(site_id: int, site: Site = Depends(_require_site), db: Session = Depends(get_db)): """Return all orders not yet synced to the local backend.""" orders = ( db.query(OnlineOrder) .filter(OnlineOrder.site_id == site_id, OnlineOrder.synced_to_local == 0) .order_by(OnlineOrder.created_at) .all() ) return orders @router.post("/{order_id}/synced", status_code=status.HTTP_204_NO_CONTENT) def mark_order_synced( order_id: int, body: OrderSyncedRequest, site: Site = Depends(_require_site), db: Session = Depends(get_db), ): """Local backend calls this after it has created the order locally.""" order = db.query(OnlineOrder).filter( OnlineOrder.id == order_id, OnlineOrder.site_id == site.id ).first() if not order: raise HTTPException(status_code=404, detail="Order not found") order.synced_to_local = 1 order.local_order_id = body.local_order_id db.commit() @router.patch("/{order_id}/status", status_code=status.HTTP_204_NO_CONTENT) def update_order_status( order_id: int, body: OrderStatusUpdateRequest, site: Site = Depends(_require_site), db: Session = Depends(get_db), ): """Update order status. Called by local_backend after staff accept/reject/progress.""" order = db.query(OnlineOrder).filter( OnlineOrder.id == order_id, OnlineOrder.site_id == site.id ).first() if not order: raise HTTPException(status_code=404, detail="Order not found") allowed = _TRANSITIONS.get(order.status, set()) if body.status not in allowed: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Cannot transition from '{order.status}' to '{body.status}'", ) order.status = body.status if body.rejection_reason: order.rejection_reason = body.rejection_reason db.commit()