Files
routesapi/app/services/routing/zone_service.py

197 lines
7.4 KiB
Python

import logging
from typing import List, Dict, Any, Optional
logger = logging.getLogger(__name__)
class ZoneService:
"""
Service to classify orders and riders into geographic zones.
Defaulting to Coimbatore logic as per user context.
"""
# Approximate Center of Coimbatore (Gandhipuram/Bus Stand area)
CENTER_LAT = 11.0168
CENTER_LON = 76.9558
def __init__(self):
pass
def determine_zone(self, lat: float, lon: float, pincode: Optional[str] = None) -> str:
"""
Determine the zone (North, South, East, West, etc.) based on coordinates.
"""
if lat == 0 or lon == 0:
return "Unknown"
lat_diff = lat - self.CENTER_LAT
lon_diff = lon - self.CENTER_LON
# Simple Quadrant Logic
# North: +Lat
# South: -Lat
# East: +Lon
# West: -Lon
# Define a small central buffer (0.01 degrees ~ 1.1km)
buffer = 0.010
is_north = lat_diff > buffer
is_south = lat_diff < -buffer
is_east = lon_diff > buffer
is_west = lon_diff < -buffer
zone_parts = []
if is_north: zone_parts.append("North")
elif is_south: zone_parts.append("South")
if is_east: zone_parts.append("East")
elif is_west: zone_parts.append("West")
if not zone_parts:
return "Central"
return " ".join(zone_parts)
def group_by_zones(self, flat_orders: List[Dict[str, Any]], unassigned_orders: List[Dict[str, Any]] = None, fuel_charge: float = 2.5, base_pay: float = 30.0) -> Dict[str, Any]:
"""
Group a flat list of optimized orders into Zones -> Riders -> Orders.
Calculates profit per order and per zone.
"""
zones_map = {} # "North East": { "riders": { rider_id: [orders] } }
unassigned_orders = unassigned_orders or []
# Merge both for initial processing if you want everything zoned
all_to_process = []
for o in flat_orders:
all_to_process.append((o, True))
for o in unassigned_orders:
all_to_process.append((o, False))
for order, is_assigned in all_to_process:
# 1. Extract Coords
try:
# Prefer Delivery location for zoning (where the customer is)
lat = float(order.get("deliverylat") or order.get("droplat") or 0)
lon = float(order.get("deliverylong") or order.get("droplon") or 0)
pincode = str(order.get("deliveryzip") or "")
except:
lat, lon, pincode = 0, 0, ""
# 2. Get Zone
zone_name = self.determine_zone(lat, lon, pincode)
order["zone_name"] = zone_name
# 3. Initialize Zone Bucket
if zone_name not in zones_map:
zones_map[zone_name] = {
"riders_map": {},
"total_orders": 0,
"assigned_orders": 0,
"unassigned_orders": [],
"total_kms": 0.0,
"total_profit": 0.0
}
# 4. Add to Rider bucket within Zone
rider_id = order.get("userid") or order.get("_id")
# Track kms and profit for this zone
try:
# 'actualkms' is preferred for delivery distance
dist = float(order.get("actualkms", order.get("previouskms", 0)))
zones_map[zone_name]["total_kms"] += dist
# Individual charge for this order: Fixed Base + Variable Distance
order_amount = float(order.get("orderamount") or order.get("deliveryamount") or 0)
rider_payment = base_pay + (dist * fuel_charge)
profit = order_amount - rider_payment
order["rider_charge"] = round(rider_payment, 2)
order["profit"] = round(profit, 2)
# Profit-based classification (Order Type)
if profit <= 0:
order["ordertype"] = "Loss"
elif profit <= 5:
order["ordertype"] = "Risky"
elif profit <= 10:
order["ordertype"] = "Economy"
else:
order["ordertype"] = "Premium"
zones_map[zone_name]["total_profit"] += profit
except:
pass
# If strictly unassigned order (no rider), put in unassigned
if not is_assigned:
zones_map[zone_name]["unassigned_orders"].append(order)
else:
str_rid = str(rider_id)
if str_rid not in zones_map[zone_name]["riders_map"]:
zones_map[zone_name]["riders_map"][str_rid] = {
"rider_details": {
"id": str_rid,
"name": order.get("username", "Unknown")
},
"orders": []
}
zones_map[zone_name]["riders_map"][str_rid]["orders"].append(order)
zones_map[zone_name]["assigned_orders"] += 1
zones_map[zone_name]["total_orders"] += 1
# 5. Restructure for API Response
output_zones = []
zone_metrics = []
sorted_zone_names = sorted(zones_map.keys())
for z_name in sorted_zone_names:
z_data = zones_map[z_name]
# Flatten riders map
riders_list = []
for r_id, r_data in z_data["riders_map"].items():
riders_list.append({
"rider_id": r_data["rider_details"]["id"],
"rider_name": r_data["rider_details"]["name"],
"orders_count": len(r_data["orders"]),
"orders": r_data["orders"]
})
# Create the flat metric summary
metrics = {
"zone_name": z_name,
"total_orders": z_data["total_orders"],
"assigned_orders": z_data["assigned_orders"],
"unassigned_orders_count": len(z_data["unassigned_orders"]),
"active_riders_count": len(riders_list),
"total_delivery_kms": round(z_data["total_kms"], 2),
"total_profit": round(z_data["total_profit"], 2)
}
zone_metrics.append(metrics)
# Create the detailed zone object with flattened metrics
zone_obj = {
"zone_name": z_name,
"total_orders": metrics["total_orders"],
"active_riders_count": metrics["active_riders_count"],
"assigned_orders": metrics["assigned_orders"],
"unassigned_orders_count": metrics["unassigned_orders_count"],
"total_delivery_kms": metrics["total_delivery_kms"],
"total_profit": metrics["total_profit"],
"riders": riders_list,
"unassigned_orders": z_data["unassigned_orders"]
}
output_zones.append(zone_obj)
return {
"detailed_zones": output_zones,
"zone_analysis": zone_metrics
}