Initial commit

This commit is contained in:
2026-06-22 17:40:08 +05:30
commit c742ef0e53
308 changed files with 68519 additions and 0 deletions

149
app/routes/riders.py Normal file
View 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))