import logging from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks from pydantic import BaseModel from sqlalchemy.orm import Session import httpx from config import settings from database import get_db from models.order import Order from schemas.order import OrderOut from routers.deps import get_current_user, require_manager from models.user import User from services.sse_bus import broadcast_sync logger = logging.getLogger(__name__) router = APIRouter() class RejectRequest(BaseModel): reason: Optional[str] = None class StatusUpdateRequest(BaseModel): status: str # "preparing" | "ready" | "out_for_delivery" | "delivered" # ── Helpers ─────────────────────────────────────────────────────────────────── def _get_online_order(order_id: int, db: Session) -> Order: order = db.query(Order).filter(Order.id == order_id, Order.source == "online").first() if not order: raise HTTPException(status_code=404, detail="Online order not found") return order def _cloud_headers() -> dict: return {"X-Site-ID": settings.SITE_ID, "X-Site-Key": settings.SITE_KEY} async def _mirror_status_to_cloud(cloud_order_ref: str, new_status: str, rejection_reason: str = None): """Best-effort push of status update to cloud. Failures are logged, not raised.""" if not settings.CLOUD_URL or not cloud_order_ref: return try: # We need the cloud order id — look it up by public_ref via the status endpoint # (The cloud orders router exposes PATCH /{order_id}/status by numeric id. # We store cloud id implicitly through public_ref; fetch it first.) async with httpx.AsyncClient(timeout=10) as client: # Get cloud order id via status lookup (public endpoint, no auth needed) status_resp = await client.get( f"{settings.CLOUD_URL}/api/orders/status/{cloud_order_ref}" ) if status_resp.status_code != 200: logger.warning("Could not find cloud order %s", cloud_order_ref) return # Search pending list to get numeric id — use the site-key protected route from middleware.license_check import license_state site_numeric_id = license_state.get("site_numeric_id") if not site_numeric_id: return pending_resp = await client.get( f"{settings.CLOUD_URL}/api/orders/pending/{site_numeric_id}", headers=_cloud_headers(), ) # The order may already be synced; look in all orders via remote dashboard # Simpler: use the manager-JWT-free internal PATCH directly via site key # We need the cloud order numeric id. Store it on first pull — for now, # the cloud also exposes PATCH via remote dashboard (manager JWT). # Best path: add a site-key variant. Until then, we mirror via the # existing PATCH endpoint using site key — works because orders.py # accepts site key auth on PATCH /{order_id}/status. # We get the numeric id from the pending list or a dedicated lookup. # Iterate pending to find our ref (may be empty if already synced). cloud_order_id = None if pending_resp.status_code == 200: for o in pending_resp.json(): if o.get("public_ref") == cloud_order_ref: cloud_order_id = o["id"] break if cloud_order_id is None: # Already synced — not in pending list. We can't easily look it up # without a dedicated search endpoint. Log and skip for now; the # remote manager dashboard's own PATCH handles the remote case. logger.debug("Cloud order %s not in pending list; skipping mirror", cloud_order_ref) return body = {"status": new_status} if rejection_reason: body["rejection_reason"] = rejection_reason patch_resp = await client.patch( f"{settings.CLOUD_URL}/api/orders/{cloud_order_id}/status", headers=_cloud_headers(), json=body, ) patch_resp.raise_for_status() logger.info("Mirrored status %s for cloud order %s", new_status, cloud_order_ref) except Exception as e: logger.warning("Failed to mirror status to cloud for %s: %s", cloud_order_ref, e) # ── Endpoints ───────────────────────────────────────────────────────────────── @router.get("/orders/incoming", response_model=list[OrderOut]) def get_incoming_orders( db: Session = Depends(get_db), user: User = Depends(get_current_user), ): """Return all online orders pending acceptance, newest first.""" orders = ( db.query(Order) .filter(Order.source == "online", Order.online_status == "pending_acceptance") .order_by(Order.opened_at.desc()) .all() ) return orders @router.post("/orders/{order_id}/accept", response_model=OrderOut) async def accept_order( order_id: int, background: BackgroundTasks, db: Session = Depends(get_db), user: User = Depends(require_manager), ): order = _get_online_order(order_id, db) if order.online_status != "pending_acceptance": raise HTTPException(status_code=400, detail=f"Order is already '{order.online_status}'") order.online_status = "accepted" db.commit() db.refresh(order) background.add_task(_mirror_status_to_cloud, order.online_order_ref, "accepted") broadcast_sync("online_order_updated", {"order_id": order_id, "status": "accepted"}) return order @router.post("/orders/{order_id}/reject", response_model=OrderOut) async def reject_order( order_id: int, body: RejectRequest, background: BackgroundTasks, db: Session = Depends(get_db), user: User = Depends(require_manager), ): order = _get_online_order(order_id, db) if order.online_status != "pending_acceptance": raise HTTPException(status_code=400, detail=f"Order is already '{order.online_status}'") order.online_status = "rejected" order.status = "cancelled" db.commit() db.refresh(order) background.add_task(_mirror_status_to_cloud, order.online_order_ref, "rejected", body.reason) broadcast_sync("online_order_updated", {"order_id": order_id, "status": "rejected"}) return order @router.post("/orders/{order_id}/status", response_model=OrderOut) async def update_order_status( order_id: int, body: StatusUpdateRequest, background: BackgroundTasks, db: Session = Depends(get_db), user: User = Depends(require_manager), ): allowed = {"preparing", "ready", "out_for_delivery", "delivered"} if body.status not in allowed: raise HTTPException(status_code=400, detail=f"Invalid status '{body.status}'") order = _get_online_order(order_id, db) order.online_status = body.status if body.status == "delivered": order.status = "closed" db.commit() db.refresh(order) background.add_task(_mirror_status_to_cloud, order.online_order_ref, body.status) broadcast_sync("online_order_updated", {"order_id": order_id, "status": body.status}) return order