1932 lines
83 KiB
Python
1932 lines
83 KiB
Python
import json
|
||
import time
|
||
import requests
|
||
from django.conf import settings
|
||
from .sfx_client import get_base_url, get_default_token, execute_sfx_call
|
||
from .models import ActivityLog
|
||
from ekart.client import EkartAPIClient
|
||
from delhivery.client import DelhiveryAPIClient
|
||
from .delhivery_serviceability import (
|
||
get_delhivery_pincode_info,
|
||
is_delhivery_serviceable,
|
||
check_delhivery_route_serviceability,
|
||
resolve_delhivery_location,
|
||
STATE_CODE_TO_NAME
|
||
)
|
||
|
||
|
||
|
||
PINCODE_PREFIX_DIRECTORY = {
|
||
# Tamil Nadu & Puducherry
|
||
'600': ('Chennai', 'Tamil Nadu'),
|
||
'601': ('Chennai', 'Tamil Nadu'),
|
||
'602': ('Thiruvallur', 'Tamil Nadu'),
|
||
'603': ('Chengalpattu', 'Tamil Nadu'),
|
||
'604': ('Tindivanam', 'Tamil Nadu'),
|
||
'605': ('Puducherry', 'Puducherry'),
|
||
'606': ('Villupuram', 'Tamil Nadu'),
|
||
'607': ('Cuddalore', 'Tamil Nadu'),
|
||
'608': ('Chidambaram', 'Tamil Nadu'),
|
||
'609': ('Mayiladuthurai', 'Tamil Nadu'),
|
||
'610': ('Thiruvarur', 'Tamil Nadu'),
|
||
'611': ('Nagapattinam', 'Tamil Nadu'),
|
||
'612': ('Kumbakonam', 'Tamil Nadu'),
|
||
'613': ('Thanjavur', 'Tamil Nadu'),
|
||
'614': ('Pattukkottai', 'Tamil Nadu'),
|
||
'620': ('Tiruchirappalli', 'Tamil Nadu'),
|
||
'621': ('Lalgudi', 'Tamil Nadu'),
|
||
'622': ('Pudukkottai', 'Tamil Nadu'),
|
||
'623': ('Ramanathapuram', 'Tamil Nadu'),
|
||
'624': ('Dindigul', 'Tamil Nadu'),
|
||
'625': ('Madurai', 'Tamil Nadu'),
|
||
'626': ('Virudhunagar', 'Tamil Nadu'),
|
||
'627': ('Tirunelveli', 'Tamil Nadu'),
|
||
'628': ('Thoothukudi', 'Tamil Nadu'),
|
||
'629': ('Nagercoil', 'Tamil Nadu'),
|
||
'630': ('Karaikudi', 'Tamil Nadu'),
|
||
'631': ('Arakkonam', 'Tamil Nadu'),
|
||
'632': ('Vellore', 'Tamil Nadu'),
|
||
'635': ('Hosur', 'Tamil Nadu'),
|
||
'636': ('Salem', 'Tamil Nadu'),
|
||
'637': ('Namakkal', 'Tamil Nadu'),
|
||
'638': ('Erode', 'Tamil Nadu'),
|
||
'639': ('Karur', 'Tamil Nadu'),
|
||
'641': ('Coimbatore', 'Tamil Nadu'),
|
||
'642': ('Pollachi', 'Tamil Nadu'),
|
||
'643': ('Udhagamandalam', 'Tamil Nadu'),
|
||
# Karnataka
|
||
'560': ('Bengaluru', 'Karnataka'),
|
||
'561': ('Bengaluru Rural', 'Karnataka'),
|
||
'562': ('Bengaluru Rural', 'Karnataka'),
|
||
'563': ('Kolar', 'Karnataka'),
|
||
'570': ('Mysuru', 'Karnataka'),
|
||
'571': ('Mysuru Rural', 'Karnataka'),
|
||
'572': ('Tumakuru', 'Karnataka'),
|
||
'573': ('Hassan', 'Karnataka'),
|
||
'574': ('Mangaluru Rural', 'Karnataka'),
|
||
'575': ('Mangaluru', 'Karnataka'),
|
||
'576': ('Udupi', 'Karnataka'),
|
||
'577': ('Shivamogga', 'Karnataka'),
|
||
'580': ('Hubballi', 'Karnataka'),
|
||
'581': ('Sirsi', 'Karnataka'),
|
||
'582': ('Gadag', 'Karnataka'),
|
||
'583': ('Ballari', 'Karnataka'),
|
||
'584': ('Raichur', 'Karnataka'),
|
||
'585': ('Kalaburagi', 'Karnataka'),
|
||
'586': ('Vijayapura', 'Karnataka'),
|
||
'587': ('Bagalkote', 'Karnataka'),
|
||
'590': ('Belagavi', 'Karnataka'),
|
||
# Delhi & NCR
|
||
'110': ('Delhi', 'Delhi'),
|
||
'121': ('Faridabad', 'Haryana'),
|
||
'122': ('Gurugram', 'Haryana'),
|
||
'201': ('Noida', 'Uttar Pradesh'),
|
||
# Maharashtra
|
||
'400': ('Mumbai', 'Maharashtra'),
|
||
'401': ('Thane', 'Maharashtra'),
|
||
'410': ('Lonavala', 'Maharashtra'),
|
||
'411': ('Pune', 'Maharashtra'),
|
||
'412': ('Pune Rural', 'Maharashtra'),
|
||
'414': ('Ahmednagar', 'Maharashtra'),
|
||
'415': ('Satara', 'Maharashtra'),
|
||
'416': ('Kolhapur', 'Maharashtra'),
|
||
'421': ('Kalyan', 'Maharashtra'),
|
||
'422': ('Nashik', 'Maharashtra'),
|
||
'431': ('Chhatrapati Sambhajinagar', 'Maharashtra'),
|
||
'440': ('Nagpur', 'Maharashtra'),
|
||
# Telangana & AP
|
||
'500': ('Hyderabad', 'Telangana'),
|
||
'501': ('Hyderabad Rural', 'Telangana'),
|
||
'502': ('Medak', 'Telangana'),
|
||
'503': ('Nizamabad', 'Telangana'),
|
||
'505': ('Karimnagar', 'Telangana'),
|
||
'506': ('Warangal', 'Telangana'),
|
||
'515': ('Anantapur', 'Andhra Pradesh'),
|
||
'516': ('Kadapa', 'Andhra Pradesh'),
|
||
'517': ('Tirupati', 'Andhra Pradesh'),
|
||
'518': ('Kurnool', 'Andhra Pradesh'),
|
||
'520': ('Vijayawada', 'Andhra Pradesh'),
|
||
'521': ('Machilipatnam', 'Andhra Pradesh'),
|
||
'522': ('Guntur', 'Andhra Pradesh'),
|
||
'530': ('Visakhapatnam', 'Andhra Pradesh'),
|
||
'533': ('Kakinada', 'Andhra Pradesh'),
|
||
'534': ('Eluru', 'Andhra Pradesh'),
|
||
# Kerala
|
||
'670': ('Kannur', 'Kerala'),
|
||
'671': ('Kasaragod', 'Kerala'),
|
||
'673': ('Kozhikode', 'Kerala'),
|
||
'676': ('Malappuram', 'Kerala'),
|
||
'678': ('Palakkad', 'Kerala'),
|
||
'680': ('Thrissur', 'Kerala'),
|
||
'682': ('Kochi', 'Kerala'),
|
||
'683': ('Aluva', 'Kerala'),
|
||
'686': ('Kottayam', 'Kerala'),
|
||
'688': ('Alappuzha', 'Kerala'),
|
||
'689': ('Thiruvalla', 'Kerala'),
|
||
'690': ('Kayamkulam', 'Kerala'),
|
||
'691': ('Kollam', 'Kerala'),
|
||
'695': ('Thiruvananthapuram', 'Kerala'),
|
||
# West Bengal
|
||
'700': ('Kolkata', 'West Bengal'),
|
||
'711': ('Howrah', 'West Bengal'),
|
||
'712': ('Hooghly', 'West Bengal'),
|
||
'713': ('Durgapur', 'West Bengal'),
|
||
'734': ('Siliguri', 'West Bengal'),
|
||
# Gujarat
|
||
'380': ('Ahmedabad', 'Gujarat'),
|
||
'382': ('Gandhinagar', 'Gujarat'),
|
||
'390': ('Vadodara', 'Gujarat'),
|
||
'395': ('Surat', 'Gujarat'),
|
||
'360': ('Rajkot', 'Gujarat'),
|
||
# Rajasthan
|
||
'302': ('Jaipur', 'Rajasthan'),
|
||
'342': ('Jodhpur', 'Rajasthan'),
|
||
'313': ('Udaipur', 'Rajasthan'),
|
||
# UP
|
||
'226': ('Lucknow', 'Uttar Pradesh'),
|
||
'208': ('Kanpur', 'Uttar Pradesh'),
|
||
'221': ('Varanasi', 'Uttar Pradesh'),
|
||
'211': ('Prayagraj', 'Uttar Pradesh'),
|
||
'282': ('Agra', 'Uttar Pradesh'),
|
||
# MP & Bihar
|
||
'452': ('Indore', 'Madhya Pradesh'),
|
||
'462': ('Bhopal', 'Madhya Pradesh'),
|
||
'800': ('Patna', 'Bihar')
|
||
}
|
||
|
||
def resolve_city_state_by_pincode(pincode, default_city=None, default_state=None):
|
||
"""
|
||
Selects the destination city and state based directly on the customer delivery pincode.
|
||
Uses official Delhivery 18,699 serviceable pincode database as the primary authority.
|
||
"""
|
||
pin_str = str(pincode or "").strip()
|
||
|
||
# 1. Official Delhivery Serviceability Matrix (18,699 Pincodes)
|
||
delhivery_loc = resolve_delhivery_location(pin_str)
|
||
if delhivery_loc:
|
||
return delhivery_loc
|
||
|
||
# 2. Regional 3-digit prefix directory
|
||
prefix3 = pin_str[:3]
|
||
if prefix3 in PINCODE_PREFIX_DIRECTORY:
|
||
return PINCODE_PREFIX_DIRECTORY[prefix3]
|
||
|
||
# 3. Live Delhivery API query if 6 digits
|
||
if len(pin_str) == 6 and pin_str.isdigit():
|
||
try:
|
||
client = DelhiveryAPIClient()
|
||
res = client.check_serviceability(pin_str)
|
||
codes = res.get('delivery_codes', [])
|
||
if codes:
|
||
p_data = codes[0].get('postal_code', {})
|
||
d_city = p_data.get('city')
|
||
d_st_code = p_data.get('state_code')
|
||
d_state = STATE_CODE_TO_NAME.get(d_st_code, d_st_code)
|
||
if d_city and d_state:
|
||
return d_city, d_state
|
||
except Exception:
|
||
pass
|
||
|
||
if default_city and default_state:
|
||
return default_city, default_state
|
||
|
||
if pin_str.startswith('6'):
|
||
return ('Chennai', 'Tamil Nadu')
|
||
elif pin_str.startswith('5'):
|
||
return ('Bengaluru', 'Karnataka')
|
||
elif pin_str.startswith('1'):
|
||
return ('Delhi', 'Delhi')
|
||
elif pin_str.startswith('4'):
|
||
return ('Mumbai', 'Maharashtra')
|
||
elif pin_str.startswith('7'):
|
||
return ('Kolkata', 'West Bengal')
|
||
|
||
return ('Bengaluru', 'Karnataka')
|
||
|
||
def check_shadowfax_availability(pickup_pincode, drop_pincode, service='customer_delivery', weight=None, env='production', token=None, use_mock=False):
|
||
"""
|
||
Checks Shadowfax pincode serviceability and logistics rate estimation directly from live Shadowfax API.
|
||
Provides detailed first-mile (pickup hub) and last-mile (doorstep delivery) status.
|
||
Package weight (in grams) is a mandatory input.
|
||
"""
|
||
if weight is None or float(weight) <= 0:
|
||
raise ValueError("Package weight (in grams) is required for Shadowfax rate evaluation.")
|
||
weight_slabs = max(1, int(weight) // 500)
|
||
estimated_rate = round(45.0 + (weight_slabs * 15.0), 2)
|
||
p_city, p_state = resolve_city_state_by_pincode(pickup_pincode)
|
||
d_city, d_state = resolve_city_state_by_pincode(drop_pincode)
|
||
|
||
if use_mock:
|
||
return {
|
||
"carrier": "SHADOWFAX",
|
||
"serviceable": True,
|
||
"cod_available": True,
|
||
"estimated_cost": estimated_rate,
|
||
"currency": "INR",
|
||
"first_mile": {
|
||
"serviceable": True,
|
||
"pincode": str(pickup_pincode),
|
||
"city": p_city,
|
||
"state": p_state,
|
||
"hub_active": True,
|
||
"pickup_type": "Seller Store / Warehouse Sweep",
|
||
"message": f"First-mile pickup serviceable in {p_city}, {p_state} (Simulation)"
|
||
},
|
||
"last_mile": {
|
||
"serviceable": True,
|
||
"pincode": str(drop_pincode),
|
||
"city": d_city,
|
||
"state": d_state,
|
||
"cod_available": True,
|
||
"delivery_type": "Doorstep Delivery (Express)",
|
||
"message": f"Last-mile delivery serviceable in {d_city}, {d_state} (Simulation)"
|
||
},
|
||
"details": {"mock": True, "pincodes": [str(pickup_pincode), str(drop_pincode)]}
|
||
}
|
||
|
||
base_url = get_base_url(env)
|
||
auth_token = str(token).strip() if token else get_default_token(env)
|
||
headers = {"Authorization": f"Token {auth_token}", "Content-Type": "application/json"}
|
||
|
||
try:
|
||
# 1. First-mile pickup check (seller/origin location)
|
||
url_pkp = f"{base_url}/v1/clients/serviceability/?service=seller_pickup&page=1&count=100&pincodes={pickup_pincode}"
|
||
res_pkp = execute_sfx_call('FORWARD', 'SFX First-Mile Pickup Check', 'GET', url_pkp, headers, env=env)
|
||
pickup_info = None
|
||
fm_ok = False
|
||
if res_pkp.get('status_code') == 200:
|
||
resp_pkp_payload = res_pkp.get('response', '')
|
||
parsed_pkp = json.loads(resp_pkp_payload) if isinstance(resp_pkp_payload, str) else resp_pkp_payload
|
||
p_list = parsed_pkp if isinstance(parsed_pkp, list) else (parsed_pkp.get('data', {}).get('pincodes', []) if isinstance(parsed_pkp, dict) else [])
|
||
pickup_info = next((p for p in p_list if str(p.get('code') or p.get('pincode')) == str(pickup_pincode)), None)
|
||
fm_ok = bool(pickup_info and len(pickup_info.get('services', [])) > 0)
|
||
|
||
# 2. Last-mile delivery check (customer/destination location)
|
||
url_dlv = f"{base_url}/v1/clients/serviceability/?service=customer_delivery&page=1&count=100&pincodes={drop_pincode}"
|
||
res_dlv = execute_sfx_call('FORWARD', 'SFX Last-Mile Delivery Check', 'GET', url_dlv, headers, env=env)
|
||
drop_info = None
|
||
lm_ok = False
|
||
if res_dlv.get('status_code') == 200:
|
||
resp_dlv_payload = res_dlv.get('response', '')
|
||
parsed_dlv = json.loads(resp_dlv_payload) if isinstance(resp_dlv_payload, str) else resp_dlv_payload
|
||
d_list = parsed_dlv if isinstance(parsed_dlv, list) else (parsed_dlv.get('data', {}).get('pincodes', []) if isinstance(parsed_dlv, dict) else [])
|
||
drop_info = next((p for p in d_list if str(p.get('code') or p.get('pincode')) == str(drop_pincode)), None)
|
||
lm_ok = bool(drop_info and len(drop_info.get('services', [])) > 0)
|
||
|
||
cod_ok = bool(drop_info.get('cod_available', True)) if drop_info else True
|
||
|
||
fm_city = (pickup_info.get('city') if pickup_info else None) or p_city
|
||
fm_state = (pickup_info.get('state') if pickup_info else None) or p_state
|
||
lm_city = (drop_info.get('city') if drop_info else None) or d_city
|
||
lm_state = (drop_info.get('state') if drop_info else None) or d_state
|
||
|
||
overall_serviceable = bool(fm_ok and lm_ok)
|
||
|
||
# Available Shadowfax delivery types
|
||
sfx_services = []
|
||
if drop_info and isinstance(drop_info.get('services'), list):
|
||
sfx_services = drop_info.get('services')
|
||
elif pickup_info and isinstance(pickup_info.get('services'), list):
|
||
sfx_services = pickup_info.get('services')
|
||
else:
|
||
sfx_services = ['Regular', 'Surface']
|
||
|
||
delivery_types = ['Prepaid', 'COD (Cash on Delivery)']
|
||
for s in sfx_services:
|
||
delivery_types.append(f"{s} Cargo/Delivery")
|
||
delivery_types.append('Reverse Pickup')
|
||
|
||
first_mile = {
|
||
"serviceable": bool(fm_ok),
|
||
"pincode": str(pickup_pincode),
|
||
"city": fm_city,
|
||
"state": fm_state,
|
||
"hub_active": bool(fm_ok),
|
||
"pickup_type": "Seller Store / Warehouse Sweep",
|
||
"message": f"First-mile pickup serviceable in {fm_city}, {fm_state}" if fm_ok else f"First-mile pickup NOT serviceable by Shadowfax in {fm_city}, {fm_state} ({pickup_pincode})"
|
||
}
|
||
|
||
last_mile = {
|
||
"serviceable": bool(lm_ok),
|
||
"pincode": str(drop_pincode),
|
||
"city": lm_city,
|
||
"state": lm_state,
|
||
"cod_available": bool(cod_ok),
|
||
"delivery_type": "Doorstep Delivery (Express)",
|
||
"message": f"Last-mile delivery serviceable in {lm_city}, {lm_state}" if lm_ok else f"Last-mile delivery NOT serviceable by Shadowfax in {lm_city}, {lm_state} ({drop_pincode})"
|
||
}
|
||
|
||
return {
|
||
"carrier": "SHADOWFAX",
|
||
"serviceable": overall_serviceable,
|
||
"first_mile_available": bool(fm_ok),
|
||
"last_mile_available": bool(lm_ok),
|
||
"cod_available": bool(cod_ok),
|
||
"estimated_cost": estimated_rate if overall_serviceable else None,
|
||
"estimated_charge": estimated_rate if overall_serviceable else None,
|
||
"currency": "INR",
|
||
"delivery_types": delivery_types if overall_serviceable else [],
|
||
"first_mile": first_mile,
|
||
"last_mile": last_mile,
|
||
"details": drop_info or pickup_info or {}
|
||
}
|
||
except Exception as e:
|
||
first_mile = {
|
||
"serviceable": False,
|
||
"pincode": str(pickup_pincode),
|
||
"city": p_city,
|
||
"state": p_state,
|
||
"hub_active": False,
|
||
"pickup_type": "Seller Store / Warehouse Sweep",
|
||
"message": str(e)
|
||
}
|
||
last_mile = {
|
||
"serviceable": False,
|
||
"pincode": str(drop_pincode),
|
||
"city": d_city,
|
||
"state": d_state,
|
||
"cod_available": False,
|
||
"delivery_type": "Doorstep Delivery (Express)",
|
||
"message": str(e)
|
||
}
|
||
return {
|
||
"carrier": "SHADOWFAX",
|
||
"serviceable": False,
|
||
"cod_available": False,
|
||
"estimated_cost": None,
|
||
"first_mile": first_mile,
|
||
"last_mile": last_mile,
|
||
"error": str(e)
|
||
}
|
||
|
||
|
||
def check_ekart_availability(pickup_pincode, drop_pincode, weight=None, payment_type='Prepaid', invoice_amount=1000, use_mock=False):
|
||
"""
|
||
Checks Ekart pincode serviceability and logistics rate estimation directly from live Ekart API.
|
||
Provides detailed first-mile and last-mile breakdown.
|
||
Package weight (in grams) is a mandatory input.
|
||
"""
|
||
if weight is None or float(weight) <= 0:
|
||
raise ValueError("Package weight (in grams) is required for Ekart rate evaluation.")
|
||
weight_slabs = max(1, int(weight) // 500)
|
||
estimated_rate = round(38.0 + (weight_slabs * 12.0), 2)
|
||
p_city, p_state = resolve_city_state_by_pincode(pickup_pincode)
|
||
d_city, d_state = resolve_city_state_by_pincode(drop_pincode)
|
||
|
||
if use_mock:
|
||
return {
|
||
"carrier": "EKART",
|
||
"serviceable": True,
|
||
"cod_available": True,
|
||
"estimated_cost": estimated_rate,
|
||
"currency": "INR",
|
||
"first_mile": {
|
||
"serviceable": True,
|
||
"pincode": str(pickup_pincode),
|
||
"city": p_city,
|
||
"state": p_state,
|
||
"hub_active": True,
|
||
"pickup_type": "Ekart Hub Sweep",
|
||
"message": f"First-mile pickup serviceable in {p_city}, {p_state} (Simulation)"
|
||
},
|
||
"last_mile": {
|
||
"serviceable": True,
|
||
"pincode": str(drop_pincode),
|
||
"city": d_city,
|
||
"state": d_state,
|
||
"cod_available": True,
|
||
"delivery_type": "Doorstep Surface/Air Delivery",
|
||
"message": f"Last-mile delivery serviceable in {d_city}, {d_state} (Simulation)"
|
||
},
|
||
"details": {"mock": True}
|
||
}
|
||
|
||
client = EkartAPIClient()
|
||
try:
|
||
# Step 1: Live First-Mile Pickup Check (Origin)
|
||
p_res = client.check_serviceability_v2(pickup_pincode)
|
||
p_details = p_res.get('details', {}) if isinstance(p_res, dict) else {}
|
||
fm_ok = bool(isinstance(p_res, dict) and p_res.get('status') and p_details.get('forward_pickup'))
|
||
if p_details.get('city'):
|
||
p_city = p_details.get('city')
|
||
if p_details.get('state'):
|
||
p_state = p_details.get('state')
|
||
|
||
# Step 2: Live Last-Mile Delivery Check (Destination)
|
||
d_res = client.check_serviceability_v2(drop_pincode)
|
||
d_details = d_res.get('details', {}) if isinstance(d_res, dict) else {}
|
||
is_cod = (str(payment_type).upper() == 'COD')
|
||
cod_available = bool(d_details.get('cod', False))
|
||
lm_drop_ok = bool(isinstance(d_res, dict) and d_res.get('status') and d_details.get('forward_drop'))
|
||
lm_ok = bool(lm_drop_ok and (cod_available if is_cod else True))
|
||
if d_details.get('city'):
|
||
d_city = d_details.get('city')
|
||
if d_details.get('state'):
|
||
d_state = d_details.get('state')
|
||
|
||
route_serviceable = bool(fm_ok and lm_ok)
|
||
|
||
# Step 3: Live End-to-End Route Serviceability & Pricing API (v3)
|
||
live_cost = None
|
||
v3_res = None
|
||
if route_serviceable:
|
||
v3_res = client.check_serviceability_v3(
|
||
pickup_pincode=pickup_pincode,
|
||
drop_pincode=drop_pincode,
|
||
length=10,
|
||
width=10,
|
||
height=10,
|
||
weight=weight,
|
||
payment_type="COD" if str(payment_type).strip().upper() == "COD" else "Prepaid",
|
||
invoice_amount=invoice_amount
|
||
)
|
||
if isinstance(v3_res, list) and len(v3_res) > 0:
|
||
charges = v3_res[0].get('forwardDeliveredCharges', {})
|
||
total_est = charges.get('totalForwardDeliveredEstimate')
|
||
if total_est:
|
||
try:
|
||
live_cost = float(total_est)
|
||
except Exception:
|
||
pass
|
||
elif isinstance(v3_res, dict) and v3_res.get('totalForwardDeliveredEstimate'):
|
||
live_cost = float(v3_res['totalForwardDeliveredEstimate'])
|
||
|
||
if live_cost is None:
|
||
live_cost = estimated_rate
|
||
|
||
first_mile = {
|
||
"serviceable": bool(fm_ok),
|
||
"pincode": str(pickup_pincode),
|
||
"city": p_city,
|
||
"state": p_state,
|
||
"hub_active": bool(fm_ok),
|
||
"pickup_type": "Ekart Hub Sweep (Live API)",
|
||
"message": f"First-mile pickup serviceable in {p_city}, {p_state}" if fm_ok else f"First-mile pickup unserviceable for pincode {pickup_pincode}"
|
||
}
|
||
|
||
last_mile = {
|
||
"serviceable": bool(lm_ok),
|
||
"pincode": str(drop_pincode),
|
||
"city": d_city,
|
||
"state": d_state,
|
||
"cod_available": bool(cod_available),
|
||
"delivery_type": "Doorstep Surface/Air Delivery (Live API)",
|
||
"message": f"Last-mile delivery serviceable in {d_city}, {d_state}" if lm_ok else f"Last-mile delivery unserviceable for pincode {drop_pincode}"
|
||
}
|
||
|
||
delivery_types = ['Prepaid', 'Forward Drop (Doorstep Delivery)', 'Forward Pickup (Hub Sweep)']
|
||
if cod_available:
|
||
delivery_types.append('COD (Cash on Delivery)')
|
||
delivery_types.extend(['Reverse Pickup (RVP)', 'Reverse Drop', 'Surface / Air Delivery'])
|
||
|
||
return {
|
||
"carrier": "EKART",
|
||
"serviceable": bool(route_serviceable),
|
||
"first_mile_available": bool(fm_ok),
|
||
"last_mile_available": bool(lm_ok),
|
||
"cod_available": bool(cod_available),
|
||
"estimated_cost": live_cost if route_serviceable else None,
|
||
"estimated_charge": live_cost if route_serviceable else None,
|
||
"currency": "INR",
|
||
"delivery_types": delivery_types if route_serviceable else [],
|
||
"first_mile": first_mile,
|
||
"last_mile": last_mile,
|
||
"details": {
|
||
"origin_v2": p_res,
|
||
"dest_v2": d_res,
|
||
"route_v3": v3_res
|
||
}
|
||
}
|
||
except Exception as e:
|
||
first_mile = {
|
||
"serviceable": False,
|
||
"pincode": str(pickup_pincode),
|
||
"city": p_city,
|
||
"state": p_state,
|
||
"hub_active": False,
|
||
"pickup_type": "Ekart Hub Sweep",
|
||
"message": str(e)
|
||
}
|
||
last_mile = {
|
||
"serviceable": False,
|
||
"pincode": str(drop_pincode),
|
||
"city": d_city,
|
||
"state": d_state,
|
||
"cod_available": False,
|
||
"delivery_type": "Doorstep Surface/Air Delivery",
|
||
"message": str(e)
|
||
}
|
||
return {
|
||
"carrier": "EKART",
|
||
"serviceable": False,
|
||
"cod_available": False,
|
||
"estimated_cost": None,
|
||
"first_mile": first_mile,
|
||
"last_mile": last_mile,
|
||
"error": str(e)
|
||
}
|
||
|
||
|
||
def check_delhivery_availability(pickup_pincode, drop_pincode, weight=None, payment_type='Prepaid', api_token=None, use_mock=False):
|
||
"""
|
||
Checks Delhivery pincode serviceability and logistics rate estimation directly
|
||
against official Delhivery serviceability matrix (18,699 pincodes) and live Delhivery API.
|
||
Provides detailed first-mile (pickup hub) and last-mile (doorstep delivery) status.
|
||
Package weight (in grams) is a mandatory input.
|
||
"""
|
||
if weight is None or float(weight) <= 0:
|
||
raise ValueError("Package weight (in grams) is required for Delhivery rate evaluation.")
|
||
weight_slabs = max(1, int(weight) // 500)
|
||
estimated_rate = round(35.0 + (weight_slabs * 11.0), 2)
|
||
is_cod = (str(payment_type).upper() == 'COD')
|
||
|
||
# Step 1: Query first-mile and last-mile serviceability
|
||
if not use_mock:
|
||
client = DelhiveryAPIClient(api_token=api_token)
|
||
# 1. Live First-Mile Pickup Check (Origin)
|
||
try:
|
||
p_res = client.check_serviceability(pickup_pincode)
|
||
p_codes = p_res.get('delivery_codes', []) if isinstance(p_res, dict) else []
|
||
if p_codes:
|
||
p_postal = p_codes[0].get('postal_code', {})
|
||
p_ok = (p_postal.get('pickup') == 'Y')
|
||
p_msg = "First-mile pickup serviceable (B2C Express/Surface Hub Active)" if p_ok else "First-mile pickup blocked for this pincode"
|
||
p_city = p_postal.get('city') or p_postal.get('district') or resolve_city_state_by_pincode(pickup_pincode)[0]
|
||
p_st_code = p_postal.get('state_code', '')
|
||
p_state = STATE_CODE_TO_NAME.get(p_st_code, p_st_code) or resolve_city_state_by_pincode(pickup_pincode)[1]
|
||
pickup_info = {
|
||
"pincode": str(pickup_pincode),
|
||
"city": p_city,
|
||
"district": p_postal.get('district'),
|
||
"state_code": p_st_code,
|
||
"state": p_state,
|
||
"prepaid": (p_postal.get('pre_paid') == 'Y'),
|
||
"cod": (p_postal.get('cod') == 'Y'),
|
||
"reverse_pickup": (p_postal.get('pickup') == 'Y'),
|
||
"cash": (p_postal.get('cash') == 'Y'),
|
||
"repl": (p_postal.get('repl') == 'Y'),
|
||
"serviceable": True
|
||
}
|
||
else:
|
||
p_ok = False
|
||
p_msg = f"Pickup pincode {pickup_pincode} not found in Delhivery network"
|
||
p_city, p_state = resolve_city_state_by_pincode(pickup_pincode)
|
||
p_st_code = ''
|
||
pickup_info = None
|
||
except Exception as e:
|
||
p_ok = False
|
||
p_msg = f"Delhivery pickup check error: {str(e)}"
|
||
p_city, p_state = resolve_city_state_by_pincode(pickup_pincode)
|
||
p_st_code = ''
|
||
pickup_info = None
|
||
|
||
# 2. Live Last-Mile Delivery Check (Destination)
|
||
try:
|
||
d_res = client.check_serviceability(drop_pincode)
|
||
d_codes = d_res.get('delivery_codes', []) if isinstance(d_res, dict) else []
|
||
if d_codes:
|
||
d_postal = d_codes[0].get('postal_code', {})
|
||
cod_avail = (d_postal.get('cod') == 'Y')
|
||
d_ok = cod_avail if is_cod else (d_postal.get('pre_paid') == 'Y' or d_postal.get('cash') == 'Y')
|
||
d_msg = "Last-mile delivery serviceable" if d_ok else ("COD delivery blocked" if is_cod else "Destination delivery unserviceable")
|
||
d_city = d_postal.get('city') or d_postal.get('district') or resolve_city_state_by_pincode(drop_pincode)[0]
|
||
d_st_code = d_postal.get('state_code', '')
|
||
d_state = STATE_CODE_TO_NAME.get(d_st_code, d_st_code) or resolve_city_state_by_pincode(drop_pincode)[1]
|
||
drop_info = {
|
||
"pincode": str(drop_pincode),
|
||
"city": d_city,
|
||
"district": d_postal.get('district'),
|
||
"state_code": d_st_code,
|
||
"state": d_state,
|
||
"prepaid": (d_postal.get('pre_paid') == 'Y'),
|
||
"cod": cod_avail,
|
||
"reverse_pickup": (d_postal.get('pickup') == 'Y'),
|
||
"cash": (d_postal.get('cash') == 'Y'),
|
||
"repl": (d_postal.get('repl') == 'Y'),
|
||
"serviceable": True
|
||
}
|
||
else:
|
||
d_ok = False
|
||
d_msg = f"Drop pincode {drop_pincode} not found in Delhivery network"
|
||
d_city, d_state = resolve_city_state_by_pincode(drop_pincode)
|
||
d_st_code = ''
|
||
drop_info = None
|
||
cod_avail = False
|
||
except Exception as e:
|
||
d_ok = False
|
||
d_msg = f"Delhivery drop check error: {str(e)}"
|
||
d_city, d_state = resolve_city_state_by_pincode(drop_pincode)
|
||
d_st_code = ''
|
||
drop_info = None
|
||
cod_avail = False
|
||
else:
|
||
# Simulation / mock mode
|
||
p_ok, p_msg, pickup_info = is_delhivery_serviceable(pickup_pincode, is_cod=False)
|
||
d_ok, d_msg, drop_info = is_delhivery_serviceable(drop_pincode, is_cod=is_cod)
|
||
p_city = (pickup_info.get('city') if pickup_info else None) or resolve_city_state_by_pincode(pickup_pincode)[0]
|
||
p_state = (pickup_info.get('state') if pickup_info else None) or resolve_city_state_by_pincode(pickup_pincode)[1]
|
||
p_st_code = pickup_info.get('state_code', '') if pickup_info else ''
|
||
d_city = (drop_info.get('city') if drop_info else None) or resolve_city_state_by_pincode(drop_pincode)[0]
|
||
d_state = (drop_info.get('state') if drop_info else None) or resolve_city_state_by_pincode(drop_pincode)[1]
|
||
d_st_code = drop_info.get('state_code', '') if drop_info else ''
|
||
cod_avail = bool(drop_info.get('cod', True)) if drop_info else False
|
||
|
||
first_mile = {
|
||
"serviceable": bool(p_ok),
|
||
"pincode": str(pickup_pincode),
|
||
"city": p_city,
|
||
"state": p_state,
|
||
"state_code": p_st_code,
|
||
"reverse_pickup": bool(pickup_info.get('reverse_pickup', True)) if pickup_info else False,
|
||
"pickup_type": "B2C Surface / Express Hub Sweep (Live API)" if not use_mock else "B2C Surface / Express Hub Sweep",
|
||
"message": f"First-mile pickup serviceable in {p_city}, {p_state}" if p_ok else f"Pickup: {p_msg}"
|
||
}
|
||
|
||
last_mile = {
|
||
"serviceable": bool(d_ok),
|
||
"pincode": str(drop_pincode),
|
||
"city": d_city,
|
||
"state": d_state,
|
||
"state_code": d_st_code,
|
||
"cod_available": cod_avail,
|
||
"cash_support": bool(drop_info.get('cash', True)) if drop_info else False,
|
||
"delivery_type": "Doorstep Express / Surface Delivery (Live API)" if not use_mock else "Doorstep Express / Surface Delivery",
|
||
"message": f"Last-mile delivery serviceable in {d_city}, {d_state}" if d_ok else f"Destination: {d_msg}"
|
||
}
|
||
|
||
route_serviceable = bool(p_ok and d_ok)
|
||
|
||
if not route_serviceable:
|
||
err_parts = []
|
||
if not p_ok:
|
||
err_parts.append(f"Pickup: {p_msg}")
|
||
if not d_ok:
|
||
err_parts.append(f"Destination: {d_msg}")
|
||
return {
|
||
"carrier": "DELHIVERY",
|
||
"serviceable": False,
|
||
"cod_available": cod_avail,
|
||
"estimated_cost": None,
|
||
"error": " | ".join(err_parts),
|
||
"city": d_city,
|
||
"state": d_state,
|
||
"state_code": d_st_code,
|
||
"first_mile": first_mile,
|
||
"last_mile": last_mile,
|
||
"details": {
|
||
"route_message": " | ".join(err_parts),
|
||
"drop_info": drop_info,
|
||
"pickup_info": pickup_info
|
||
}
|
||
}
|
||
|
||
# Step 2: Route is verified serviceable in Delhivery network
|
||
live_cost = estimated_rate
|
||
api_details = {
|
||
"drop_info": drop_info,
|
||
"pickup_info": pickup_info
|
||
}
|
||
|
||
if not use_mock:
|
||
try:
|
||
rate_res = client.get_rate_estimate(
|
||
origin_pin=pickup_pincode,
|
||
dest_pin=drop_pincode,
|
||
weight=weight,
|
||
mode="S",
|
||
payment_type=payment_type
|
||
)
|
||
if isinstance(rate_res, dict) and rate_res.get('total_amount'):
|
||
live_cost = float(rate_res['total_amount'])
|
||
api_details['rate_estimate'] = rate_res
|
||
except Exception as e:
|
||
api_details['rate_api_warning'] = str(e)
|
||
|
||
delivery_types = []
|
||
if drop_info and drop_info.get('prepaid'):
|
||
delivery_types.append('Prepaid')
|
||
else:
|
||
delivery_types.append('Prepaid')
|
||
if drop_info and drop_info.get('cod'):
|
||
delivery_types.append('COD (Cash on Delivery)')
|
||
if drop_info and drop_info.get('cash'):
|
||
delivery_types.append('Cash Support')
|
||
if drop_info and drop_info.get('reverse_pickup'):
|
||
delivery_types.append('Reverse Pickup (RVP)')
|
||
if drop_info and drop_info.get('repl'):
|
||
delivery_types.append('Replacement (REPL)')
|
||
delivery_types.extend(['Surface Cargo', 'Express Delivery'])
|
||
|
||
return {
|
||
"carrier": "DELHIVERY",
|
||
"serviceable": True,
|
||
"first_mile_available": bool(p_ok),
|
||
"last_mile_available": bool(d_ok),
|
||
"cod_available": cod_avail,
|
||
"estimated_cost": live_cost,
|
||
"estimated_charge": live_cost,
|
||
"currency": "INR",
|
||
"city": d_city,
|
||
"state": d_state,
|
||
"state_code": d_st_code,
|
||
"delivery_types": delivery_types,
|
||
"first_mile": first_mile,
|
||
"last_mile": last_mile,
|
||
"details": api_details
|
||
}
|
||
|
||
|
||
def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type='Prepaid', weight=None, env='production', sfx_token=None, delhivery_token=None, use_mock=False):
|
||
"""
|
||
Evaluates serviceability, first-mile, and last-mile delivery across all registered carriers (Shadowfax, Delhivery, Ekart).
|
||
Package weight (in grams) is a mandatory input.
|
||
"""
|
||
if weight is None or float(weight) <= 0:
|
||
return {
|
||
"status": "error",
|
||
"message": "Package weight is required. Please provide parcel weight in grams."
|
||
}
|
||
weight = float(weight)
|
||
|
||
sfx_res = check_shadowfax_availability(
|
||
pickup_pincode=pickup_pincode,
|
||
drop_pincode=drop_pincode,
|
||
weight=weight,
|
||
env=env,
|
||
token=sfx_token,
|
||
use_mock=use_mock
|
||
)
|
||
|
||
delhivery_res = check_delhivery_availability(
|
||
pickup_pincode=pickup_pincode,
|
||
drop_pincode=drop_pincode,
|
||
weight=weight,
|
||
payment_type=payment_type,
|
||
api_token=delhivery_token,
|
||
use_mock=use_mock
|
||
)
|
||
|
||
ekart_res = check_ekart_availability(
|
||
pickup_pincode=pickup_pincode,
|
||
drop_pincode=drop_pincode,
|
||
weight=weight,
|
||
payment_type=payment_type,
|
||
use_mock=use_mock
|
||
)
|
||
|
||
availability_dict = {
|
||
"status": "success",
|
||
"pickup_pincode": str(pickup_pincode),
|
||
"drop_pincode": str(drop_pincode),
|
||
"payment_type": payment_type,
|
||
"weight": weight,
|
||
"carriers": {
|
||
"SHADOWFAX": sfx_res,
|
||
"DELHIVERY": delhivery_res,
|
||
"EKART": ekart_res
|
||
}
|
||
}
|
||
|
||
best_carrier = select_best_carrier(
|
||
availability_dict,
|
||
selection_strategy='CHEAPEST',
|
||
preferred_carrier='SHADOWFAX',
|
||
is_cod=(str(payment_type).upper() == 'COD')
|
||
)
|
||
availability_dict["best_carrier"] = best_carrier
|
||
|
||
return availability_dict
|
||
|
||
|
||
CARRIER_PRIORITY = {
|
||
'SHADOWFAX': 0,
|
||
'DELHIVERY': 1,
|
||
'EKART': 2
|
||
}
|
||
|
||
|
||
def select_best_carrier(availability_dict, selection_strategy='CHEAPEST', preferred_carrier='SHADOWFAX', allow_fallback=True, is_cod=False):
|
||
"""
|
||
Selects the optimal carrier based on live API availability:
|
||
- CHEAPEST: Picks the serviceable carrier with lowest estimated logistics cost.
|
||
If costs tie across carriers, priority hierarchy is applied:
|
||
1. Shadowfax (Priority #1)
|
||
2. Delhivery (Priority #2)
|
||
3. Ekart (Priority #3)
|
||
- PREFERRED: Tries preferred_carrier first; falls back to cheapest serviceable carrier using tie-breaker priority.
|
||
"""
|
||
carriers = availability_dict.get('carriers', {})
|
||
strategy = (selection_strategy or 'CHEAPEST').upper()
|
||
pref = (preferred_carrier or 'SHADOWFAX').upper()
|
||
|
||
valid_candidates = []
|
||
for c_name, c_info in carriers.items():
|
||
if not c_info:
|
||
continue
|
||
is_serv = c_info.get('serviceable', False)
|
||
cod_ok = True if not is_cod else c_info.get('cod_available', False)
|
||
cost = c_info.get('estimated_cost')
|
||
|
||
if is_serv and cod_ok:
|
||
valid_candidates.append({
|
||
"carrier": c_name,
|
||
"cost": cost if cost is not None else float('inf'),
|
||
"info": c_info
|
||
})
|
||
|
||
if not valid_candidates:
|
||
return {
|
||
"selected": False,
|
||
"carrier": None,
|
||
"cost": None,
|
||
"reason": "No serviceable carrier found for route and payment mode"
|
||
}
|
||
|
||
if strategy == 'PREFERRED':
|
||
pref_match = next((cand for cand in valid_candidates if cand['carrier'] == pref), None)
|
||
if pref_match:
|
||
return {
|
||
"selected": True,
|
||
"carrier": pref_match['carrier'],
|
||
"cost": pref_match['cost'],
|
||
"reason": f"Selected {pref_match['carrier']} as preferred carrier (Cost: ₹{pref_match['cost']})"
|
||
}
|
||
elif not allow_fallback:
|
||
return {
|
||
"selected": False,
|
||
"carrier": None,
|
||
"cost": None,
|
||
"reason": f"Preferred carrier {pref} is unserviceable and allow_fallback=False"
|
||
}
|
||
|
||
# CHEAPEST strategy with tie-breaker priority: Shadowfax (0) > Delhivery (1) > Ekart (2)
|
||
valid_candidates.sort(key=lambda x: (x['cost'], CARRIER_PRIORITY.get(x['carrier'].upper(), 99)))
|
||
best = valid_candidates[0]
|
||
|
||
return {
|
||
"selected": True,
|
||
"carrier": best['carrier'],
|
||
"cost": best['cost'],
|
||
"reason": f"Selected {best['carrier']} as lowest cost carrier (Cost: ₹{best['cost']})"
|
||
}
|
||
|
||
|
||
def check_single_pincode_all_carriers(pincode, weight=None, use_mock=False):
|
||
"""
|
||
Evaluates logistics capabilities and estimated charges for a single pincode across
|
||
all three carriers: Shadowfax, Delhivery, and Ekart.
|
||
Package weight (in grams) is a mandatory input.
|
||
"""
|
||
if weight is None or float(weight) <= 0:
|
||
return {
|
||
"status": "error",
|
||
"message": "Package weight is required. Please provide parcel weight in grams."
|
||
}
|
||
weight = float(weight)
|
||
pin_str = str(pincode or "").strip()
|
||
city, state = resolve_city_state_by_pincode(pin_str)
|
||
weight_slabs = max(1, int(weight) // 500)
|
||
|
||
# 1. Ekart Logistics Check
|
||
ekart_data = {}
|
||
if use_mock:
|
||
ekart_data = {
|
||
"carrier": "EKART",
|
||
"pincode": pin_str,
|
||
"city": city,
|
||
"state": state,
|
||
"serviceable": True,
|
||
"first_mile_available": True,
|
||
"first_mile_details": f"First-mile pickup serviceable in {city}, {state} (Hub Sweep Active)",
|
||
"last_mile_available": True,
|
||
"last_mile_details": f"Last-mile delivery serviceable in {city}, {state} (Doorstep Active)",
|
||
"cod_available": True,
|
||
"delivery_types": ["Prepaid", "COD (Max ₹49,999)", "Forward Drop (Doorstep)", "Forward Pickup (Hub Sweep)", "Reverse Pickup (RVP)", "Reverse Drop", "Surface / Air Delivery"],
|
||
"estimated_charge": round(38.0 + (weight_slabs * 12.0), 2),
|
||
"currency": "INR"
|
||
}
|
||
else:
|
||
try:
|
||
client = EkartAPIClient()
|
||
v2_res = client.check_serviceability_v2(pin_str)
|
||
e_serv = bool(v2_res.get('status'))
|
||
e_det = v2_res.get('details', {}) if e_serv else {}
|
||
e_city = e_det.get('city') or city
|
||
e_state = e_det.get('state') or state
|
||
e_fm = bool(e_det.get('forward_pickup', e_serv))
|
||
e_lm = bool(e_det.get('forward_drop', e_serv))
|
||
e_cod = bool(e_det.get('cod', e_serv))
|
||
e_max_cod = e_det.get('max_cod_amount', 49999)
|
||
|
||
e_types = ['Prepaid']
|
||
if e_cod:
|
||
e_types.append(f"COD (Max Rs. {e_max_cod:,})" if isinstance(e_max_cod, (int, float)) else "COD (Cash on Delivery)")
|
||
if e_det.get('forward_drop', e_serv):
|
||
e_types.append('Forward Drop (Doorstep Delivery)')
|
||
if e_det.get('forward_pickup', e_serv):
|
||
e_types.append('Forward Pickup (Hub Sweep)')
|
||
if e_det.get('reverse_pickup', e_serv):
|
||
e_types.append('Reverse Pickup (RVP)')
|
||
if e_det.get('reverse_drop', e_serv):
|
||
e_types.append('Reverse Drop')
|
||
e_types.append('Surface / Air Delivery')
|
||
|
||
# Ekart Rate calculation
|
||
e_cost = round(33.04 + ((weight_slabs - 1) * 12.0), 2)
|
||
try:
|
||
v3_res = client.check_serviceability_v3(pin_str, pin_str, 10, 10, 10, weight, 'Prepaid', 1000)
|
||
if isinstance(v3_res, list) and len(v3_res) > 0:
|
||
chg = v3_res[0].get('forwardDeliveredCharges', {}).get('totalForwardDeliveredEstimate')
|
||
if chg:
|
||
e_cost = float(chg)
|
||
except Exception:
|
||
pass
|
||
|
||
ekart_data = {
|
||
"carrier": "EKART",
|
||
"pincode": pin_str,
|
||
"city": e_city,
|
||
"state": e_state,
|
||
"serviceable": e_serv,
|
||
"first_mile_available": e_fm,
|
||
"first_mile_details": f"First-mile pickup serviceable in {e_city}, {e_state} (Hub Sweep Active)" if e_fm else "Pickup Hub Sweep Blocked",
|
||
"last_mile_available": e_lm,
|
||
"last_mile_details": f"Last-mile delivery serviceable in {e_city}, {e_state} (Doorstep Active)" if e_lm else "Doorstep Delivery Blocked",
|
||
"cod_available": e_cod,
|
||
"delivery_types": e_types if e_serv else [],
|
||
"estimated_charge": e_cost if e_serv else None,
|
||
"currency": "INR"
|
||
}
|
||
except Exception as e:
|
||
ekart_data = {
|
||
"carrier": "EKART",
|
||
"pincode": pin_str,
|
||
"city": city,
|
||
"state": state,
|
||
"serviceable": False,
|
||
"first_mile_available": False,
|
||
"first_mile_details": str(e),
|
||
"last_mile_available": False,
|
||
"last_mile_details": str(e),
|
||
"cod_available": False,
|
||
"delivery_types": [],
|
||
"estimated_charge": None,
|
||
"currency": "INR",
|
||
"error": str(e)
|
||
}
|
||
|
||
# 2. Delhivery Logistics Check
|
||
if use_mock:
|
||
d_info = get_delhivery_pincode_info(pin_str)
|
||
d_serv = bool(d_info and d_info.get('serviceable'))
|
||
d_city = (d_info.get('city') if d_info else None) or city
|
||
d_state = (d_info.get('state') if d_info else None) or state
|
||
d_fm = bool(d_serv and (d_info.get('reverse_pickup', True) if d_info else True))
|
||
d_lm = bool(d_serv)
|
||
d_cod = bool(d_info.get('cod', True) if d_info else False)
|
||
d_types = []
|
||
if d_info and d_info.get('prepaid'):
|
||
d_types.append('Prepaid')
|
||
if d_cod:
|
||
d_types.append('COD (Cash on Delivery)')
|
||
if d_info and d_info.get('cash'):
|
||
d_types.append('Cash Support')
|
||
if d_info and d_info.get('reverse_pickup'):
|
||
d_types.append('Reverse Pickup (RVP)')
|
||
if d_info and d_info.get('repl'):
|
||
d_types.append('Replacement (REPL)')
|
||
if d_serv:
|
||
d_types.extend(['Surface Cargo', 'Express Delivery'])
|
||
d_cost = round(35.0 + ((weight_slabs - 1) * 11.0), 2) if d_serv else None
|
||
else:
|
||
try:
|
||
d_client = DelhiveryAPIClient()
|
||
d_res = d_client.check_serviceability(pin_str)
|
||
d_codes = d_res.get('delivery_codes', []) if isinstance(d_res, dict) else []
|
||
if d_codes:
|
||
p_code = d_codes[0].get('postal_code', {})
|
||
d_serv = True
|
||
d_city = p_code.get('city') or p_code.get('district') or city
|
||
d_state = STATE_CODE_TO_NAME.get(p_code.get('state_code'), p_code.get('state_code', '')) or state
|
||
d_fm = (p_code.get('pickup') == 'Y')
|
||
d_lm = (p_code.get('pre_paid') == 'Y' or p_code.get('cash') == 'Y')
|
||
d_cod = (p_code.get('cod') == 'Y')
|
||
d_types = []
|
||
if p_code.get('pre_paid') == 'Y':
|
||
d_types.append('Prepaid')
|
||
if d_cod:
|
||
d_types.append('COD (Cash on Delivery)')
|
||
if p_code.get('cash') == 'Y':
|
||
d_types.append('Cash Support')
|
||
if p_code.get('pickup') == 'Y':
|
||
d_types.append('Reverse Pickup (RVP)')
|
||
if p_code.get('repl') == 'Y':
|
||
d_types.append('Replacement (REPL)')
|
||
d_types.extend(['Surface Cargo', 'Express Delivery'])
|
||
# Live rate from Delhivery
|
||
d_cost = None
|
||
try:
|
||
r_res = d_client.get_rate_estimate(origin_pin=pin_str, dest_pin='600001', weight=weight or 1000)
|
||
if isinstance(r_res, dict) and r_res.get('total_amount'):
|
||
d_cost = float(r_res['total_amount'])
|
||
except Exception:
|
||
d_cost = round(35.0 + ((weight_slabs - 1) * 11.0), 2)
|
||
else:
|
||
d_serv = False
|
||
d_city = city
|
||
d_state = state
|
||
d_fm = False
|
||
d_lm = False
|
||
d_cod = False
|
||
d_types = []
|
||
d_cost = None
|
||
except Exception as e:
|
||
d_serv = False
|
||
d_city = city
|
||
d_state = state
|
||
d_fm = False
|
||
d_lm = False
|
||
d_cod = False
|
||
d_types = []
|
||
d_cost = None
|
||
|
||
delhivery_data = {
|
||
"carrier": "DELHIVERY",
|
||
"pincode": pin_str,
|
||
"city": d_city,
|
||
"state": d_state,
|
||
"serviceable": d_serv,
|
||
"first_mile_available": d_fm,
|
||
"first_mile_details": f"First-mile pickup serviceable in {d_city}, {d_state} (B2C Express Hub Active)" if d_fm else "Pickup Hub Blocked",
|
||
"last_mile_available": d_lm,
|
||
"last_mile_details": f"Last-mile delivery serviceable in {d_city}, {d_state} (Doorstep Active)" if d_lm else "Destination Pincode Unserviceable",
|
||
"cod_available": d_cod,
|
||
"delivery_types": d_types,
|
||
"estimated_charge": d_cost,
|
||
"currency": "INR"
|
||
}
|
||
|
||
# 3. Shadowfax Logistics Check
|
||
s_cost = round(45.0 + ((weight_slabs - 1) * 15.0), 2)
|
||
sfx_data = {}
|
||
if use_mock:
|
||
sfx_data = {
|
||
"carrier": "SHADOWFAX",
|
||
"pincode": pin_str,
|
||
"city": city,
|
||
"state": state,
|
||
"serviceable": True,
|
||
"first_mile_available": True,
|
||
"first_mile_details": f"First-mile pickup serviceable in {city}, {state} (Store Sweep Active)",
|
||
"last_mile_available": True,
|
||
"last_mile_details": f"Last-mile delivery serviceable in {city}, {state} (Doorstep Active)",
|
||
"cod_available": True,
|
||
"delivery_types": ["Prepaid", "COD (Cash on Delivery)", "Regular Cargo/Delivery", "Surface Cargo/Delivery", "Reverse Pickup"],
|
||
"estimated_charge": s_cost,
|
||
"currency": "INR"
|
||
}
|
||
else:
|
||
try:
|
||
base_url = get_base_url('production')
|
||
token = get_default_token('production')
|
||
headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"}
|
||
|
||
# 1. Query first-mile pickup capabilities from seller/origin
|
||
url_pkp = f"{base_url}/v1/clients/serviceability/?service=seller_pickup&page=1&count=10&pincodes={pin_str}"
|
||
res_pkp = execute_sfx_call('FORWARD', 'Single Pincode Pickup Check', 'GET', url_pkp, headers, env='production')
|
||
fm_ok = False
|
||
if res_pkp.get('status_code') == 200:
|
||
resp_pkp_payload = res_pkp.get('response', '')
|
||
parsed_pkp = json.loads(resp_pkp_payload) if isinstance(resp_pkp_payload, str) else resp_pkp_payload
|
||
pkp_list = parsed_pkp if isinstance(parsed_pkp, list) else (parsed_pkp.get('data', {}).get('pincodes', []) if isinstance(parsed_pkp, dict) else [])
|
||
match_pkp = next((p for p in pkp_list if str(p.get('code') or p.get('pincode')) == pin_str), None)
|
||
fm_ok = bool(match_pkp and len(match_pkp.get('services', [])) > 0)
|
||
|
||
# 2. Query last-mile delivery capabilities to customer/destination
|
||
url_dlv = f"{base_url}/v1/clients/serviceability/?service=customer_delivery&page=1&count=10&pincodes={pin_str}"
|
||
res_dlv = execute_sfx_call('FORWARD', 'Single Pincode Delivery Check', 'GET', url_dlv, headers, env='production')
|
||
lm_ok = False
|
||
s_services = []
|
||
if res_dlv.get('status_code') == 200:
|
||
resp_dlv_payload = res_dlv.get('response', '')
|
||
parsed_dlv = json.loads(resp_dlv_payload) if isinstance(resp_dlv_payload, str) else resp_dlv_payload
|
||
dlv_list = parsed_dlv if isinstance(parsed_dlv, list) else (parsed_dlv.get('data', {}).get('pincodes', []) if isinstance(parsed_dlv, dict) else [])
|
||
match_dlv = next((p for p in dlv_list if str(p.get('code') or p.get('pincode')) == pin_str), None)
|
||
lm_ok = bool(match_dlv and len(match_dlv.get('services', [])) > 0)
|
||
if match_dlv:
|
||
s_services = match_dlv.get('services', ['Regular', 'Surface'])
|
||
|
||
s_serv = bool(fm_ok and lm_ok)
|
||
s_types = ['Prepaid', 'COD (Cash on Delivery)']
|
||
for s in s_services:
|
||
s_types.append(f"{s} Cargo/Delivery")
|
||
s_types.append('Reverse Pickup')
|
||
|
||
sfx_data = {
|
||
"carrier": "SHADOWFAX",
|
||
"pincode": pin_str,
|
||
"city": city,
|
||
"state": state,
|
||
"serviceable": s_serv,
|
||
"first_mile_available": fm_ok,
|
||
"first_mile_details": f"First-mile pickup serviceable in {city}, {state} (Store Sweep Active)" if fm_ok else f"First-mile pickup NOT serviceable by Shadowfax in {city}, {state} ({pin_str})",
|
||
"last_mile_available": lm_ok,
|
||
"last_mile_details": f"Last-mile delivery serviceable in {city}, {state} (Doorstep Active)" if lm_ok else f"Last-mile delivery NOT serviceable by Shadowfax in {city}, {state} ({pin_str})",
|
||
"cod_available": True,
|
||
"delivery_types": s_types if lm_ok else [],
|
||
"estimated_charge": s_cost if s_serv else None,
|
||
"currency": "INR"
|
||
}
|
||
except Exception as e:
|
||
sfx_data = {
|
||
"carrier": "SHADOWFAX",
|
||
"pincode": pin_str,
|
||
"city": city,
|
||
"state": state,
|
||
"serviceable": False,
|
||
"first_mile_available": False,
|
||
"first_mile_details": str(e),
|
||
"last_mile_available": False,
|
||
"last_mile_details": str(e),
|
||
"cod_available": False,
|
||
"delivery_types": [],
|
||
"estimated_charge": None,
|
||
"currency": "INR",
|
||
"error": str(e)
|
||
}
|
||
|
||
carriers_map = {
|
||
"SHADOWFAX": sfx_data,
|
||
"DELHIVERY": delhivery_data,
|
||
"EKART": ekart_data
|
||
}
|
||
|
||
# Best carrier recommendation
|
||
serviceable_carriers = [
|
||
c for c in carriers_map.values()
|
||
if c.get('serviceable') and c.get('estimated_charge') is not None
|
||
]
|
||
if serviceable_carriers:
|
||
serviceable_carriers.sort(key=lambda x: (x['estimated_charge'], CARRIER_PRIORITY.get(x['carrier'].upper(), 99)))
|
||
best = serviceable_carriers[0]
|
||
best_carrier = {
|
||
"selected": True,
|
||
"carrier": best['carrier'],
|
||
"charge": best['estimated_charge'],
|
||
"reason": f"Optimal carrier for pincode {pin_str} with lowest estimated charge of Rs. {best['estimated_charge']}"
|
||
}
|
||
else:
|
||
best_carrier = {
|
||
"selected": False,
|
||
"carrier": None,
|
||
"charge": None,
|
||
"reason": f"Pincode {pin_str} is not serviceable by any registered carrier."
|
||
}
|
||
|
||
return {
|
||
"status": "success",
|
||
"pincode": pin_str,
|
||
"city": city,
|
||
"state": state,
|
||
"weight": weight,
|
||
"carriers": carriers_map,
|
||
"best_carrier": best_carrier
|
||
}
|
||
|
||
|
||
|
||
def dispatch_auto_allocated_order(order_payload, selection_strategy='CHEAPEST', preferred_carrier='SHADOWFAX', allow_fallback=True, env='production'):
|
||
"""
|
||
Full cost-optimized auto-dispatch pipeline querying live production APIs for Shadowfax, Ekart, and Delhivery.
|
||
"""
|
||
order_details = order_payload.get('order_details', {})
|
||
customer_details = order_payload.get('customer_details', {})
|
||
pickup_details = order_payload.get('pickup_details', {})
|
||
|
||
pickup_pincode = pickup_details.get('pincode') or order_payload.get('pickup_pincode', '560077')
|
||
drop_pincode = customer_details.get('pincode') or order_payload.get('drop_pincode', '110009')
|
||
payment_mode = order_details.get('payment_mode', order_payload.get('payment_mode', 'Prepaid'))
|
||
weight = order_details.get('actual_weight') or order_payload.get('weight') or order_payload.get('package_weight')
|
||
|
||
if not weight or float(weight) <= 0:
|
||
return {
|
||
"status": "error",
|
||
"message": "Package weight is required. Please provide actual_weight or package_weight in grams."
|
||
}, 400
|
||
|
||
weight = float(weight)
|
||
is_cod = payment_mode.upper() == 'COD'
|
||
|
||
# Step 1: Check live availability & rates across all 3 carriers
|
||
avail = check_all_carriers_availability(
|
||
pickup_pincode=pickup_pincode,
|
||
drop_pincode=drop_pincode,
|
||
payment_type=payment_mode,
|
||
weight=weight,
|
||
env=env
|
||
)
|
||
|
||
# Step 2: Select best carrier
|
||
selection = select_best_carrier(
|
||
availability_dict=avail,
|
||
selection_strategy=selection_strategy,
|
||
preferred_carrier=preferred_carrier,
|
||
allow_fallback=allow_fallback,
|
||
is_cod=is_cod
|
||
)
|
||
|
||
if not selection.get('selected'):
|
||
return {
|
||
"status": "error",
|
||
"message": selection.get('reason'),
|
||
"availability": avail
|
||
}, 422
|
||
|
||
chosen_carrier = selection.get('carrier')
|
||
|
||
# Step 3: Dispatch to chosen carrier live API
|
||
if chosen_carrier == 'SHADOWFAX':
|
||
base_url = get_base_url(env)
|
||
token = get_default_token(env)
|
||
headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"}
|
||
url = f"{base_url}/v3/clients/orders/"
|
||
|
||
res = execute_sfx_call('FORWARD', 'API: Auto-Dispatch Shadowfax', 'POST', url, headers, order_payload, env=env)
|
||
res['assigned_carrier'] = 'SHADOWFAX'
|
||
res['estimated_cost'] = selection.get('cost')
|
||
res['selection_reason'] = selection.get('reason')
|
||
return res, res.get('status_code', 200)
|
||
|
||
elif chosen_carrier == 'EKART':
|
||
client_order_id = order_details.get('client_order_id') or order_payload.get('order_number') or f"ORD-EKART-{int(time.time())}"
|
||
ekart_payload = {
|
||
"order_number": client_order_id,
|
||
"payment_mode": payment_mode,
|
||
"service": "SURFACE",
|
||
"consignee_name": customer_details.get('name', ''),
|
||
"consignee_alternate_phone": str(customer_details.get('phone', '')),
|
||
"pickup_location": {
|
||
"name": pickup_details.get('name', ''),
|
||
"pin": str(pickup_pincode)
|
||
},
|
||
"drop_location": {
|
||
"pin": str(drop_pincode),
|
||
"address_line1": customer_details.get('address_line_1', '')
|
||
}
|
||
}
|
||
|
||
client = EkartAPIClient()
|
||
try:
|
||
res = client.create_shipment(ekart_payload)
|
||
if res.get('status') and (res.get('tracking_id') or res.get('awb')):
|
||
res['assigned_carrier'] = 'EKART'
|
||
res['estimated_cost'] = selection.get('cost')
|
||
res['selection_reason'] = selection.get('reason')
|
||
res['awb_number'] = res.get('tracking_id') or res.get('awb')
|
||
return res, 200
|
||
|
||
# If Ekart booking failed (e.g. unregistered facility), fallback to Delhivery
|
||
if allow_fallback and avail.get('carriers', {}).get('DELHIVERY', {}).get('serviceable'):
|
||
chosen_carrier = 'DELHIVERY'
|
||
else:
|
||
res['assigned_carrier'] = 'EKART'
|
||
return res, 400
|
||
except Exception as e:
|
||
if allow_fallback and avail.get('carriers', {}).get('DELHIVERY', {}).get('serviceable'):
|
||
chosen_carrier = 'DELHIVERY'
|
||
else:
|
||
return {"status": False, "error": str(e), "assigned_carrier": "EKART"}, 500
|
||
|
||
if chosen_carrier == 'DELHIVERY':
|
||
client_order_id = order_details.get('client_order_id') or order_payload.get('order_number') or f"ORD-DELHIVERY-{int(time.time())}"
|
||
# Select destination city and state based directly on customer drop_pincode
|
||
dest_city, dest_state = resolve_city_state_by_pincode(
|
||
drop_pincode,
|
||
default_city=customer_details.get('city'),
|
||
default_state=customer_details.get('state')
|
||
)
|
||
|
||
# Resolve seller pickup city and state based directly on pickup_pincode
|
||
pickup_city, pickup_state = resolve_city_state_by_pincode(
|
||
pickup_pincode,
|
||
default_city=pickup_details.get('city'),
|
||
default_state=pickup_details.get('state')
|
||
)
|
||
seller_facility_name = pickup_details.get('name') or 'vignesh store'
|
||
|
||
delhivery_cmu_payload = {
|
||
"shipments": [
|
||
{
|
||
"name": customer_details.get('name', ''),
|
||
"add": customer_details.get('address_line_1', ''),
|
||
"pin": str(drop_pincode),
|
||
"city": dest_city,
|
||
"state": dest_state,
|
||
"country": "India",
|
||
"phone": str(customer_details.get('phone', '')),
|
||
"order": client_order_id,
|
||
"payment_mode": payment_mode,
|
||
"cod_amount": str(order_details.get('cod_amount', order_details.get('product_value', 0))) if is_cod else "0",
|
||
"total_amount": str(order_details.get('product_value', 0)),
|
||
"quantity": "1",
|
||
"weight": str(weight),
|
||
"shipping_mode": "Surface"
|
||
}
|
||
],
|
||
"pickup_location": {
|
||
"name": seller_facility_name,
|
||
"add": pickup_details.get('address_line_1', ''),
|
||
"pin": str(pickup_pincode),
|
||
"city": pickup_city,
|
||
"state": pickup_state,
|
||
"country": "India",
|
||
"phone": str(pickup_details.get('phone', ''))
|
||
}
|
||
}
|
||
|
||
client = DelhiveryAPIClient()
|
||
try:
|
||
res = client.create_shipment(delhivery_cmu_payload)
|
||
pkg_list = res.get('packages', []) if isinstance(res, dict) else []
|
||
has_waybill = bool(pkg_list and pkg_list[0].get('waybill'))
|
||
if res.get('success') and has_waybill:
|
||
res['assigned_carrier'] = 'DELHIVERY'
|
||
res['estimated_cost'] = selection.get('cost')
|
||
res['selection_reason'] = selection.get('reason')
|
||
res['awb_number'] = pkg_list[0]['waybill']
|
||
res['waybill'] = pkg_list[0]['waybill']
|
||
res['awb'] = pkg_list[0]['waybill']
|
||
return res, 200
|
||
|
||
# If Delhivery booking was unconfirmed, failover to Shadowfax only if Shadowfax is actually serviceable
|
||
is_sfx_serv = avail.get('carriers', {}).get('SHADOWFAX', {}).get('serviceable')
|
||
if allow_fallback and is_sfx_serv:
|
||
base_url = get_base_url(env)
|
||
token = get_default_token(env)
|
||
headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"}
|
||
url = f"{base_url}/v3/clients/orders/"
|
||
sfx_res = execute_sfx_call('FORWARD', 'Auto-Dispatch Failover to Shadowfax', 'POST', url, headers, order_payload, env=env)
|
||
sfx_res['assigned_carrier'] = 'SHADOWFAX'
|
||
sfx_res['selection_reason'] = f"Delhivery booking unconfirmed ({res.get('rmk', 'No waybill')}). Successfully failed over to Shadowfax."
|
||
return sfx_res, sfx_res.get('status_code', 200)
|
||
|
||
res['assigned_carrier'] = 'DELHIVERY'
|
||
return res, 400
|
||
except Exception as e:
|
||
is_sfx_serv = avail.get('carriers', {}).get('SHADOWFAX', {}).get('serviceable')
|
||
if allow_fallback and is_sfx_serv:
|
||
base_url = get_base_url(env)
|
||
token = get_default_token(env)
|
||
headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"}
|
||
url = f"{base_url}/v3/clients/orders/"
|
||
sfx_res = execute_sfx_call('FORWARD', 'Auto-Dispatch Failover to Shadowfax', 'POST', url, headers, order_payload, env=env)
|
||
sfx_res['assigned_carrier'] = 'SHADOWFAX'
|
||
sfx_res['selection_reason'] = f"Delhivery exception ({str(e)}). Successfully failed over to Shadowfax."
|
||
return sfx_res, sfx_res.get('status_code', 200)
|
||
return {"status": False, "error": str(e), "assigned_carrier": "DELHIVERY"}, 500
|
||
|
||
return {"status": "error", "message": "Unknown carrier error"}, 500
|
||
|
||
|
||
def get_carrier_pickup_capabilities(carrier=''):
|
||
"""
|
||
Return pickup scheduling capabilities for each carrier:
|
||
- DELHIVERY: Both Date and Time Slot are supported.
|
||
- EKART: Date is supported (dispatchDate), Time is fixed to standard hub sweeps.
|
||
- SHADOWFAX: Both Date and Time are automatically managed based on daily cutoff SLA.
|
||
"""
|
||
carrier_upper = str(carrier or '').strip().upper()
|
||
if 'DELHI' in carrier_upper:
|
||
key = 'DELHIVERY'
|
||
elif 'EKART' in carrier_upper:
|
||
key = 'EKART'
|
||
elif 'SHADOW' in carrier_upper or 'SFX' in carrier_upper:
|
||
key = 'SHADOWFAX'
|
||
else:
|
||
key = 'ALL'
|
||
|
||
capabilities = {
|
||
'DELHIVERY': {
|
||
'carrier': 'DELHIVERY',
|
||
'carrier_display': 'Delhivery Logistics',
|
||
'date_selection': True,
|
||
'time_selection': True,
|
||
'time_slots': [
|
||
{'id': 'morning', 'label': 'Morning (10:00 AM – 1:00 PM)', 'value': '10:00:00'},
|
||
{'id': 'afternoon', 'label': 'Afternoon (2:00 PM – 5:00 PM)', 'value': '14:00:00'},
|
||
{'id': 'evening', 'label': 'Evening (5:00 PM – 7:00 PM)', 'value': '17:00:00'}
|
||
],
|
||
'description': 'Delhivery supports custom pickup date and flexible time slot booking via dedicated first-mile courier scheduling.'
|
||
},
|
||
'EKART': {
|
||
'carrier': 'EKART',
|
||
'carrier_display': 'Ekart Logistics',
|
||
'date_selection': True,
|
||
'time_selection': False,
|
||
'time_slots': [],
|
||
'time_slot_label': 'Standard Daily Hub Sweep (Morning & Evening)',
|
||
'description': 'Ekart supports setting a preferred dispatch date. Pickup time is governed by daily routine courier hub sweeps.'
|
||
},
|
||
'SHADOWFAX': {
|
||
'carrier': 'SHADOWFAX',
|
||
'carrier_display': 'Shadowfax Logistics',
|
||
'date_selection': False,
|
||
'time_selection': False,
|
||
'time_slots': [],
|
||
'time_slot_label': 'Automated Cut-Off Dispatch Sweep',
|
||
'description': 'Shadowfax automatically assigns courier sweep riders for the registered seller location based on daily dispatch cut-off SLA.'
|
||
}
|
||
}
|
||
|
||
if key in capabilities:
|
||
return capabilities[key]
|
||
return capabilities
|
||
|
||
|
||
def schedule_carrier_pickup(carrier, pickup_date, pickup_time=None, pickup_location=None, package_count=1, tracking_ids=None, client_order_id=None, notes=None, env='production'):
|
||
"""
|
||
Unified multi-carrier pickup scheduling dispatcher.
|
||
Carrier specific behaviors:
|
||
- DELHIVERY: Calls POST /fm/request/create/ with date, time, location, package count.
|
||
- EKART: Calls POST /data/shipment/dispatch-date with tracking IDs and dispatchDate.
|
||
- SHADOWFAX: Confirms and records automated warehouse courier sweep.
|
||
"""
|
||
carrier_upper = str(carrier or '').strip().upper()
|
||
if 'DELHI' in carrier_upper:
|
||
carrier_key = 'DELHIVERY'
|
||
elif 'EKART' in carrier_upper:
|
||
carrier_key = 'EKART'
|
||
elif 'SHADOW' in carrier_upper or 'SFX' in carrier_upper:
|
||
carrier_key = 'SHADOWFAX'
|
||
else:
|
||
# Fallback identification based on tracking ID pattern
|
||
trk = str((tracking_ids[0] if isinstance(tracking_ids, list) and tracking_ids else tracking_ids) or '')
|
||
if trk.isdigit() or trk.startswith(('DELHI', '698', '14', '12')):
|
||
carrier_key = 'DELHIVERY'
|
||
elif trk.startswith(('EKART', 'ELIT', '500')):
|
||
carrier_key = 'EKART'
|
||
else:
|
||
carrier_key = 'SHADOWFAX'
|
||
|
||
clean_date = str(pickup_date or '').strip()
|
||
if not clean_date:
|
||
clean_date = time.strftime('%Y-%m-%d')
|
||
|
||
# Normalize tracking IDs
|
||
if tracking_ids and not isinstance(tracking_ids, list):
|
||
tracking_ids = [str(tracking_ids).strip()]
|
||
elif not tracking_ids:
|
||
tracking_ids = []
|
||
|
||
# --- 1. DELHIVERY ---
|
||
if carrier_key == 'DELHIVERY':
|
||
# Map time keywords or format HH:MM:SS
|
||
slot_map = {
|
||
'morning': '10:00:00',
|
||
'afternoon': '14:00:00',
|
||
'evening': '17:00:00'
|
||
}
|
||
clean_time = str(pickup_time or '14:00:00').strip().lower()
|
||
if clean_time in slot_map:
|
||
clean_time = slot_map[clean_time]
|
||
elif ':' not in clean_time:
|
||
clean_time = '14:00:00'
|
||
elif len(clean_time) == 5:
|
||
clean_time = f"{clean_time}:00"
|
||
|
||
location_name = str(pickup_location or '').strip()
|
||
pkg_cnt = max(1, int(package_count or 1))
|
||
|
||
# If location name is not provided, is generic, or is just a city name, resolve from shipment or tracking
|
||
if not location_name or location_name.lower() in ('seller warehouse', 'seller store', 'artisan workshop hub', 'coimbatore', 'chennai', 'bangalore', 'bengaluru'):
|
||
resolved_loc = None
|
||
# Check local DelhiveryShipment first
|
||
if tracking_ids:
|
||
first_trk = str(tracking_ids[0]).strip()
|
||
try:
|
||
from delhivery.models import DelhiveryShipment
|
||
ds = DelhiveryShipment.objects.filter(waybill=first_trk).first()
|
||
if ds and ds.pickup_location_name:
|
||
resolved_loc = ds.pickup_location_name
|
||
except Exception:
|
||
pass
|
||
|
||
# If not found locally, query live Delhivery package details
|
||
if not resolved_loc:
|
||
try:
|
||
import requests
|
||
from delhivery.models import DelhiveryCredential
|
||
cred = DelhiveryCredential.objects.filter(is_active=True).first()
|
||
tok = cred.api_token if cred and cred.api_token else 'e70dfd63665335ee6e0a1094f6d34e17a126f5a6'
|
||
t_resp = requests.get(
|
||
f"https://track.delhivery.com/api/v1/packages/json/?waybill={first_trk}",
|
||
headers={"Authorization": f"Token {tok}"},
|
||
timeout=5
|
||
)
|
||
if t_resp.status_code == 200:
|
||
s_data = t_resp.json().get('ShipmentData', [])
|
||
if s_data and s_data[0].get('Shipment', {}).get('PickupLocation'):
|
||
resolved_loc = s_data[0]['Shipment']['PickupLocation']
|
||
except Exception:
|
||
pass
|
||
|
||
if resolved_loc:
|
||
location_name = resolved_loc
|
||
elif not location_name:
|
||
location_name = 'SAM'
|
||
|
||
client = DelhiveryAPIClient()
|
||
try:
|
||
res = client.create_pickup_request(
|
||
pickup_time=clean_time,
|
||
pickup_date=clean_date,
|
||
pickup_location=location_name,
|
||
expected_package_count=pkg_cnt
|
||
)
|
||
# Check for error from Delhivery
|
||
has_error = isinstance(res, dict) and (res.get('status') == 'error' or (res.get('success') is False and not res.get('pr_exist')) or (res.get('error') and not res.get('pr_exist')) or res.get('prepaid'))
|
||
if has_error:
|
||
err_msg = res.get('message') or res.get('error') or (f"Delhivery error: {res.get('prepaid')}" if res.get('prepaid') else "Delhivery pickup request failed")
|
||
return {
|
||
'status': 'error',
|
||
'carrier': 'DELHIVERY',
|
||
'message': err_msg,
|
||
'details': res
|
||
}, 400
|
||
|
||
pickup_id = res.get('pickup_id') or res.get('id') or res.get('pr_id')
|
||
record_pickup_schedule(
|
||
tracking_ids=tracking_ids,
|
||
carrier='DELHIVERY',
|
||
pickup_date=clean_date,
|
||
pickup_time=clean_time,
|
||
pickup_id=pickup_id,
|
||
pickup_location=location_name,
|
||
client_order_id=client_order_id,
|
||
expected_package_count=pkg_cnt,
|
||
notes=notes,
|
||
raw_response=res
|
||
)
|
||
return {
|
||
'status': 'success',
|
||
'carrier': 'DELHIVERY',
|
||
'carrier_display': 'Delhivery Logistics',
|
||
'capabilities': {'date_selection': True, 'time_selection': True},
|
||
'pickup_date': clean_date,
|
||
'pickup_time': clean_time,
|
||
'pickup_location': location_name,
|
||
'expected_package_count': pkg_cnt,
|
||
'pickup_id': pickup_id,
|
||
'tracking_ids': tracking_ids,
|
||
'client_order_id': client_order_id,
|
||
'message': f"Delhivery courier pickup scheduled successfully for {clean_date} at {clean_time}",
|
||
'raw_response': res
|
||
}, 200
|
||
except Exception as e:
|
||
return {
|
||
'status': 'error',
|
||
'carrier': 'DELHIVERY',
|
||
'message': f"Failed to schedule Delhivery pickup: {str(e)}",
|
||
'details': str(e)
|
||
}, 500
|
||
|
||
# --- 2. EKART ---
|
||
elif carrier_key == 'EKART':
|
||
client = EkartAPIClient()
|
||
try:
|
||
res = client.set_dispatch_date(
|
||
tracking_ids=tracking_ids,
|
||
dispatch_date=clean_date
|
||
) if tracking_ids else {'status': True, 'message': 'Preferred dispatch date recorded'}
|
||
|
||
record_pickup_schedule(
|
||
tracking_ids=tracking_ids,
|
||
carrier='EKART',
|
||
pickup_date=clean_date,
|
||
pickup_time='Standard Hub Sweep',
|
||
pickup_location=pickup_location,
|
||
client_order_id=client_order_id,
|
||
notes=notes,
|
||
raw_response=res
|
||
)
|
||
return {
|
||
'status': 'success',
|
||
'carrier': 'EKART',
|
||
'carrier_display': 'Ekart Logistics',
|
||
'capabilities': {'date_selection': True, 'time_selection': False},
|
||
'pickup_date': clean_date,
|
||
'pickup_time': 'Standard Hub Sweep',
|
||
'time_slot_note': 'Ekart routine warehouse sweep (Morning & Evening cutoffs)',
|
||
'tracking_ids': tracking_ids,
|
||
'client_order_id': client_order_id,
|
||
'message': f"Ekart preferred dispatch date set to {clean_date}. Sweep scheduled during daily Ekart run.",
|
||
'raw_response': res
|
||
}, 200
|
||
except Exception as e:
|
||
return {
|
||
'status': 'error',
|
||
'carrier': 'EKART',
|
||
'message': f"Failed to set Ekart dispatch date: {str(e)}",
|
||
'details': str(e)
|
||
}, 500
|
||
|
||
# --- 3. SHADOWFAX ---
|
||
else:
|
||
record_pickup_schedule(
|
||
tracking_ids=tracking_ids,
|
||
carrier='SHADOWFAX',
|
||
pickup_date=clean_date,
|
||
pickup_time='Automated Cut-Off Dispatch Sweep',
|
||
pickup_location=pickup_location,
|
||
client_order_id=client_order_id,
|
||
notes=notes,
|
||
raw_response={'status': 'success', 'scheduled': True}
|
||
)
|
||
return {
|
||
'status': 'success',
|
||
'carrier': 'SHADOWFAX',
|
||
'carrier_display': 'Shadowfax Logistics',
|
||
'capabilities': {'date_selection': False, 'time_selection': False},
|
||
'pickup_date': clean_date,
|
||
'pickup_time': 'Automated Cut-Off Dispatch Sweep',
|
||
'time_slot_note': 'Shadowfax courier rider allocated automatically per registered seller cutoff SLA',
|
||
'tracking_ids': tracking_ids,
|
||
'client_order_id': client_order_id,
|
||
'message': f"Shadowfax automated pickup sweep confirmed for warehouse on {clean_date}.",
|
||
'raw_response': {'status': 'success', 'scheduled': True}
|
||
}, 200
|
||
|
||
|
||
def record_pickup_schedule(tracking_ids, carrier, pickup_date, pickup_time, pickup_id=None, pickup_location=None, client_order_id=None, expected_package_count=1, notes='', raw_response=None, status='SCHEDULED'):
|
||
"""
|
||
Saves or updates PickupSchedule records for tracking ID(s) upon scheduling.
|
||
"""
|
||
try:
|
||
from .models import PickupSchedule
|
||
carrier_upper = str(carrier or 'DELHIVERY').strip().upper()
|
||
carrier_disp = 'Delhivery Logistics' if 'DELHI' in carrier_upper else ('Ekart Logistics' if 'EKART' in carrier_upper else 'Shadowfax Logistics')
|
||
|
||
slot_label = ''
|
||
t_str = str(pickup_time or '').strip()
|
||
if t_str.startswith('10'):
|
||
slot_label = 'Morning Slot (10:00 AM – 1:00 PM)'
|
||
elif t_str.startswith('14'):
|
||
slot_label = 'Afternoon Slot (2:00 PM – 5:00 PM / 14:00 – 18:00)'
|
||
elif t_str.startswith('17'):
|
||
slot_label = 'Evening Slot (5:00 PM – 7:00 PM)'
|
||
else:
|
||
slot_label = t_str or 'Standard Sweep'
|
||
|
||
t_list = tracking_ids if isinstance(tracking_ids, list) and tracking_ids else ([str(tracking_ids).strip()] if tracking_ids else [])
|
||
if not t_list and client_order_id:
|
||
t_list = [f"ORD-{client_order_id}"]
|
||
if not t_list:
|
||
t_list = [f"LOC-{pickup_location or 'HUB'}-{pickup_date}"]
|
||
|
||
created_objs = []
|
||
for trk in t_list:
|
||
if not trk:
|
||
continue
|
||
obj, _ = PickupSchedule.objects.update_or_create(
|
||
tracking_id=str(trk).strip(),
|
||
defaults={
|
||
'client_order_id': str(client_order_id or '').strip(),
|
||
'carrier': carrier_upper,
|
||
'carrier_display': carrier_disp,
|
||
'pickup_id': str(pickup_id or '').strip(),
|
||
'pickup_date': str(pickup_date),
|
||
'pickup_time': str(pickup_time),
|
||
'pickup_slot': slot_label,
|
||
'pickup_location': str(pickup_location or ''),
|
||
'status': status,
|
||
'expected_package_count': int(expected_package_count or 1),
|
||
'notes': str(notes or ''),
|
||
'raw_response': raw_response if isinstance(raw_response, dict) else {'data': str(raw_response)}
|
||
}
|
||
)
|
||
created_objs.append(obj)
|
||
return created_objs
|
||
except Exception as e:
|
||
logger.warning(f"Failed to record pickup schedule: {e}")
|
||
return []
|
||
|
||
|
||
def check_pickup_schedule_for_tracking(tracking_id):
|
||
"""
|
||
Checks courier pickup schedule (date, time, arrival window, pickup request ID, location, carrier, status)
|
||
for a given tracking / AWB number.
|
||
Queries:
|
||
1. Local PickupSchedule database model in shippingservice.
|
||
2. Live Carrier Tracking API (Delhivery, Ekart, Shadowfax).
|
||
3. Cross-database Order records (customerprofile_beta database).
|
||
"""
|
||
trk = str(tracking_id or '').strip()
|
||
if not trk or trk in ('Pending', 'N/A'):
|
||
return {
|
||
'status': 'error',
|
||
'message': 'Valid tracking ID / AWB number is required.'
|
||
}, 400
|
||
|
||
from .models import PickupSchedule
|
||
local_obj = PickupSchedule.objects.filter(tracking_id=trk).order_by('-updated_at').first()
|
||
if not local_obj:
|
||
local_obj = PickupSchedule.objects.filter(client_order_id=trk).order_by('-updated_at').first()
|
||
|
||
# Identify carrier
|
||
if local_obj and local_obj.carrier:
|
||
carrier_key = local_obj.carrier
|
||
elif trk.isdigit() or trk.startswith(('DELHI', '698', '14', '12', '179', '139')):
|
||
carrier_key = 'DELHIVERY'
|
||
elif trk.startswith(('EKART', 'ELIT', '500')):
|
||
carrier_key = 'EKART'
|
||
else:
|
||
carrier_key = 'SHADOWFAX'
|
||
|
||
carrier_display = 'Delhivery Logistics' if carrier_key == 'DELHIVERY' else ('Ekart Logistics' if carrier_key == 'EKART' else 'Shadowfax Logistics')
|
||
|
||
pickup_id = local_obj.pickup_id if (local_obj and local_obj.pickup_id) else None
|
||
pickup_date = local_obj.pickup_date if (local_obj and local_obj.pickup_date) else None
|
||
pickup_time = local_obj.pickup_time if (local_obj and local_obj.pickup_time) else None
|
||
pickup_slot = local_obj.pickup_slot if (local_obj and local_obj.pickup_slot) else None
|
||
pickup_location = local_obj.pickup_location if (local_obj and local_obj.pickup_location) else None
|
||
order_id = local_obj.client_order_id if (local_obj and local_obj.client_order_id) else None
|
||
shipment_status = 'Manifested'
|
||
carrier_live_data = {}
|
||
consignee_info = {}
|
||
destination_info = {}
|
||
scan_history = []
|
||
|
||
# 1. Query live carrier tracking API if carrier is DELHIVERY
|
||
if carrier_key == 'DELHIVERY':
|
||
try:
|
||
import requests
|
||
from delhivery.models import DelhiveryCredential
|
||
cred = DelhiveryCredential.objects.filter(is_active=True).first()
|
||
tok = cred.api_token if cred and cred.api_token else 'e70dfd63665335ee6e0a1094f6d34e17a126f5a6'
|
||
|
||
t_resp = requests.get(
|
||
f"https://track.delhivery.com/api/v1/packages/json/?waybill={trk}",
|
||
headers={"Authorization": f"Token {tok}"},
|
||
timeout=6
|
||
)
|
||
if t_resp.status_code == 200:
|
||
s_data = t_resp.json().get('ShipmentData', [])
|
||
if s_data and isinstance(s_data, list):
|
||
sh = s_data[0].get('Shipment', {})
|
||
carrier_live_data = sh
|
||
shipment_status = sh.get('Status', {}).get('Status') or 'Manifested'
|
||
if not pickup_location and sh.get('PickupLocation'):
|
||
pickup_location = sh.get('PickupLocation')
|
||
if not order_id and sh.get('ReferenceNo'):
|
||
order_id = sh.get('ReferenceNo')
|
||
|
||
consignee = sh.get('Consignee') or {}
|
||
consignee_info = {
|
||
'name': consignee.get('Name'),
|
||
'city': consignee.get('City'),
|
||
'state': consignee.get('State'),
|
||
'pincode': consignee.get('PinCode')
|
||
}
|
||
destination_info = {
|
||
'city': sh.get('Destination') or consignee.get('City'),
|
||
'pincode': consignee.get('PinCode')
|
||
}
|
||
|
||
for sc in sh.get('Scans', []):
|
||
sd = sc.get('ScanDetail', {})
|
||
scan_history.append({
|
||
'scan': sd.get('Scan'),
|
||
'instructions': sd.get('Instructions'),
|
||
'location': sd.get('ScannedLocation'),
|
||
'timestamp': sd.get('ScanDateTime')
|
||
})
|
||
except Exception as dlh_err:
|
||
logger.warning(f"Error querying Delhivery live tracking for {trk}: {dlh_err}")
|
||
|
||
# 2. Query cross-database Order records (customerprofile_beta) to retrieve confirmed pickup ID & ETA
|
||
if not pickup_id or not pickup_date:
|
||
try:
|
||
import psycopg2
|
||
conn = psycopg2.connect(
|
||
dbname='customerprofile_beta',
|
||
user='vignesh',
|
||
password='vignesh_secret_pass_2026',
|
||
host='16.113.106.152',
|
||
port='5432'
|
||
)
|
||
cur = conn.cursor()
|
||
cur.execute(
|
||
"SELECT id, order_id, status, carrier, tracking, pickup_id, eta, pickup_payload FROM api_order WHERE tracking=%s OR order_id=%s",
|
||
(trk, order_id or trk)
|
||
)
|
||
row = cur.fetchone()
|
||
if row:
|
||
db_id, db_order_id, db_status, db_carrier, db_tracking, db_pickup_id, db_eta, db_payload = row
|
||
if not order_id and db_order_id:
|
||
order_id = db_order_id
|
||
if db_pickup_id:
|
||
pickup_id = str(db_pickup_id)
|
||
if db_eta and 'Pickup:' in str(db_eta):
|
||
import re
|
||
m_date = re.search(r'Pickup:\s*([\d-]+)', str(db_eta))
|
||
m_time = re.search(r'\(([\d:]+)\)', str(db_eta))
|
||
if m_date and not pickup_date:
|
||
pickup_date = m_date.group(1)
|
||
if m_time and not pickup_time:
|
||
pickup_time = m_time.group(1)
|
||
if isinstance(db_payload, dict):
|
||
if not pickup_id and db_payload.get('pickup_id'):
|
||
pickup_id = str(db_payload.get('pickup_id'))
|
||
if not pickup_date and db_payload.get('pickup_date'):
|
||
pickup_date = db_payload.get('pickup_date')
|
||
if not pickup_time and db_payload.get('pickup_time'):
|
||
pickup_time = db_payload.get('pickup_time')
|
||
conn.close()
|
||
except Exception as db_err:
|
||
logger.warning(f"Cross-database order lookup error: {db_err}")
|
||
|
||
# 3. Check if warehouse location has an active confirmed Pickup Request
|
||
if not pickup_id and pickup_location:
|
||
loc_match = PickupSchedule.objects.filter(pickup_location=pickup_location).exclude(pickup_id='').order_by('-updated_at').first()
|
||
if loc_match:
|
||
pickup_id = loc_match.pickup_id
|
||
if not pickup_date:
|
||
pickup_date = loc_match.pickup_date
|
||
if not pickup_time:
|
||
pickup_time = loc_match.pickup_time
|
||
|
||
# Default fallbacks if date or time are not yet resolved
|
||
if not pickup_date:
|
||
pickup_date = time.strftime('%Y-%m-%d')
|
||
if not pickup_time:
|
||
pickup_time = '14:00:00' if carrier_key == 'DELHIVERY' else ('Standard Hub Sweep' if carrier_key == 'EKART' else 'Automated Cut-Off Sweep')
|
||
|
||
# Format slot description
|
||
t_clean = str(pickup_time).strip()
|
||
if t_clean.startswith('10'):
|
||
pickup_slot = 'Morning Slot (10:00 AM – 1:00 PM)'
|
||
elif t_clean.startswith('14'):
|
||
pickup_slot = 'Afternoon Slot (2:00 PM – 5:00 PM / 14:00 – 18:00)'
|
||
elif t_clean.startswith('17'):
|
||
pickup_slot = 'Evening Slot (5:00 PM – 7:00 PM)'
|
||
elif not pickup_slot:
|
||
pickup_slot = t_clean
|
||
|
||
# Determine lifecycle status
|
||
is_picked_up = str(shipment_status).lower() in ['in transit', 'picked up', 'out for delivery', 'delivered'] or carrier_live_data.get('PickedupDate')
|
||
if is_picked_up:
|
||
status_code = 'IN_TRANSIT'
|
||
status_display = f"Package Picked Up & In Transit with {carrier_display}"
|
||
elif pickup_id:
|
||
status_code = 'CONFIRMED'
|
||
status_display = f"Pickup Confirmed with {carrier_display} (PR #{pickup_id}) for {pickup_date} ({pickup_slot})"
|
||
else:
|
||
status_code = 'SCHEDULED'
|
||
status_display = f"Pickup Scheduled with {carrier_display} for {pickup_date} ({pickup_slot})"
|
||
|
||
# Cache/update into PickupSchedule table
|
||
try:
|
||
PickupSchedule.objects.update_or_create(
|
||
tracking_id=trk,
|
||
defaults={
|
||
'client_order_id': str(order_id or ''),
|
||
'carrier': carrier_key,
|
||
'carrier_display': carrier_display,
|
||
'pickup_id': str(pickup_id or ''),
|
||
'pickup_date': str(pickup_date),
|
||
'pickup_time': str(pickup_time),
|
||
'pickup_slot': str(pickup_slot),
|
||
'pickup_location': str(pickup_location or 'SAM'),
|
||
'status': status_code,
|
||
'notes': f"Queried via check_pickup_schedule ({time.strftime('%Y-%m-%d %H:%M:%S')})"
|
||
}
|
||
)
|
||
except Exception as save_err:
|
||
logger.warning(f"Error caching PickupSchedule: {save_err}")
|
||
|
||
return {
|
||
'status': 'success',
|
||
'tracking_id': trk,
|
||
'order_id': order_id,
|
||
'carrier': carrier_key,
|
||
'carrier_display': carrier_display,
|
||
'pickup_scheduled': True,
|
||
'pickup_id': pickup_id,
|
||
'pickup_date': pickup_date,
|
||
'pickup_time': pickup_time,
|
||
'pickup_slot': pickup_slot,
|
||
'pickup_location': pickup_location or 'SAM',
|
||
'shipment_status': shipment_status,
|
||
'pickup_status': status_code,
|
||
'status_display': status_display,
|
||
'destination': destination_info,
|
||
'consignee': consignee_info,
|
||
'scans': scan_history,
|
||
'details': {
|
||
'expected_package_count': local_obj.expected_package_count if local_obj else 1,
|
||
'manifest_date': carrier_live_data.get('PickUpDate'),
|
||
'picked_up_date': carrier_live_data.get('PickedupDate')
|
||
}
|
||
}, 200
|
||
|
||
|