"""Nagercoil single-rider assignment endpoint. Nagercoil currently operates with exactly one active rider, so the full matching / VRP / pattern-preassignment machinery in ``riderassign`` is unnecessary here: every created order goes to that one rider. This endpoint keeps the same request/response contract as ``POST /api/v1/optimization/riderassign`` but replaces the assignment step with a straight "all orders → the single rider" hand-off, then reuses the existing TSP sequencer so the rider still gets an optimised stop order and per-stop ETAs. If Nagercoil later grows to more than one rider, switch its caller back to the main ``riderassign`` endpoint (or add real matching here) — this route deliberately assumes a single rider and will reject an ambiguous roster. """ import asyncio import logging import time from datetime import timedelta from typing import Any from fastapi import APIRouter, Body, Query, status from dateutil.parser import parse as parse_date # Reuse the exact same sequencer + thread pool the main endpoint uses, so the # stop ordering and cumulative-km output are identical. from app.routes.optimization import _sync_optimize_route, _route_executor logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/optimization/nagercoil", tags=["Nagercoil"]) # Nagercoil currently runs with exactly one rider. Every order goes to him # unless the caller overrides via a `rider` object in the request body. NAGERCOIL_RIDER = { "userid": 852, "username": "Sivakumar Subramani", "contactno": "8248176099", } def _rider_id(r: dict) -> int: """Extract a rider's numeric id, tolerating the roster's field variants.""" try: return int(r.get("userid") or r.get("riderid") or r.get("id") or 0) except (TypeError, ValueError): return 0 @router.post( "/riderassign", status_code=status.HTTP_200_OK, summary="Assign all created orders to Nagercoil's single rider", description=""" Straight-assigns every order to Nagercoil's one active rider, sequences the stops with the shared TSP optimizer, and returns the same shape as the main `/api/v1/optimization/riderassign` endpoint. Rider resolution (first match wins): 1. `rider` object in the body — `{"userid": 555, "username": "...", "contactno": "..."}` 2. otherwise the single active rider returned by the live roster; if the roster returns zero or more than one rider this endpoint returns a 200 with `status: false` and a message, since Nagercoil is single-rider. Orders: - `deliveries` in the body (or a legacy flat list) are used as-is. - if empty, all `created` orders are fetched from the external API. """, responses={ 200: { "content": { "application/json": { "example": { "code": 200, "details": [{"orderid": "...", "userid": 555, "step": 1}], "message": "Success", "status": True, } } } } }, ) async def nagercoil_assign( body: Any = Body(default=None), reshuffle: bool = Query(False, alias="reshuffle"), ): from app.services.rider.get_active_riders import fetch_created_orders from app.services.routing.empirical_eta_calculator import EmpiricalETACalculator from app.services.routing.realistic_eta_calculator import get_time_of_day_category eta_calculator = EmpiricalETACalculator() _t0 = time.time() try: # ── Parse payload: {"deliveries": [...], "rider": {...}} or flat list ── payload_rider = None if isinstance(body, dict): payload_rider = body.get("rider") or None orders = body.get("deliveries") or [] elif isinstance(body, list): orders = body else: orders = [] # ── Resolve the single rider ───────────────────────────────────────── rider = None if isinstance(payload_rider, dict) and _rider_id(payload_rider): rider = payload_rider if not orders: orders = await fetch_created_orders() else: # No rider in the payload — default to Nagercoil's fixed single rider. rider = dict(NAGERCOIL_RIDER) if not orders: orders = await fetch_created_orders() rid = _rider_id(rider) rider_name = rider.get("username", "") or rider.get("name", "") rider_contact = rider.get("contactno", "") if not orders: logger.info("[Nagercoil] No orders to assign.") return { "code": 200, "details": [], "message": "No created orders to assign.", "status": True, "meta": { "total_orders": 0, "assigned_orders": 0, "rider": {"userid": rid, "username": rider_name}, }, } logger.info( f"[Nagercoil] Straight-assigning {len(orders)} orders to rider " f"{rid} ({rider_name})." ) # ── Sequence the single rider's stops with the shared TSP optimizer ── loop = asyncio.get_running_loop() optimized_route = await loop.run_in_executor( _route_executor, _sync_optimize_route, orders ) or orders # fall back to input order if the optimizer returns nothing # Total distance for the rider (mirrors riderassign) try: total_rider_kms = max( float(o.get("cumulativekms", 0)) for o in optimized_route ) except Exception: total_rider_kms = sum( float(o.get("actualkms", o.get("kms", 0))) for o in optimized_route ) # ── Enrich each order exactly like riderassign does ────────────────── for order in optimized_route: order["userid"] = rid order["username"] = rider_name order["rider"] = rider_name order["ridercontactno"] = rider_contact order["riderkms"] = str(round(total_rider_kms, 2)) pickup_slot_str = ( order.get("pickupSlot") or order.get("pickupslot") or order.get("pickup_slot") or order.get("pickuptime") ) if pickup_slot_str: try: pickup_time = parse_date(str(pickup_slot_str)) if order.get("cumulative_eta"): eta_mins = int(order["cumulative_eta"]) else: dist_km = float( order.get("cumulativekms") or order.get("actualkms", order.get("kms", 0)) ) step = int(order.get("step", 1)) order_type = order.get("ordertype", "Economy") _dcoords = None try: _dlat = float(order.get("deliverylat") or order.get("droplat") or 0) _dlon = float(order.get("deliverylong") or order.get("droplon") or 0) if _dlat and _dlon: _dcoords = (_dlat, _dlon) except (TypeError, ValueError): _dcoords = None eta_mins = eta_calculator.calculate_eta( distance_km=dist_km, is_first_order=(step == 1), order_type=order_type, time_of_day=get_time_of_day_category(), kitchen=order.get("pickupcustomer") or order.get("locationname"), drop_coords=_dcoords, rider_id=rid, ) expected_time = pickup_time + timedelta(minutes=eta_mins) order["expectedDeliveryTime"] = expected_time.strftime( "%Y-%m-%d %I:%M %p" ) order["transitMinutes"] = eta_mins order["calculationDistanceKm"] = round( float(order.get("cumulativekms") or order.get("actualkms", 0)), 2 ) except Exception as e: logger.warning( f"[Nagercoil] ETA calc failed for pickupSlot " f"'{pickup_slot_str}': {e}" ) _elapsed_ms = (time.time() - _t0) * 1000.0 logger.info( f"[API] nagercoil/riderassign ◀ assigned={len(optimized_route)}/" f"{len(orders)} rider={rid} elapsed={round(_elapsed_ms)}ms" ) return { "code": 200, "details": optimized_route, "message": "Success", "status": True, "meta": { "total_orders": len(orders), "assigned_orders": len(optimized_route), "unassigned_orders": 0, "rider": { "userid": rid, "username": rider_name, "contactno": rider_contact, }, "rider_total_kms": round(total_rider_kms, 2), "reshuffle_mode": reshuffle, "elapsed_ms": round(_elapsed_ms, 1), }, } except Exception as e: logger.error(f"[Nagercoil] assignment error: {e}", exc_info=True) return { "code": 500, "details": [], "message": "Internal server error during Nagercoil assignment.", "status": False, }