from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import func from sqlalchemy.orm import Session from typing import Optional from datetime import datetime, timezone from database import get_db from models.business_day import BusinessDay from models.order import Order, OrderItem, OrderAuditLog from models.shift import WaiterShift from models.flag import TableFlagAssignment from models.message import StaffMessage, StaffMessageAck from schemas.business_day import BusinessDayOut, OpenBusinessDayRequest, CloseBusinessDayRequest from routers.deps import get_current_user, require_manager from models.user import User from middleware.license_check import license_state router = APIRouter() def _dt(dt): if dt is None: return None return (dt.isoformat() + "Z") if dt.tzinfo is None else dt.isoformat() @router.get("/current", response_model=Optional[BusinessDayOut]) def get_current_business_day( db: Session = Depends(get_db), user: User = Depends(get_current_user), ): return db.query(BusinessDay).filter(BusinessDay.status == "open").first() @router.post("/open", response_model=BusinessDayOut, status_code=status.HTTP_201_CREATED) def open_business_day( body: OpenBusinessDayRequest, db: Session = Depends(get_db), user: User = Depends(require_manager), ): existing = db.query(BusinessDay).filter(BusinessDay.status == "open").first() if existing: raise HTTPException(status_code=400, detail="A business day is already open") # Gate: admin lock (already enforced or pending) if license_state.get("locked") or license_state.get("lock_pending"): raise HTTPException( status_code=423, detail={ "code": "SYSTEM_LOCKED", "message": "Το σύστημα έχει κλειδωθεί από διαχειριστή. Επικοινωνήστε με την υποστήριξη.", }, ) # Gate: license expired and expiry grace period also over if not license_state.get("licensed", True): expires_at = license_state.get("expires_at", "") raise HTTPException( status_code=402, detail={ "code": "LICENSE_EXPIRED", "message": "Η άδεια χρήσης έχει λήξει. Ανανεώστε την άδεια ή επικοινωνήστε με την υποστήριξη.", "expires_at": expires_at, }, ) day = BusinessDay(opened_by_id=user.id, notes=body.notes) db.add(day) db.commit() db.refresh(day) return day @router.post("/close", response_model=BusinessDayOut) def close_business_day( body: CloseBusinessDayRequest, db: Session = Depends(get_db), user: User = Depends(require_manager), ): day = db.query(BusinessDay).filter(BusinessDay.status == "open").first() if not day: raise HTTPException(status_code=404, detail="No open business day") open_orders = db.query(Order).filter( Order.business_day_id == day.id, Order.status.in_(["open", "partially_paid"]), ).all() if open_orders and not body.force: # Count orders that have at least one active (unpaid) item — covers both # "open" (fully unpaid) and "partially_paid" (partially unpaid) orders. with_pending = sum( 1 for o in open_orders if db.query(OrderItem).filter( OrderItem.order_id == o.id, OrderItem.status == "active", ).first() is not None ) raise HTTPException( status_code=409, detail={ "message": f"{len(open_orders)} table(s) still open, {with_pending} with unpaid items.", "open_orders": len(open_orders), "partially_paid": with_pending, }, ) now = datetime.now(timezone.utc) # Close all non-terminal orders for this business day (open, partially_paid, paid) all_unclosed = db.query(Order).filter( Order.business_day_id == day.id, Order.status.in_(["open", "partially_paid", "paid"]), ).all() for order in all_unclosed: was_unpaid = order.status in ("open", "partially_paid") order.status = "closed" order.closed_at = now order.closed_by = user.id if was_unpaid: db.add(OrderAuditLog( order_id=order.id, event_type="ORDER_CLOSED", waiter_id=user.id, note="Force-closed at end of business day", )) active_shifts = db.query(WaiterShift).filter( WaiterShift.business_day_id == day.id, WaiterShift.ended_at == None, ).all() for shift in active_shifts: items = db.query(OrderItem).filter( OrderItem.paid_in_shift_id == shift.id, OrderItem.status == "paid", ).all() shift.total_collected = sum(i.unit_price * i.quantity for i in items) shift.ended_at = now # Clear all table flags and staff messages — fresh slate for the next day db.query(TableFlagAssignment).delete(synchronize_session=False) db.query(StaffMessageAck).delete(synchronize_session=False) db.query(StaffMessage).delete(synchronize_session=False) day.status = "closed" day.closed_at = now day.closed_by_id = user.id if body.notes: day.notes = body.notes db.commit() db.refresh(day) # Deferred lock: if cloud requested a lock while the workday was open, # enforce it now that the day has closed. if license_state.get("lock_pending"): license_state["lock_pending"] = False license_state["locked"] = True from services.cloud_sync import _persist_state _persist_state() return day @router.delete("/{day_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_business_day( day_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager), ): """Permanently delete a business day. Only allowed when it has no shifts.""" day = db.query(BusinessDay).filter(BusinessDay.id == day_id).first() if not day: raise HTTPException(status_code=404, detail="Business day not found") if day.status == "open": raise HTTPException(status_code=400, detail="Cannot delete an open business day — close it first") shift_count = db.query(WaiterShift).filter(WaiterShift.business_day_id == day_id).count() if shift_count > 0: raise HTTPException( status_code=409, detail=f"Δεν είναι δυνατή η διαγραφή: η εργάσιμη μέρα έχει {shift_count} βάρδια/ες. Διαγράψτε πρώτα όλες τις βάρδιες.", ) db.query(Order).filter(Order.business_day_id == day_id).update( {"business_day_id": None}, synchronize_session=False ) db.delete(day) db.commit() @router.get("/history") def business_day_history( db: Session = Depends(get_db), user: User = Depends(require_manager), ): days = db.query(BusinessDay).order_by(BusinessDay.opened_at.desc()).all() result = [] for day in days: order_count = db.query(Order).filter(Order.business_day_id == day.id).count() revenue = ( db.query(func.sum(OrderItem.unit_price * OrderItem.quantity)) .join(Order) .filter(Order.business_day_id == day.id, OrderItem.status == "paid") .scalar() or 0.0 ) w_opener = day.opener w_closer = day.closer result.append({ "id": day.id, "status": day.status, "opened_at": _dt(day.opened_at), "closed_at": _dt(day.closed_at), "opened_by_id": day.opened_by_id, "opened_by_name": (w_opener.full_name or w_opener.username) if w_opener else None, "closed_by_id": day.closed_by_id, "closed_by_name": (w_closer.full_name or w_closer.username) if w_closer else None, "notes": day.notes, "order_count": order_count, "revenue": round(revenue, 2), }) return {"business_days": result}