initial project setup with README and ignore
This commit is contained in:
196
app/services/routing/zone_service.py
Normal file
196
app/services/routing/zone_service.py
Normal file
@@ -0,0 +1,196 @@
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user