197 lines
7.4 KiB
Python
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
|
|
}
|