150 lines
5.2 KiB
Python
150 lines
5.2 KiB
Python
"""
|
||
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))
|