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 }