from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from typing import List, Optional from database import get_db from models.tabs import Tab, TabEntry, TabPayment from models.customers import Customer from models.order import Order, OrderItem from models.user import User from schemas.tabs import TabOut, TabEntryOut, TabPaymentOut, TabPayRequest, TabForgiveRequest, TabCloseRequest from routers.deps import require_manager router = APIRouter() def _utcnow(): return datetime.now(timezone.utc) def _tab_out(tab: Tab) -> dict: c = tab.customer customer_name = c.name if c else None if c and c.nickname: customer_name = f"{c.name} «{c.nickname}»" customer_phone = c.phone if c else None total_charged = round(sum(e.amount for e in tab.entries), 2) total_paid = round(sum(p.amount for p in tab.payments), 2) balance = round(total_charged - total_paid, 2) closed_by_name = None if tab.closed_by: closed_by_name = tab.closed_by.full_name or tab.closed_by.username entries_out = [] for e in sorted(tab.entries, key=lambda x: x.created_at): creator = e.created_by.full_name or e.created_by.username if e.created_by else None entries_out.append({ "id": e.id, "tab_id": e.tab_id, "order_id": e.order_id, "order_item_id": e.order_item_id, "amount": e.amount, "description": e.description, "created_by_id": e.created_by_id, "created_by_name": creator, "created_at": e.created_at, }) payments_out = [] for p in sorted(tab.payments, key=lambda x: x.created_at): receiver = p.received_by.full_name or p.received_by.username if p.received_by else None payments_out.append({ "id": p.id, "tab_id": p.tab_id, "amount": p.amount, "payment_method": p.payment_method, "received_by_id": p.received_by_id, "received_by_name": receiver, "notes": p.notes, "created_at": p.created_at, }) return { "id": tab.id, "customer_id": tab.customer_id, "customer_name": customer_name, "customer_phone": customer_phone, "status": tab.status, "opened_at": tab.opened_at, "closed_at": tab.closed_at, "closed_by_id": tab.closed_by_id, "notes": tab.notes, "balance": balance, "total_charged": total_charged, "total_paid": total_paid, "entries": entries_out, "payments": payments_out, } @router.get("/", response_model=List[TabOut]) def list_tabs( tab_status: Optional[str] = None, customer_id: Optional[int] = None, db: Session = Depends(get_db), user: User = Depends(require_manager), ): q = db.query(Tab) if tab_status: q = q.filter(Tab.status == tab_status) else: q = q.filter(Tab.status == "open") # default: only open tabs if customer_id: q = q.filter(Tab.customer_id == customer_id) tabs = q.order_by(Tab.opened_at.desc()).all() result = [_tab_out(t) for t in tabs] # Sort open tabs by balance descending result.sort(key=lambda x: x["balance"], reverse=True) return result @router.get("/{tab_id}", response_model=TabOut) def get_tab(tab_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): tab = db.query(Tab).filter(Tab.id == tab_id).first() if not tab: raise HTTPException(status_code=404, detail="Tab not found") return _tab_out(tab) @router.post("/", response_model=TabOut, status_code=status.HTTP_201_CREATED) def open_tab(customer_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): customer = db.query(Customer).filter(Customer.id == customer_id, Customer.is_active == True).first() if not customer: raise HTTPException(status_code=404, detail="Customer not found") existing = db.query(Tab).filter(Tab.customer_id == customer_id, Tab.status == "open").first() if existing: raise HTTPException(status_code=400, detail="Customer already has an open tab") tab = Tab(customer_id=customer_id) db.add(tab) db.commit() db.refresh(tab) return _tab_out(tab) @router.post("/{tab_id}/entries", response_model=TabOut) def add_tab_entry( tab_id: int, order_id: Optional[int] = None, order_item_id: Optional[int] = None, amount: float = 0.0, description: Optional[str] = None, db: Session = Depends(get_db), user: User = Depends(require_manager), ): tab = db.query(Tab).filter(Tab.id == tab_id, Tab.status == "open").first() if not tab: raise HTTPException(status_code=404, detail="Open tab not found") if amount <= 0: raise HTTPException(status_code=400, detail="Amount must be positive") entry = TabEntry( tab_id=tab_id, order_id=order_id, order_item_id=order_item_id, amount=amount, description=description, created_by_id=user.id, ) db.add(entry) db.commit() db.refresh(tab) return _tab_out(tab) @router.post("/{tab_id}/pay", response_model=TabOut) def pay_tab( tab_id: int, body: TabPayRequest, db: Session = Depends(get_db), user: User = Depends(require_manager), ): tab = db.query(Tab).filter(Tab.id == tab_id, Tab.status == "open").first() if not tab: raise HTTPException(status_code=404, detail="Open tab not found") if body.amount <= 0: raise HTTPException(status_code=400, detail="Amount must be positive") total_charged = sum(e.amount for e in tab.entries) total_paid = sum(p.amount for p in tab.payments) balance = total_charged - total_paid if body.amount > balance + 0.005: raise HTTPException(status_code=400, detail=f"Payment exceeds balance ({balance:.2f})") payment = TabPayment( tab_id=tab_id, amount=body.amount, payment_method=body.payment_method, notes=body.notes, received_by_id=user.id, ) db.add(payment) db.commit() db.refresh(tab) return _tab_out(tab) @router.post("/{tab_id}/close", response_model=TabOut) def close_tab( tab_id: int, body: TabCloseRequest, db: Session = Depends(get_db), user: User = Depends(require_manager), ): tab = db.query(Tab).filter(Tab.id == tab_id, Tab.status == "open").first() if not tab: raise HTTPException(status_code=404, detail="Open tab not found") total_charged = sum(e.amount for e in tab.entries) total_paid = sum(p.amount for p in tab.payments) balance = round(total_charged - total_paid, 2) if balance > 0.005: raise HTTPException(status_code=400, detail=f"Tab still has a balance of €{balance:.2f} — pay it off first or use /forgive") tab.status = "closed" tab.closed_at = _utcnow() tab.closed_by_id = user.id if body.notes: tab.notes = body.notes db.commit() db.refresh(tab) return _tab_out(tab) @router.post("/{tab_id}/forgive", response_model=TabOut) def forgive_tab( tab_id: int, body: TabForgiveRequest, db: Session = Depends(get_db), user: User = Depends(require_manager), ): tab = db.query(Tab).filter(Tab.id == tab_id, Tab.status == "open").first() if not tab: raise HTTPException(status_code=404, detail="Open tab not found") tab.status = "forgiven" tab.closed_at = _utcnow() tab.closed_by_id = user.id if body.reason: tab.notes = body.reason db.commit() db.refresh(tab) return _tab_out(tab) # ── Customer tabs ────────────────────────────────────────────────────────────── @router.get("/customer/{customer_id}", response_model=List[TabOut]) def customer_tabs(customer_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): tabs = db.query(Tab).filter(Tab.customer_id == customer_id).order_by( Tab.status.asc(), # open first Tab.opened_at.desc(), ).all() return [_tab_out(t) for t in tabs]