import json from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks from sqlalchemy.orm import Session from typing import List, Optional from database import get_db from models.order import Order, OrderItem, OrderWaiter from models.user import User, AssistantAssignment from models.product import Product from schemas.order import OrderCreate, OrderOut, OrderItemOut, AddItemsRequest, PayItemsRequest, AssignWaiterRequest from routers.deps import get_current_user, require_manager from services.printer_service import route_and_print router = APIRouter() def _can_access_order(order: Order, user: User, db: Session) -> bool: if user.role in ("manager", "sysadmin"): return True if order.opened_by == user.id: return True if any(ow.waiter_id == user.id for ow in order.waiters): return True # Assistant check: user is assistant to any waiter assigned to this order assigned_ids = {ow.waiter_id for ow in order.waiters} assistant_of = db.query(AssistantAssignment).filter( AssistantAssignment.assistant_waiter_id == user.id, AssistantAssignment.primary_waiter_id.in_(assigned_ids), ).first() return assistant_of is not None @router.get("/", response_model=List[OrderOut]) def list_orders( order_status: Optional[str] = None, waiter_id: Optional[int] = None, db: Session = Depends(get_db), user: User = Depends(require_manager), ): q = db.query(Order) if order_status: q = q.filter(Order.status == order_status) if waiter_id: q = q.join(OrderWaiter).filter(OrderWaiter.waiter_id == waiter_id) return q.all() @router.get("/my", response_model=List[OrderOut]) def my_orders(db: Session = Depends(get_db), user: User = Depends(get_current_user)): direct = db.query(Order).join(OrderWaiter).filter( OrderWaiter.waiter_id == user.id, Order.status.in_(["open", "partially_paid"]), ).all() # Also orders where user is opener but not explicitly assigned also_opened = db.query(Order).filter( Order.opened_by == user.id, Order.status.in_(["open", "partially_paid"]), ).all() seen = {o.id for o in direct} return direct + [o for o in also_opened if o.id not in seen] @router.get("/{order_id}", response_model=OrderOut) def get_order(order_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)): order = db.query(Order).filter(Order.id == order_id).first() if not order: raise HTTPException(status_code=404, detail="Order not found") if not _can_access_order(order, user, db): raise HTTPException(status_code=403, detail="Access denied") return order @router.post("/", response_model=OrderOut, status_code=status.HTTP_201_CREATED) def open_order(body: OrderCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user)): existing = db.query(Order).filter( Order.table_id == body.table_id, Order.status.in_(["open", "partially_paid"]), ).first() if existing: raise HTTPException(status_code=400, detail="Table already has an open order") order = Order(table_id=body.table_id, opened_by=user.id) db.add(order) db.flush() db.add(OrderWaiter(order_id=order.id, waiter_id=user.id)) db.commit() db.refresh(order) return order @router.post("/{order_id}/items", response_model=OrderOut) def add_items( order_id: int, body: AddItemsRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): order = db.query(Order).filter(Order.id == order_id).first() if not order: raise HTTPException(status_code=404, detail="Order not found") if not _can_access_order(order, user, db): raise HTTPException(status_code=403, detail="Access denied") if order.status not in ("open", "partially_paid"): raise HTTPException(status_code=400, detail="Order is not open") new_item_ids = [] for item_in in body.items: product = db.query(Product).filter(Product.id == item_in.product_id).first() if not product or not product.is_available: raise HTTPException(status_code=400, detail=f"Product {item_in.product_id} not available") item = OrderItem( order_id=order_id, product_id=item_in.product_id, added_by=user.id, quantity=item_in.quantity, unit_price=product.base_price, # price snapshot selected_options=json.dumps(item_in.selected_options) if item_in.selected_options else None, removed_ingredients=json.dumps(item_in.removed_ingredients) if item_in.removed_ingredients else None, notes=item_in.notes, ) db.add(item) db.flush() new_item_ids.append(item.id) db.commit() db.refresh(order) # Printer routing runs in background — must never block the order save background_tasks.add_task(route_and_print, order_id, new_item_ids) return order @router.put("/{order_id}/items/{item_id}", response_model=OrderItemOut) def edit_item(order_id: int, item_id: int, notes: Optional[str] = None, db: Session = Depends(get_db), user: User = Depends(require_manager)): item = db.query(OrderItem).filter(OrderItem.id == item_id, OrderItem.order_id == order_id).first() if not item: raise HTTPException(status_code=404, detail="Item not found") if notes is not None: item.notes = notes db.commit() db.refresh(item) return item @router.delete("/{order_id}/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT) def cancel_item(order_id: int, item_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): item = db.query(OrderItem).filter(OrderItem.id == item_id, OrderItem.order_id == order_id).first() if not item: raise HTTPException(status_code=404, detail="Item not found") item.status = "cancelled" db.commit() @router.post("/{order_id}/pay") def pay_items(order_id: int, body: PayItemsRequest, db: Session = Depends(get_db), user: User = Depends(get_current_user)): order = db.query(Order).filter(Order.id == order_id).first() if not order: raise HTTPException(status_code=404, detail="Order not found") if not _can_access_order(order, user, db): raise HTTPException(status_code=403, detail="Access denied") items = db.query(OrderItem).filter( OrderItem.id.in_(body.item_ids), OrderItem.order_id == order_id, OrderItem.status == "active", ).all() for item in items: item.status = "paid" active_remaining = db.query(OrderItem).filter( OrderItem.order_id == order_id, OrderItem.status == "active" ).count() order.status = "paid" if active_remaining == 0 else "partially_paid" db.commit() return {"status": order.status, "paid_item_ids": [i.id for i in items]} @router.post("/{order_id}/close") def close_order(order_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)): order = db.query(Order).filter(Order.id == order_id).first() if not order: raise HTTPException(status_code=404, detail="Order not found") if not _can_access_order(order, user, db): raise HTTPException(status_code=403, detail="Access denied") if order.status not in ("paid", "open", "partially_paid"): raise HTTPException(status_code=400, detail="Cannot close order in current status") order.status = "closed" order.closed_at = datetime.utcnow() order.closed_by = user.id db.commit() return {"status": "closed"} @router.delete("/{order_id}", status_code=status.HTTP_204_NO_CONTENT) def cancel_order(order_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): order = db.query(Order).filter(Order.id == order_id).first() if not order: raise HTTPException(status_code=404, detail="Order not found") order.status = "cancelled" order.closed_at = datetime.utcnow() order.closed_by = user.id db.commit() @router.put("/{order_id}/assign-waiter") def assign_waiter(order_id: int, body: AssignWaiterRequest, db: Session = Depends(get_db), user: User = Depends(require_manager)): order = db.query(Order).filter(Order.id == order_id).first() if not order: raise HTTPException(status_code=404, detail="Order not found") existing = db.query(OrderWaiter).filter( OrderWaiter.order_id == order_id, OrderWaiter.waiter_id == body.waiter_id ).first() if existing: raise HTTPException(status_code=400, detail="Waiter already assigned") db.add(OrderWaiter(order_id=order_id, waiter_id=body.waiter_id)) db.commit() return {"status": "assigned"} @router.delete("/{order_id}/waiters/{waiter_id}", status_code=status.HTTP_204_NO_CONTENT) def remove_waiter(order_id: int, waiter_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): assignment = db.query(OrderWaiter).filter( OrderWaiter.order_id == order_id, OrderWaiter.waiter_id == waiter_id ).first() if not assignment: raise HTTPException(status_code=404, detail="Assignment not found") db.delete(assignment) db.commit()