163 lines
5.6 KiB
Python
163 lines
5.6 KiB
Python
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
|
|
|
|
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")
|
|
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)
|
|
return day
|
|
|
|
|
|
@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}
|