Initial commit
This commit is contained in:
149
app/routes/riders.py
Normal file
149
app/routes/riders.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
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))
|
||||
Reference in New Issue
Block a user