""" Riders Admin API ================ Endpoints for the operations team to manage rider substitutions. POST /api/v1/riders/substitution – register one or many substitutions GET /api/v1/riders/substitution – list upcoming/active subs DELETE /api/v1/riders/substitution/{sub_date}/{absent_rider_id} – cancel one """ import logging from datetime import date from typing import List from fastapi import APIRouter, Body, HTTPException, Path from pydantic import BaseModel, model_validator logger = logging.getLogger(__name__) router = APIRouter( prefix="/api/v1/riders", tags=["Riders & Substitutions"], responses={500: {"description": "Internal server error"}}, ) class SubstitutionEntry(BaseModel): sub_date: str absent_rider_id: int sub_rider_id: int @model_validator(mode="after") def validate_entry(self): try: date.fromisoformat(self.sub_date) except ValueError: raise ValueError(f"Invalid date format '{self.sub_date}'. Use YYYY-MM-DD.") if self.absent_rider_id == self.sub_rider_id: raise ValueError("absent_rider_id and sub_rider_id must be different.") return self @router.post("/substitution", summary="Register one or multiple rider substitutions") def register_substitution(entries: List[SubstitutionEntry] = Body(...)): """ Register rider substitutions for the operations team. Accepts a list so you can submit all absent riders for a day in one call. Example — single: ```json [{"sub_date": "2026-06-20", "absent_rider_id": 101, "sub_rider_id": 205}] ``` Example — batch (3 riders absent same day): ```json [ {"sub_date": "2026-06-20", "absent_rider_id": 101, "sub_rider_id": 205}, {"sub_date": "2026-06-20", "absent_rider_id": 102, "sub_rider_id": 206}, {"sub_date": "2026-06-21", "absent_rider_id": 103, "sub_rider_id": 207} ] ``` Each sub rider **must** appear in that day's getriderlogs response. The assignment engine copies the absent rider's kitchen ownership, soft preferences, and home location onto the sub rider automatically — reverts the next day with no action needed. Posting the same (sub_date, absent_rider_id) again updates sub_rider_id. """ if not entries: raise HTTPException(status_code=400, detail="Request body must be a non-empty list.") try: from app.services.rider.substitution_service import get_substitution_service svc = get_substitution_service() results = [svc.register(e.sub_date, e.absent_rider_id, e.sub_rider_id) for e in entries] return { "status": "ok", "registered": len(results), "substitutions": results, } except Exception as e: logger.error(f"[Riders API] register_substitution: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.get("/substitution", summary="List upcoming and active rider substitutions") def list_substitutions(from_date: str = None): """ Returns all substitution records on or after `from_date` (default: today). Use `?from_date=2026-06-01` to look back further. """ if from_date is not None: try: date.fromisoformat(from_date) except ValueError: raise HTTPException( status_code=400, detail=f"Invalid date format '{from_date}'. Use YYYY-MM-DD.", ) try: from app.services.rider.substitution_service import get_substitution_service records = get_substitution_service().list_all(from_date) return { "status": "ok", "count": len(records), "substitutions": records, } except Exception as e: logger.error(f"[Riders API] list_substitutions: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.delete( "/substitution/{sub_date}/{absent_rider_id}", summary="Cancel a rider substitution", ) def cancel_substitution( sub_date: str = Path(..., example="2026-06-20"), absent_rider_id: int = Path(..., example=101), ): """ Remove a substitution record. The sub rider will no longer inherit the absent rider's profile on that date. """ try: date.fromisoformat(sub_date) except ValueError: raise HTTPException( status_code=400, detail=f"Invalid date format '{sub_date}'. Use YYYY-MM-DD.", ) try: from app.services.rider.substitution_service import get_substitution_service removed = get_substitution_service().cancel(sub_date, absent_rider_id) if not removed: raise HTTPException( status_code=404, detail=f"No substitution found for absent_rider_id={absent_rider_id} on {sub_date}.", ) return { "status": "ok", "message": f"Substitution cancelled: rider {absent_rider_id} on {sub_date}.", } except HTTPException: raise except Exception as e: logger.error(f"[Riders API] cancel_substitution: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e))