feat: add shipping service application with forward and reverse logistics, carrier allocation, and dashboard templates

This commit is contained in:
vignesh 2026-10-11 20:49:03 +05:30
parent ce484c4f11
commit e327ac8988
18 changed files with 1121 additions and 132 deletions

View file

@ -32,7 +32,7 @@ sequenceDiagram
- **Forward Logistics**: Pincode serviceability checks, pre-allocated AWB numbers, Marketplace seller pickup orders, Warehouse hub dispatch orders, V4 real-time tracking, order cancellations, support escalations, digital Proof of Delivery (POD), and dynamic doorstep COD UPI QR code generation. - **Forward Logistics**: Pincode serviceability checks, pre-allocated AWB numbers, Marketplace seller pickup orders, Warehouse hub dispatch orders, V4 real-time tracking, order cancellations, support escalations, digital Proof of Delivery (POD), and dynamic doorstep COD UPI QR code generation.
- **Reverse Logistics**: Reverse pincode serviceability, reverse AWB pre-allocation, customer doorstep return requests with configurable **Doorstep Quality Check (QC)** rules (brand match, IMEI/serial number validation, physical condition, photo requirements), V4 reverse tracking, and Proof of Pickup (POP). - **Reverse Logistics**: Reverse pincode serviceability, reverse AWB pre-allocation, customer doorstep return requests with configurable **Doorstep Quality Check (QC)** rules (brand match, IMEI/serial number validation, physical condition, photo requirements), V4 reverse tracking, and Proof of Pickup (POP).
- **Automated Audit Logging**: Every outbound request to Shadowfax and every incoming webhook callback is stored in PostgreSQL (`ActivityLog`) capturing headers, request payload, status code, response time in ms, and response payload. - **Automated Audit Logging**: Every outbound request to Shadowfax and every incoming webhook callback is stored in PostgreSQL (`ActivityLog`) capturing headers, request payload, status code, response time in ms, and response payload.
- **Visual Testing Dashboard**: Clean dark-mode web interface at `/forward/`, `/reverse/`, `/logs/`, and `/webhooks/` for interactive debugging and live log inspection. - **Visual Testing Dashboard**: Clean dark-mode web interface at `/checker/` (Multi-Carrier Route Checker), `/forward/`, `/reverse/`, `/label/`, `/logs/`, and `/webhooks/` for interactive debugging and live inspection.
--- ---
@ -55,9 +55,10 @@ External services consume these REST API endpoints using JSON payloads over HTTP
### 0. Dynamic Carrier Allocation & Routing APIs ### 0. Dynamic Carrier Allocation & Routing APIs
#### 0.1 Check All Carrier Availability #### 0.1 Check All Carrier Availability (First-Mile & Last-Mile)
Evaluate pincode serviceability and COD availability across all integrated carriers (Shadowfax & Ekart) in parallel. Evaluate first-mile origin pickup hub and last-mile destination drop serviceability, COD availability, and freight rates across all integrated carriers (Shadowfax, Delhivery, Ekart) in parallel.
- **Interactive UI**: `http://localhost:8000/checker/` (or `https://beta-shipping.tradhox.com/checker/`)
- **Endpoint**: `POST /api/v1/carrier/check-availability/` - **Endpoint**: `POST /api/v1/carrier/check-availability/`
- **Request Body**: - **Request Body**:
```json ```json
@ -66,7 +67,7 @@ Evaluate pincode serviceability and COD availability across all integrated carri
"drop_pincode": "110009", "drop_pincode": "110009",
"payment_type": "Prepaid", "payment_type": "Prepaid",
"weight": 500, "weight": 500,
"use_mock": true "use_mock": false
} }
``` ```
- **Response (`200 OK`)**: - **Response (`200 OK`)**:
@ -76,9 +77,57 @@ Evaluate pincode serviceability and COD availability across all integrated carri
"pickup_pincode": "560077", "pickup_pincode": "560077",
"drop_pincode": "110009", "drop_pincode": "110009",
"payment_type": "Prepaid", "payment_type": "Prepaid",
"best_carrier": {
"selected": true,
"carrier": "DELHIVERY",
"cost": 35.0,
"reason": "Selected DELHIVERY as lowest cost carrier (Cost: ₹35.0)"
},
"carriers": { "carriers": {
"SHADOWFAX": { "carrier": "SHADOWFAX", "serviceable": true, "cod_available": true }, "SHADOWFAX": {
"EKART": { "carrier": "EKART", "serviceable": true, "cod_available": true } "carrier": "SHADOWFAX",
"serviceable": true,
"cod_available": true,
"estimated_cost": 60.0,
"first_mile": {
"serviceable": true,
"pincode": "560077",
"city": "Bengaluru",
"state": "Karnataka",
"hub_active": true,
"message": "First-mile pickup serviceable in Bengaluru, Karnataka"
},
"last_mile": {
"serviceable": true,
"pincode": "110009",
"city": "Delhi",
"state": "Delhi",
"cod_available": true,
"message": "Last-mile delivery serviceable in Delhi, Delhi"
}
},
"DELHIVERY": {
"carrier": "DELHIVERY",
"serviceable": true,
"cod_available": true,
"estimated_cost": 35.0,
"first_mile": {
"serviceable": true,
"pincode": "560077",
"city": "Bengaluru",
"state": "Karnataka",
"reverse_pickup": true,
"message": "First-mile pickup serviceable in Bengaluru, Karnataka"
},
"last_mile": {
"serviceable": true,
"pincode": "110009",
"city": "North Delhi",
"state": "Delhi",
"cod_available": true,
"message": "Last-mile delivery serviceable in North Delhi, Delhi"
}
}
} }
} }
``` ```
@ -146,7 +195,7 @@ Determine if Shadowfax serves specified destination/pickup pincodes for Express
{ {
"pincodes": "560077,110009", "pincodes": "560077,110009",
"service": "express", "service": "express",
"env": "staging", "env": "production",
"use_mock": true "use_mock": true
} }
``` ```
@ -182,7 +231,7 @@ Create a new forward delivery order (Marketplace Seller Pickup or Warehouse Hub
- **Request Body Example**: - **Request Body Example**:
```json ```json
{ {
"env": "staging", "env": "production",
"use_mock": true, "use_mock": true,
"payload": { "payload": {
"order_details": { "order_details": {

View file

@ -19,7 +19,7 @@
<div class="card" style="margin-bottom: 1.5rem;"> <div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title"> <div class="card-title">
<span>🌐 Ekart API Host & Credentials</span> <span>🌐 Ekart API Host & Credentials</span>
<span class="badge badge-forward">Production / Staging Base: https://app.elite.ekartlogistics.in</span> <span class="badge badge-forward">Production Base: https://app.elite.ekartlogistics.in</span>
</div> </div>
<div style="display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 1rem; align-items: end;"> <div style="display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 1rem; align-items: end;">
<div class="form-group" style="margin: 0;"> <div class="form-group" style="margin: 0;">

View file

@ -203,16 +203,47 @@ def resolve_city_state_by_pincode(pincode, default_city=None, default_state=None
return ('Bengaluru', 'Karnataka') return ('Bengaluru', 'Karnataka')
def check_shadowfax_availability(pickup_pincode, drop_pincode, service='express', weight=500, env='production'): def check_shadowfax_availability(pickup_pincode, drop_pincode, service='express', weight=500, env='production', token=None, use_mock=False):
""" """
Checks Shadowfax pincode serviceability and logistics rate estimation directly from live Shadowfax API. 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.
""" """
weight_slabs = max(1, int(weight) // 500) weight_slabs = max(1, int(weight) // 500)
estimated_rate = round(45.0 + (weight_slabs * 15.0), 2) 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) base_url = get_base_url(env)
token = get_default_token(env) auth_token = str(token).strip() if token else get_default_token(env)
headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"} headers = {"Authorization": f"Token {auth_token}", "Content-Type": "application/json"}
pincodes_str = f"{pickup_pincode},{drop_pincode}" pincodes_str = f"{pickup_pincode},{drop_pincode}"
sfx_service = "express" if service == "express" else "customer_delivery" sfx_service = "express" if service == "express" else "customer_delivery"
url = f"{base_url}/v1/clients/serviceability/?service={sfx_service}&page=1&count=100&pincodes={pincodes_str}" url = f"{base_url}/v1/clients/serviceability/?service={sfx_service}&page=1&count=100&pincodes={pincodes_str}"
@ -223,49 +254,149 @@ def check_shadowfax_availability(pickup_pincode, drop_pincode, service='express'
parsed = json.loads(res.get('response', '{}')) if isinstance(res.get('response'), str) else res.get('response', {}) parsed = json.loads(res.get('response', '{}')) if isinstance(res.get('response'), str) else res.get('response', {})
data = parsed.get('data', {}) data = parsed.get('data', {})
p_list = data.get('pincodes', []) if isinstance(data, dict) else [] p_list = data.get('pincodes', []) if isinstance(data, dict) else []
pickup_info = next((p for p in p_list if str(p.get('pincode')) == str(pickup_pincode)), None)
drop_info = next((p for p in p_list if str(p.get('pincode')) == str(drop_pincode)), None) drop_info = next((p for p in p_list if str(p.get('pincode')) == str(drop_pincode)), None)
if drop_info: if str(pickup_pincode) == str(drop_pincode) and p_list:
return { pickup_info = drop_info = p_list[0]
"carrier": "SHADOWFAX",
"serviceable": drop_info.get('serviceable', True), fm_ok = pickup_info.get('serviceable', True) if pickup_info else True
"cod_available": drop_info.get('cod_available', True), lm_ok = drop_info.get('serviceable', True) if drop_info else True
"estimated_cost": estimated_rate, cod_ok = drop_info.get('cod_available', True) if drop_info else True
"currency": "INR",
"details": drop_info 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)
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"Pickup pincode {pickup_pincode} not serviceable by Shadowfax"
}
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"Destination pincode {drop_pincode} not serviceable by Shadowfax"
}
return { return {
"carrier": "SHADOWFAX", "carrier": "SHADOWFAX",
"serviceable": True, "serviceable": overall_serviceable,
"cod_available": True, "cod_available": bool(cod_ok),
"estimated_cost": estimated_rate, "estimated_cost": estimated_rate if overall_serviceable else None,
"currency": "INR", "currency": "INR",
"details": parsed "first_mile": first_mile,
"last_mile": last_mile,
"details": drop_info or parsed
} }
else: else:
first_mile = {
"serviceable": False,
"pincode": str(pickup_pincode),
"city": p_city,
"state": p_state,
"hub_active": False,
"pickup_type": "Seller Store / Warehouse Sweep",
"message": f"SFX API returned status {res.get('status_code')}"
}
last_mile = {
"serviceable": False,
"pincode": str(drop_pincode),
"city": d_city,
"state": d_state,
"cod_available": False,
"delivery_type": "Doorstep Delivery (Express)",
"message": f"SFX API returned status {res.get('status_code')}"
}
return { return {
"carrier": "SHADOWFAX", "carrier": "SHADOWFAX",
"serviceable": False, "serviceable": False,
"cod_available": False, "cod_available": False,
"estimated_cost": None, "estimated_cost": None,
"first_mile": first_mile,
"last_mile": last_mile,
"error": f"SFX API returned status {res.get('status_code')}" "error": f"SFX API returned status {res.get('status_code')}"
} }
except Exception as e: 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 { return {
"carrier": "SHADOWFAX", "carrier": "SHADOWFAX",
"serviceable": False, "serviceable": False,
"cod_available": False, "cod_available": False,
"estimated_cost": None, "estimated_cost": None,
"first_mile": first_mile,
"last_mile": last_mile,
"error": str(e) "error": str(e)
} }
def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid', invoice_amount=1000): def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid', invoice_amount=1000, use_mock=False):
""" """
Checks Ekart pincode serviceability and logistics rate estimation directly from live Ekart API. Checks Ekart pincode serviceability and logistics rate estimation directly from live Ekart API.
Provides detailed first-mile and last-mile breakdown.
""" """
weight_slabs = max(1, int(weight) // 500) weight_slabs = max(1, int(weight) // 500)
estimated_rate = round(38.0 + (weight_slabs * 12.0), 2) 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() client = EkartAPIClient()
try: try:
@ -303,49 +434,133 @@ def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_t
except Exception: except Exception:
pass pass
first_mile = {
"serviceable": bool(serviceable),
"pincode": str(pickup_pincode),
"city": p_city,
"state": p_state,
"hub_active": bool(serviceable),
"pickup_type": "Ekart Hub Sweep",
"message": f"First-mile pickup serviceable in {p_city}, {p_state}" if serviceable else f"Pickup pincode {pickup_pincode} not serviceable"
}
last_mile = {
"serviceable": bool(serviceable),
"pincode": str(drop_pincode),
"city": d_city,
"state": d_state,
"cod_available": bool(cod_available),
"delivery_type": "Doorstep Surface/Air Delivery",
"message": f"Last-mile delivery serviceable in {d_city}, {d_state}" if serviceable else f"Destination pincode {drop_pincode} not serviceable"
}
return { return {
"carrier": "EKART", "carrier": "EKART",
"serviceable": bool(serviceable), "serviceable": bool(serviceable),
"cod_available": bool(cod_available), "cod_available": bool(cod_available),
"estimated_cost": live_cost, "estimated_cost": live_cost if serviceable else None,
"currency": "INR", "currency": "INR",
"first_mile": first_mile,
"last_mile": last_mile,
"details": res "details": res
} }
except Exception as e: 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 { return {
"carrier": "EKART", "carrier": "EKART",
"serviceable": False, "serviceable": False,
"cod_available": False, "cod_available": False,
"estimated_cost": None, "estimated_cost": None,
"first_mile": first_mile,
"last_mile": last_mile,
"error": str(e) "error": str(e)
} }
def check_delhivery_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid'): def check_delhivery_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid', api_token=None, use_mock=False):
""" """
Checks Delhivery pincode serviceability and logistics rate estimation directly Checks Delhivery pincode serviceability and logistics rate estimation directly
against official Delhivery serviceability matrix (18,699 pincodes) and live Delhivery API. against official Delhivery serviceability matrix (18,699 pincodes) and live Delhivery API.
Provides detailed first-mile (pickup hub) and last-mile (doorstep delivery) status.
""" """
weight_slabs = max(1, int(weight) // 500) weight_slabs = max(1, int(weight) // 500)
estimated_rate = round(35.0 + (weight_slabs * 11.0), 2) estimated_rate = round(35.0 + (weight_slabs * 11.0), 2)
is_cod = (str(payment_type).upper() == 'COD') is_cod = (str(payment_type).upper() == 'COD')
# Step 1: Validate against official Delhivery serviceability matrix # Step 1: Validate individual first-mile and last-mile against Delhivery 18,699 database
route_ok, route_msg, drop_info, pickup_info = check_delhivery_route_serviceability( p_ok, p_msg, pickup_info = is_delhivery_serviceable(pickup_pincode, is_cod=False)
pickup_pincode=pickup_pincode, d_ok, d_msg, drop_info = is_delhivery_serviceable(drop_pincode, is_cod=is_cod)
drop_pincode=drop_pincode,
is_cod=is_cod
)
if not route_ok: 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",
"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",
"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 { return {
"carrier": "DELHIVERY", "carrier": "DELHIVERY",
"serviceable": False, "serviceable": False,
"cod_available": False, "cod_available": cod_avail,
"estimated_cost": None, "estimated_cost": None,
"error": route_msg, "error": " | ".join(err_parts),
"city": d_city,
"state": d_state,
"state_code": d_st_code,
"first_mile": first_mile,
"last_mile": last_mile,
"details": { "details": {
"route_message": route_msg, "route_message": " | ".join(err_parts),
"drop_info": drop_info, "drop_info": drop_info,
"pickup_info": pickup_info "pickup_info": pickup_info
} }
@ -358,60 +573,68 @@ def check_delhivery_availability(pickup_pincode, drop_pincode, weight=500, payme
"pickup_info": pickup_info "pickup_info": pickup_info
} }
try: if not use_mock:
client = DelhiveryAPIClient() try:
rate_res = client.get_rate_estimate( client = DelhiveryAPIClient(api_token=api_token)
origin_pin=pickup_pincode, rate_res = client.get_rate_estimate(
dest_pin=drop_pincode, origin_pin=pickup_pincode,
weight=weight, dest_pin=drop_pincode,
mode="S", weight=weight,
payment_type=payment_type mode="S",
) payment_type=payment_type
if isinstance(rate_res, dict) and rate_res.get('total_amount'): )
live_cost = float(rate_res['total_amount']) if isinstance(rate_res, dict) and rate_res.get('total_amount'):
api_details['rate_estimate'] = rate_res live_cost = float(rate_res['total_amount'])
except Exception as e: api_details['rate_estimate'] = rate_res
api_details['rate_api_warning'] = str(e) except Exception as e:
api_details['rate_api_warning'] = str(e)
return { return {
"carrier": "DELHIVERY", "carrier": "DELHIVERY",
"serviceable": True, "serviceable": True,
"cod_available": bool(drop_info.get('cod', True)), "cod_available": cod_avail,
"estimated_cost": live_cost, "estimated_cost": live_cost,
"currency": "INR", "currency": "INR",
"city": drop_info.get('city'), "city": d_city,
"state": drop_info.get('state'), "state": d_state,
"state_code": drop_info.get('state_code'), "state_code": d_st_code,
"first_mile": first_mile,
"last_mile": last_mile,
"details": api_details "details": api_details
} }
def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type='Prepaid', weight=500, env='production'): def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type='Prepaid', weight=500, env='production', sfx_token=None, delhivery_token=None, use_mock=False):
""" """
Evaluates serviceability and rate estimates across all registered carriers (Shadowfax, Ekart, Delhivery) via live APIs. Evaluates serviceability, first-mile, and last-mile delivery across all registered carriers (Shadowfax, Delhivery, Ekart).
""" """
sfx_res = check_shadowfax_availability( sfx_res = check_shadowfax_availability(
pickup_pincode=pickup_pincode, pickup_pincode=pickup_pincode,
drop_pincode=drop_pincode, drop_pincode=drop_pincode,
weight=weight, weight=weight,
env=env env=env,
) token=sfx_token,
use_mock=use_mock
ekart_res = check_ekart_availability(
pickup_pincode=pickup_pincode,
drop_pincode=drop_pincode,
weight=weight,
payment_type=payment_type
) )
delhivery_res = check_delhivery_availability( delhivery_res = check_delhivery_availability(
pickup_pincode=pickup_pincode, pickup_pincode=pickup_pincode,
drop_pincode=drop_pincode, drop_pincode=drop_pincode,
weight=weight, weight=weight,
payment_type=payment_type payment_type=payment_type,
api_token=delhivery_token,
use_mock=use_mock
) )
return { 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", "status": "success",
"pickup_pincode": str(pickup_pincode), "pickup_pincode": str(pickup_pincode),
"drop_pincode": str(drop_pincode), "drop_pincode": str(drop_pincode),
@ -419,11 +642,21 @@ def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type='
"weight": weight, "weight": weight,
"carriers": { "carriers": {
"SHADOWFAX": sfx_res, "SHADOWFAX": sfx_res,
"EKART": ekart_res, "DELHIVERY": delhivery_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 = { CARRIER_PRIORITY = {
'SHADOWFAX': 0, 'SHADOWFAX': 0,

View file

@ -4,11 +4,10 @@ import requests
from django.conf import settings from django.conf import settings
from .models import ActivityLog from .models import ActivityLog
STAGING_BASE_URL = getattr(settings, 'SFX_STAGING_BASE_URL', 'https://dale.shadowfax.in/api')
PRODUCTION_BASE_URL = getattr(settings, 'SFX_PRODUCTION_BASE_URL', 'https://dale.shadowfax.in/api') PRODUCTION_BASE_URL = getattr(settings, 'SFX_PRODUCTION_BASE_URL', 'https://dale.shadowfax.in/api')
def get_base_url(env="production"): def get_base_url(env="production"):
return getattr(settings, 'SFX_PRODUCTION_BASE_URL', 'https://dale.shadowfax.in/api').rstrip('/') return PRODUCTION_BASE_URL.rstrip('/')
def get_default_token(env="production"): def get_default_token(env="production"):
@ -56,7 +55,7 @@ def execute_sfx_call(service_type, action_name, method, full_url, headers, paylo
response_body_str = json.dumps({ response_body_str = json.dumps({
"error": "Upstream Connection Failed", "error": "Upstream Connection Failed",
"details": str(e), "details": str(e),
"note": "Shadowfax Staging API host may be unreachable directly from your machine or blocked by Akamai WAF. Simulation mode fallback can be enabled." "note": "Shadowfax Production API host may be unreachable directly from your machine or blocked by Akamai WAF. Simulation mode fallback can be enabled."
}, indent=2) }, indent=2)
status_label = "ERROR" status_label = "ERROR"

View file

@ -19,6 +19,9 @@
Multi-Carrier Shipping Platform Multi-Carrier Shipping Platform
</div> </div>
<div class="nav-tabs"> <div class="nav-tabs">
<a href="{% url 'carrier_checker_view' %}" class="nav-link {% if request.resolver_match.url_name == 'carrier_checker_view' %}active{% endif %}">
⚖️ Route & Carrier Checker
</a>
<a href="{% url 'forward_view' %}" class="nav-link {% if request.resolver_match.url_name == 'forward_view' %}active{% endif %}"> <a href="{% url 'forward_view' %}" class="nav-link {% if request.resolver_match.url_name == 'forward_view' %}active{% endif %}">
📦 Shadowfax Forward 📦 Shadowfax Forward
</a> </a>

View file

@ -0,0 +1,675 @@
{% extends 'shipping/base.html' %}
{% block title %}Multi-Carrier Route Checker — Shadowfax vs Delhivery{% endblock %}
{% block extra_head %}
<style>
.carrier-card {
background: rgba(15, 23, 42, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.9rem;
padding: 1.4rem;
position: relative;
overflow: hidden;
transition: all 0.25s ease;
}
.carrier-card:hover {
border-color: rgba(56, 189, 248, 0.4);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.5);
}
.carrier-card.recommended {
border-color: rgba(16, 185, 129, 0.6);
background: linear-gradient(180deg, rgba(16, 185, 129, 0.08) 0%, rgba(15, 23, 42, 0.8) 100%);
}
.carrier-card.recommended::before {
content: "⭐ RECOMMENDED CARRIER";
position: absolute;
top: 0;
right: 0;
background: linear-gradient(135deg, #10b981, #059669);
color: #ffffff;
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.06em;
padding: 0.25rem 0.75rem;
border-bottom-left-radius: 0.6rem;
}
.mile-box {
background: rgba(10, 15, 29, 0.75);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 0.65rem;
padding: 1rem;
margin-top: 0.85rem;
}
.mile-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.85rem;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.preset-chip {
background: rgba(30, 41, 59, 0.7);
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--text-muted);
font-size: 0.8rem;
padding: 0.35rem 0.8rem;
border-radius: 2rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.preset-chip:hover {
color: #fff;
background: rgba(56, 189, 248, 0.2);
border-color: rgba(56, 189, 248, 0.5);
transform: translateY(-1px);
}
.verdict-banner {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.15), rgba(99, 102, 241, 0.15));
border: 1px solid rgba(6, 182, 212, 0.4);
border-radius: 1rem;
padding: 1.4rem 1.8rem;
margin-bottom: 1.75rem;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
}
.spinner {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 0.8s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.collapse-btn {
background: none;
border: none;
color: var(--accent-cyan);
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0;
}
.collapse-btn:hover {
text-decoration: underline;
}
</style>
{% endblock %}
{% block content %}
<div class="page-header">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<div>
<h1 class="page-title">⚖️ Multi-Carrier Route & Serviceability Checker</h1>
<p class="page-desc">Enter origin & destination details to instantly test both <strong>First-Mile (Pickup Hub)</strong> and <strong>Last-Mile (Doorstep Delivery & COD)</strong> serviceability with Shadowfax and Delhivery.</p>
</div>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<span class="badge badge-forward">Shadowfax SFX v1/v3</span>
<span class="badge" style="background: rgba(245, 158, 11, 0.18); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.35);">Delhivery 18.7K Database</span>
<span class="badge badge-webhook">Ekart Elite v3</span>
</div>
</div>
</div>
<!-- Main Route Input Card -->
<div class="card" style="margin-bottom: 1.75rem;">
<div class="card-title">
<span>📍 Route & Shipment Parameters</span>
<span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">Real-time Multi-Carrier API Evaluation</span>
</div>
<form id="checkerForm" onsubmit="handleCheckSubmit(event)">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.2rem; margin-bottom: 1.2rem;">
<!-- Pickup Pincode (First Mile) -->
<div class="form-group" style="margin: 0;">
<label class="form-label" style="display: flex; justify-content: space-between;">
<span>🛫 Origin Pincode (First Mile)</span>
<span id="pickupLocationHint" style="color: var(--accent-cyan); font-weight: 500; text-transform: none;"></span>
</label>
<input type="text" id="pickup_pincode" class="form-control" placeholder="e.g. 560077" value="560077" maxlength="6" oninput="updateLocationHints()">
</div>
<!-- Drop Pincode (Last Mile) -->
<div class="form-group" style="margin: 0;">
<label class="form-label" style="display: flex; justify-content: space-between;">
<span>🛬 Destination Pincode (Last Mile)</span>
<span id="dropLocationHint" style="color: var(--accent-cyan); font-weight: 500; text-transform: none;"></span>
</label>
<input type="text" id="drop_pincode" class="form-control" placeholder="e.g. 110009" value="110009" maxlength="6" oninput="updateLocationHints()">
</div>
<!-- Payment Type -->
<div class="form-group" style="margin: 0;">
<label class="form-label">💳 Payment Method</label>
<select id="payment_type" class="form-select">
<option value="Prepaid" selected>Prepaid (Standard)</option>
<option value="COD">COD (Cash on Delivery)</option>
</select>
</div>
<!-- Weight -->
<div class="form-group" style="margin: 0;">
<label class="form-label">⚖️ Parcel Weight (Grams)</label>
<input type="number" id="weight" class="form-control" value="500" min="50" max="30000" step="50">
</div>
</div>
<!-- Quick Preset Route Chips -->
<div style="margin-bottom: 1.2rem; display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;">
<span style="font-size: 0.8rem; color: var(--text-muted); font-weight: 600;">⚡ Quick Presets:</span>
<button type="button" class="preset-chip" onclick="applyPreset('560077', '110009', 'Prepaid')">
Bengaluru (560077) → Delhi (110009)
</button>
<button type="button" class="preset-chip" onclick="applyPreset('600001', '400001', 'COD')">
Chennai (600001) → Mumbai (400001) [COD]
</button>
<button type="button" class="preset-chip" onclick="applyPreset('600059', '110001', 'Prepaid')">
Tambaram (600059) → New Delhi (110001)
</button>
<button type="button" class="preset-chip" onclick="applyPreset('700001', '500001', 'Prepaid')">
Kolkata (700001) → Hyderabad (500001)
</button>
</div>
<!-- Collapsible Advanced API Drawer -->
<div style="border-top: 1px solid var(--border-color); padding-top: 1rem; margin-top: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<button type="button" class="collapse-btn" onclick="toggleApiSettings()">
<span id="apiToggleIcon">▶</span> Advanced API Credentials & Environment Settings
</button>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<label style="font-size: 0.8rem; color: var(--text-muted);">Mode:</label>
<select id="cfg_mode" class="form-select" style="width: auto; padding: 0.35rem 0.75rem; font-size: 0.82rem;">
<option value="false" selected>Live Upstream API Call</option>
<option value="true">Offline Sandbox Simulation</option>
</select>
</div>
</div>
<div id="apiSettingsDrawer" style="display: none; background: rgba(10, 15, 29, 0.5); padding: 1.1rem; border-radius: 0.75rem; border: 1px solid var(--border-color); margin-bottom: 1.2rem;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem;">
<div class="form-group" style="margin: 0;">
<label class="form-label">Shadowfax Auth API Token</label>
<input type="text" id="sfx_token" class="form-control" value="{{ default_sfx_token|default:'7328f980a6eca01fedf37a63e39e44893481f856' }}">
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">Delhivery Auth API Token</label>
<input type="text" id="delhivery_token" class="form-control" value="{{ default_delhivery_token|default:'e70dfd63665335ee6e0a1094f6d34e17a126f5a6' }}">
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">Active Environment</label>
<input type="text" class="form-control" value="Production (dale.shadowfax.in)" readonly style="opacity: 0.8; cursor: not-allowed;">
</div>
</div>
</div>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 1rem;">
<button type="submit" id="btnSubmitCheck" class="btn btn-primary" style="padding: 0.85rem 2rem; font-size: 1rem;">
<span id="btnIcon">⚡</span>
<span id="btnText">Check First-Mile & Last-Mile Delivery</span>
</button>
</div>
</form>
</div>
<!-- Results Section (Populated dynamically) -->
<div id="resultsWrapper" style="display: none;">
<!-- Verdict Banner -->
<div class="verdict-banner" id="verdictBanner">
<div>
<div style="font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent-cyan); font-weight: 700; margin-bottom: 0.25rem;">
Route Evaluation Verdict
</div>
<div id="verdictTitle" style="font-size: 1.35rem; font-weight: 800; color: #fff;">
Checking carriers...
</div>
<div id="verdictDesc" style="color: var(--text-muted); font-size: 0.95rem; margin-top: 0.25rem;">
Multi-carrier serviceability determined.
</div>
</div>
<div id="verdictBadgeContainer"></div>
</div>
<!-- Side-by-Side Carrier Cards Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
<!-- Carrier 1: Shadowfax Card -->
<div class="carrier-card" id="card_SHADOWFAX">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); padding-bottom: 0.85rem; margin-bottom: 1rem;">
<div>
<div style="font-size: 1.2rem; font-weight: 800; color: #fff; display: flex; align-items: center; gap: 0.5rem;">
<span>📦</span> Shadowfax Logistics
</div>
<span style="font-size: 0.78rem; color: var(--text-muted);">API Integrated / Express & Hyperlocal</span>
</div>
<div id="badge_SHADOWFAX"></div>
</div>
<!-- Shadowfax First Mile Box -->
<div class="mile-box">
<div class="mile-header">
<span style="color: var(--accent-cyan);">🛫 First-Mile (Pickup Hub)</span>
<span id="fm_status_SHADOWFAX"></span>
</div>
<div style="font-size: 0.9rem; margin-bottom: 0.35rem;">
<strong>Origin Pincode:</strong> <span id="fm_pin_SHADOWFAX">-</span>
</div>
<div style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 0.35rem;">
<strong>Hub Location:</strong> <span id="fm_loc_SHADOWFAX">-</span>
</div>
<div style="font-size: 0.82rem; color: #94a3b8;" id="fm_msg_SHADOWFAX">-</div>
</div>
<!-- Shadowfax Last Mile Box -->
<div class="mile-box">
<div class="mile-header">
<span style="color: var(--accent-purple);">🛬 Last-Mile (Delivery)</span>
<span id="lm_status_SHADOWFAX"></span>
</div>
<div style="font-size: 0.9rem; margin-bottom: 0.35rem;">
<strong>Destination Pincode:</strong> <span id="lm_pin_SHADOWFAX">-</span>
</div>
<div style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 0.35rem;">
<strong>Delivery Area:</strong> <span id="lm_loc_SHADOWFAX">-</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid rgba(255, 255, 255, 0.05);">
<span id="cod_SHADOWFAX">-</span>
<span id="cost_SHADOWFAX" style="font-size: 1.15rem; font-weight: 800; color: var(--accent-emerald);">-</span>
</div>
</div>
</div>
<!-- Carrier 2: Delhivery Card -->
<div class="carrier-card" id="card_DELHIVERY">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); padding-bottom: 0.85rem; margin-bottom: 1rem;">
<div>
<div style="font-size: 1.2rem; font-weight: 800; color: #fff; display: flex; align-items: center; gap: 0.5rem;">
<span>🚚</span> Delhivery Logistics
</div>
<span style="font-size: 0.78rem; color: var(--text-muted);">18,699 Pincodes / Surface & Express</span>
</div>
<div id="badge_DELHIVERY"></div>
</div>
<!-- Delhivery First Mile Box -->
<div class="mile-box">
<div class="mile-header">
<span style="color: var(--accent-cyan);">🛫 First-Mile (Pickup Hub)</span>
<span id="fm_status_DELHIVERY"></span>
</div>
<div style="font-size: 0.9rem; margin-bottom: 0.35rem;">
<strong>Origin Pincode:</strong> <span id="fm_pin_DELHIVERY">-</span>
</div>
<div style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 0.35rem;">
<strong>Hub Location:</strong> <span id="fm_loc_DELHIVERY">-</span>
</div>
<div style="font-size: 0.82rem; color: #94a3b8;" id="fm_msg_DELHIVERY">-</div>
</div>
<!-- Delhivery Last Mile Box -->
<div class="mile-box">
<div class="mile-header">
<span style="color: var(--accent-purple);">🛬 Last-Mile (Delivery)</span>
<span id="lm_status_DELHIVERY"></span>
</div>
<div style="font-size: 0.9rem; margin-bottom: 0.35rem;">
<strong>Destination Pincode:</strong> <span id="lm_pin_DELHIVERY">-</span>
</div>
<div style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 0.35rem;">
<strong>Delivery Area:</strong> <span id="lm_loc_DELHIVERY">-</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid rgba(255, 255, 255, 0.05);">
<span id="cod_DELHIVERY">-</span>
<span id="cost_DELHIVERY" style="font-size: 1.15rem; font-weight: 800; color: var(--accent-emerald);">-</span>
</div>
</div>
</div>
<!-- Carrier 3: Ekart Card (Bonus carrier in fleet) -->
<div class="carrier-card" id="card_EKART">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); padding-bottom: 0.85rem; margin-bottom: 1rem;">
<div>
<div style="font-size: 1.2rem; font-weight: 800; color: #fff; display: flex; align-items: center; gap: 0.5rem;">
<span>⚡</span> Ekart Logistics
</div>
<span style="font-size: 0.78rem; color: var(--text-muted);">Elite API v3.8.10 / Pan-India Network</span>
</div>
<div id="badge_EKART"></div>
</div>
<!-- Ekart First Mile Box -->
<div class="mile-box">
<div class="mile-header">
<span style="color: var(--accent-cyan);">🛫 First-Mile (Pickup Hub)</span>
<span id="fm_status_EKART"></span>
</div>
<div style="font-size: 0.9rem; margin-bottom: 0.35rem;">
<strong>Origin Pincode:</strong> <span id="fm_pin_EKART">-</span>
</div>
<div style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 0.35rem;">
<strong>Hub Location:</strong> <span id="fm_loc_EKART">-</span>
</div>
<div style="font-size: 0.82rem; color: #94a3b8;" id="fm_msg_EKART">-</div>
</div>
<!-- Ekart Last Mile Box -->
<div class="mile-box">
<div class="mile-header">
<span style="color: var(--accent-purple);">🛬 Last-Mile (Delivery)</span>
<span id="lm_status_EKART"></span>
</div>
<div style="font-size: 0.9rem; margin-bottom: 0.35rem;">
<strong>Destination Pincode:</strong> <span id="lm_pin_EKART">-</span>
</div>
<div style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 0.35rem;">
<strong>Delivery Area:</strong> <span id="lm_loc_EKART">-</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid rgba(255, 255, 255, 0.05);">
<span id="cod_EKART">-</span>
<span id="cost_EKART" style="font-size: 1.15rem; font-weight: 800; color: var(--accent-emerald);">-</span>
</div>
</div>
</div>
</div>
<!-- Detailed Comparison Table -->
<div class="card" style="margin-bottom: 1.75rem;">
<div class="card-title">
<span>📊 Side-by-Side Capability Comparison Table</span>
</div>
<div style="overflow-x: auto;">
<table class="data-table">
<thead>
<tr>
<th>Carrier Partner</th>
<th>First-Mile (Origin Pickup)</th>
<th>Last-Mile (Destination Drop)</th>
<th>COD Support</th>
<th>Estimated Freight</th>
<th>Route Status</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</div>
<!-- Collapsible Raw API JSON Viewer -->
<div class="card">
<div class="card-title">
<span>🔍 Raw Multi-Carrier JSON Response (`/api/v1/carrier/check-availability/`)</span>
<button type="button" class="btn btn-secondary" style="padding: 0.35rem 0.85rem; font-size: 0.8rem;" onclick="copyJson()">
📋 Copy JSON
</button>
</div>
<pre class="json-viewer" id="rawJsonResponse">// Response will appear here</pre>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
// Pincode prefix city guide for instant input hints
const PIN_HINTS = {
'600': 'Chennai, TN', '601': 'Chennai, TN', '602': 'Thiruvallur, TN', '603': 'Chengalpattu, TN',
'641': 'Coimbatore, TN', '625': 'Madurai, TN', '620': 'Tiruchirappalli, TN', '636': 'Salem, TN',
'560': 'Bengaluru, KA', '561': 'Bengaluru Rural, KA', '562': 'Bengaluru Rural, KA', '570': 'Mysuru, KA',
'110': 'Delhi, DL', '400': 'Mumbai, MH', '411': 'Pune, MH', '500': 'Hyderabad, TS', '700': 'Kolkata, WB',
'380': 'Ahmedabad, GJ', '302': 'Jaipur, RJ', '226': 'Lucknow, UP', '682': 'Kochi, KL'
};
function updateLocationHints() {
const pPin = document.getElementById('pickup_pincode').value.trim();
const dPin = document.getElementById('drop_pincode').value.trim();
const pHintEl = document.getElementById('pickupLocationHint');
const dHintEl = document.getElementById('dropLocationHint');
if (pPin.length >= 3 && PIN_HINTS[pPin.substring(0, 3)]) {
pHintEl.textContent = PIN_HINTS[pPin.substring(0, 3)];
} else {
pHintEl.textContent = '';
}
if (dPin.length >= 3 && PIN_HINTS[dPin.substring(0, 3)]) {
dHintEl.textContent = PIN_HINTS[dPin.substring(0, 3)];
} else {
dHintEl.textContent = '';
}
}
function toggleApiSettings() {
const drawer = document.getElementById('apiSettingsDrawer');
const icon = document.getElementById('apiToggleIcon');
if (drawer.style.display === 'none' || drawer.style.display === '') {
drawer.style.display = 'block';
icon.textContent = '▼';
} else {
drawer.style.display = 'none';
icon.textContent = '▶';
}
}
function applyPreset(pickup, drop, payment) {
document.getElementById('pickup_pincode').value = pickup;
document.getElementById('drop_pincode').value = drop;
document.getElementById('payment_type').value = payment;
updateLocationHints();
document.getElementById('checkerForm').dispatchEvent(new Event('submit', { cancelable: true }));
}
let latestResponseData = null;
async function handleCheckSubmit(e) {
if (e) e.preventDefault();
const pickupPin = document.getElementById('pickup_pincode').value.trim();
const dropPin = document.getElementById('drop_pincode').value.trim();
const paymentType = document.getElementById('payment_type').value;
const weight = parseInt(document.getElementById('weight').value) || 500;
const isMock = document.getElementById('cfg_mode').value === 'true';
const sfxToken = document.getElementById('sfx_token').value.trim();
const delhiveryToken = document.getElementById('delhivery_token').value.trim();
const sfxEnv = 'production';
if (!pickupPin || pickupPin.length !== 6) {
alert('Please enter a valid 6-digit origin pickup pincode.');
return;
}
if (!dropPin || dropPin.length !== 6) {
alert('Please enter a valid 6-digit destination drop pincode.');
return;
}
const btn = document.getElementById('btnSubmitCheck');
const btnIcon = document.getElementById('btnIcon');
const btnText = document.getElementById('btnText');
btn.disabled = true;
btnIcon.innerHTML = '<span class="spinner"></span>';
btnText.textContent = 'Evaluating Carriers...';
const payload = {
pickup_pincode: pickupPin,
drop_pincode: dropPin,
payment_type: paymentType,
weight: weight,
env: sfxEnv,
sfx_token: sfxToken,
delhivery_token: delhiveryToken,
use_mock: isMock
};
try {
const response = await fetch('/api/v1/carrier/check-availability/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
latestResponseData = data;
renderResults(data);
} catch (err) {
alert('Failed to connect to carrier availability API: ' + err.message);
} finally {
btn.disabled = false;
btnIcon.textContent = '⚡';
btnText.textContent = 'Check First-Mile & Last-Mile Delivery';
}
}
function renderResults(data) {
document.getElementById('resultsWrapper').style.display = 'block';
document.getElementById('rawJsonResponse').textContent = JSON.stringify(data, null, 2);
const carriers = data.carriers || {};
const best = data.best_carrier || {};
// 1. Verdict Banner
const verdictTitle = document.getElementById('verdictTitle');
const verdictDesc = document.getElementById('verdictDesc');
const verdictBadgeContainer = document.getElementById('verdictBadgeContainer');
if (best && best.selected) {
verdictTitle.innerHTML = `Optimal Carrier: <span style="color: var(--accent-emerald);">${best.carrier}</span> (₹${best.cost})`;
verdictDesc.textContent = best.reason || 'Selected as best serviceable carrier for this route.';
verdictBadgeContainer.innerHTML = `<span class="badge badge-success" style="font-size: 0.9rem; padding: 0.5rem 1rem;">Route Serviceable</span>`;
} else {
verdictTitle.innerHTML = `<span style="color: var(--accent-rose);">Route Unserviceable</span>`;
verdictDesc.textContent = best.reason || 'No integrated carrier can service both the origin pickup and destination drop.';
verdictBadgeContainer.innerHTML = `<span class="badge badge-error" style="font-size: 0.9rem; padding: 0.5rem 1rem;">Action Required</span>`;
}
// 2. Render each carrier card
const carrierKeys = ['SHADOWFAX', 'DELHIVERY', 'EKART'];
const tableBody = document.getElementById('tableBody');
tableBody.innerHTML = '';
carrierKeys.forEach(key => {
const c = carriers[key] || {};
const cardEl = document.getElementById(`card_${key}`);
if (!cardEl) return;
const isBest = (best && best.selected && best.carrier === key);
if (isBest) {
cardEl.classList.add('recommended');
} else {
cardEl.classList.remove('recommended');
}
const overallServiceable = Boolean(c.serviceable);
const badgeEl = document.getElementById(`badge_${key}`);
badgeEl.innerHTML = overallServiceable
? `<span class="badge badge-success">✓ Serviceable</span>`
: `<span class="badge badge-error">✕ Unserviceable</span>`;
// First Mile
const fm = c.first_mile || {};
const fmStatusEl = document.getElementById(`fm_status_${key}`);
const fmPinEl = document.getElementById(`fm_pin_${key}`);
const fmLocEl = document.getElementById(`fm_loc_${key}`);
const fmMsgEl = document.getElementById(`fm_msg_${key}`);
fmStatusEl.innerHTML = fm.serviceable
? `<span style="color: var(--accent-emerald);">● Pickup Active</span>`
: `<span style="color: var(--accent-rose);">● Pickup Blocked</span>`;
fmPinEl.textContent = fm.pincode || data.pickup_pincode || '-';
fmLocEl.textContent = `${fm.city || 'Unknown'}, ${fm.state || 'India'}`;
fmMsgEl.textContent = fm.message || (fm.serviceable ? 'First-mile pickup serviceable' : 'Origin not serviceable');
// Last Mile
const lm = c.last_mile || {};
const lmStatusEl = document.getElementById(`lm_status_${key}`);
const lmPinEl = document.getElementById(`lm_pin_${key}`);
const lmLocEl = document.getElementById(`lm_loc_${key}`);
const codEl = document.getElementById(`cod_${key}`);
const costEl = document.getElementById(`cost_${key}`);
lmStatusEl.innerHTML = lm.serviceable
? `<span style="color: var(--accent-emerald);">● Delivery Active</span>`
: `<span style="color: var(--accent-rose);">● Delivery Blocked</span>`;
lmPinEl.textContent = lm.pincode || data.drop_pincode || '-';
lmLocEl.textContent = `${lm.city || 'Unknown'}, ${lm.state || 'India'}`;
if (c.cod_available) {
codEl.innerHTML = `<span class="badge badge-success" style="font-size: 0.72rem;">COD Available</span>`;
} else {
codEl.innerHTML = `<span class="badge badge-error" style="font-size: 0.72rem;">Prepaid Only</span>`;
}
if (overallServiceable && c.estimated_cost !== null && c.estimated_cost !== undefined) {
costEl.textContent = `₹${c.estimated_cost}`;
} else {
costEl.textContent = 'N/A';
costEl.style.color = 'var(--text-muted)';
}
// Add row to table
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="font-weight: 700; color: #fff;">
${key === 'SHADOWFAX' ? '📦 Shadowfax' : (key === 'DELHIVERY' ? '🚚 Delhivery' : '⚡ Ekart')}
${isBest ? ' <span class="badge badge-success" style="font-size: 0.65rem;">Best Choice</span>' : ''}
</td>
<td>
${fm.serviceable ? '<span style="color: #34d399; font-weight: 600;">✓ Ready</span>' : '<span style="color: #fb7185; font-weight: 600;">✕ Blocked</span>'}
<span style="color: var(--text-muted); font-size: 0.8rem; display: block;">${fm.city || '-'}, ${fm.state || '-'} (${fm.pincode || '-'})</span>
</td>
<td>
${lm.serviceable ? '<span style="color: #34d399; font-weight: 600;">✓ Ready</span>' : '<span style="color: #fb7185; font-weight: 600;">✕ Blocked</span>'}
<span style="color: var(--text-muted); font-size: 0.8rem; display: block;">${lm.city || '-'}, ${lm.state || '-'} (${lm.pincode || '-'})</span>
</td>
<td>
${c.cod_available ? '<span style="color: #34d399;">Active</span>' : '<span style="color: #fb7185;">Disabled</span>'}
</td>
<td style="font-weight: 700; color: ${overallServiceable ? '#34d399' : 'var(--text-muted)'}; font-size: 1rem;">
${overallServiceable && c.estimated_cost ? `₹${c.estimated_cost}` : '—'}
</td>
<td>
${overallServiceable
? '<span class="badge badge-success">Serviceable</span>'
: '<span class="badge badge-error">Unserviceable</span>'}
</td>
`;
tableBody.appendChild(tr);
});
// Smooth scroll to verdict
document.getElementById('verdictBanner').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function copyJson() {
if (!latestResponseData) return;
navigator.clipboard.writeText(JSON.stringify(latestResponseData, null, 2))
.then(() => alert('JSON response copied to clipboard!'))
.catch(() => alert('Failed to copy.'));
}
// Initialize input hints on page load
window.addEventListener('DOMContentLoaded', () => {
updateLocationHints();
// Auto-trigger on initial load to show preview
handleCheckSubmit(null);
});
</script>
{% endblock %}

View file

@ -17,10 +17,10 @@
<div style="display: grid; grid-template-columns: 1fr 2fr 1fr auto; gap: 1rem; align-items: end;"> <div style="display: grid; grid-template-columns: 1fr 2fr 1fr auto; gap: 1rem; align-items: end;">
<div class="form-group" style="margin: 0;"> <div class="form-group" style="margin: 0;">
<label class="form-label">Target Environment</label> <label class="form-label">Target Environment</label>
<select id="cfg_env" class="form-select"> <input type="hidden" id="cfg_env" value="production">
<option value="production" selected>Production (dale.shadowfax.in)</option> <div style="padding: 0.7rem 0.95rem; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: 0.6rem; color: #38bdf8; font-weight: 600; font-size: 0.92rem;">
<option value="staging">Staging (dale.staging.shadowfax.in)</option> Production (dale.shadowfax.in)
</select> </div>
</div> </div>
<div class="form-group" style="margin: 0;"> <div class="form-group" style="margin: 0;">
<label class="form-label">Shadowfax Authorization API Token</label> <label class="form-label">Shadowfax Authorization API Token</label>
@ -295,18 +295,7 @@
{% block extra_scripts %} {% block extra_scripts %}
<script> <script>
document.getElementById('cfg_env').addEventListener('change', function() {
const tokenInput = document.getElementById('cfg_token');
if (this.value === 'production') {
if (tokenInput.value === 'sample_staging_token_12345' || !tokenInput.value) {
tokenInput.value = '7328f980a6eca01fedf37a63e39e44893481f856';
}
} else {
if (tokenInput.value === '7328f980a6eca01fedf37a63e39e44893481f856' || !tokenInput.value) {
tokenInput.value = 'sample_staging_token_12345';
}
}
});
function executeAction(e, actionType) { function executeAction(e, actionType) {
e.preventDefault(); e.preventDefault();

View file

@ -17,10 +17,10 @@
<div style="display: grid; grid-template-columns: 1fr 2fr 1fr auto; gap: 1rem; align-items: end;"> <div style="display: grid; grid-template-columns: 1fr 2fr 1fr auto; gap: 1rem; align-items: end;">
<div class="form-group" style="margin: 0;"> <div class="form-group" style="margin: 0;">
<label class="form-label">Target Environment</label> <label class="form-label">Target Environment</label>
<select id="cfg_env" class="form-select"> <input type="hidden" id="cfg_env" value="production">
<option value="production" selected>Production (dale.shadowfax.in)</option> <div style="padding: 0.7rem 0.95rem; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: 0.6rem; color: #38bdf8; font-weight: 600; font-size: 0.92rem;">
<option value="staging">Staging (dale.staging.shadowfax.in)</option> Production (dale.shadowfax.in)
</select> </div>
</div> </div>
<div class="form-group" style="margin: 0;"> <div class="form-group" style="margin: 0;">
<label class="form-label">Shadowfax Authorization API Token</label> <label class="form-label">Shadowfax Authorization API Token</label>
@ -208,18 +208,7 @@
{% block extra_scripts %} {% block extra_scripts %}
<script> <script>
document.getElementById('cfg_env').addEventListener('change', function() {
const tokenInput = document.getElementById('cfg_token');
if (this.value === 'production') {
if (tokenInput.value === 'sample_staging_token_12345' || !tokenInput.value) {
tokenInput.value = '7328f980a6eca01fedf37a63e39e44893481f856';
}
} else {
if (tokenInput.value === '7328f980a6eca01fedf37a63e39e44893481f856' || !tokenInput.value) {
tokenInput.value = 'sample_staging_token_12345';
}
}
});
function executeAction(e, actionType) { function executeAction(e, actionType) {
e.preventDefault(); e.preventDefault();

View file

@ -241,6 +241,42 @@ class ShippingAPITestCase(TestCase):
res_invalid = check_delhivery_availability('560077', '999999') res_invalid = check_delhivery_availability('560077', '999999')
self.assertFalse(res_invalid['serviceable']) self.assertFalse(res_invalid['serviceable'])
def test_carrier_checker_ui_and_first_last_mile(self):
# Test UI template rendering
response = self.client.get(reverse('carrier_checker_view'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Multi-Carrier Route & Serviceability Checker")
self.assertContains(response, "First-Mile (Pickup Hub)")
self.assertContains(response, "Last-Mile (Delivery)")
# Test API response with first-mile and last-mile breakdown
api_url = reverse('api_carrier_availability')
payload = {
"pickup_pincode": "560077",
"drop_pincode": "600001",
"payment_type": "COD",
"use_mock": True
}
api_res = self.client.post(api_url, data=json.dumps(payload), content_type='application/json')
self.assertEqual(api_res.status_code, 200)
data = api_res.json()
self.assertEqual(data['status'], 'success')
self.assertIn('SHADOWFAX', data['carriers'])
self.assertIn('DELHIVERY', data['carriers'])
sfx = data['carriers']['SHADOWFAX']
self.assertIn('first_mile', sfx)
self.assertIn('last_mile', sfx)
self.assertTrue(sfx['first_mile']['serviceable'])
self.assertTrue(sfx['last_mile']['serviceable'])
delhivery = data['carriers']['DELHIVERY']
self.assertIn('first_mile', delhivery)
self.assertIn('last_mile', delhivery)
self.assertTrue(delhivery['first_mile']['serviceable'])
self.assertTrue(delhivery['last_mile']['serviceable'])

View file

@ -4,6 +4,7 @@ from . import views
urlpatterns = [ urlpatterns = [
# Dashboard Web UI Routes # Dashboard Web UI Routes
path('', views.index_redirect, name='index_redirect'), path('', views.index_redirect, name='index_redirect'),
path('checker/', views.carrier_checker_view, name='carrier_checker_view'),
path('forward/', views.forward_view, name='forward_view'), path('forward/', views.forward_view, name='forward_view'),
path('reverse/', views.reverse_view, name='reverse_view'), path('reverse/', views.reverse_view, name='reverse_view'),
path('label/', views.print_label_view, name='print_label_view'), path('label/', views.print_label_view, name='print_label_view'),

View file

@ -47,13 +47,11 @@ def index_redirect(request):
def forward_view(request): def forward_view(request):
if request.method == 'POST': if request.method == 'POST':
action = request.POST.get('action_type') action = request.POST.get('action_type')
env = request.POST.get('environment', 'production') env = 'production'
token = request.POST.get('api_token', '').strip() token = request.POST.get('api_token', '').strip()
if not token or (token == 'sample_staging_token_12345' and env == 'production'): if not token:
token = get_default_token(env) token = get_default_token()
elif not token: base_url = get_base_url()
token = get_default_token(env)
base_url = get_base_url(env)
headers = { headers = {
'Authorization': f'Token {token}', 'Authorization': f'Token {token}',
@ -237,13 +235,11 @@ def forward_view(request):
def reverse_view(request): def reverse_view(request):
if request.method == 'POST': if request.method == 'POST':
action = request.POST.get('action_type') action = request.POST.get('action_type')
env = request.POST.get('environment', 'production') env = 'production'
token = request.POST.get('api_token', '').strip() token = request.POST.get('api_token', '').strip()
if not token or (token == 'sample_staging_token_12345' and env == 'production'): if not token:
token = get_default_token(env) token = get_default_token()
elif not token: base_url = get_base_url()
token = get_default_token(env)
base_url = get_base_url(env)
headers = { headers = {
'Authorization': f'Token {token}', 'Authorization': f'Token {token}',
@ -660,11 +656,9 @@ def api_forward_router(request, action_name):
else: else:
is_mock = data.get('use_mock', False) is_mock = data.get('use_mock', False)
token = (data.get('token') or request.headers.get('Authorization', '').replace('Token ', '')).strip() token = (data.get('token') or request.headers.get('Authorization', '').replace('Token ', '')).strip()
if not token or (token == 'sample_staging_token_12345' and env == 'production'): if not token:
token = get_default_token(env) token = get_default_token()
elif not token: base_url = get_base_url()
token = get_default_token(env)
base_url = get_base_url(env)
headers = { headers = {
'Authorization': f'Token {token}', 'Authorization': f'Token {token}',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -831,11 +825,9 @@ def api_reverse_router(request, action_name):
else: else:
is_mock = data.get('use_mock', False) is_mock = data.get('use_mock', False)
token = (data.get('token') or request.headers.get('Authorization', '').replace('Token ', '')).strip() token = (data.get('token') or request.headers.get('Authorization', '').replace('Token ', '')).strip()
if not token or (token == 'sample_staging_token_12345' and env == 'production'): if not token:
token = get_default_token(env) token = get_default_token()
elif not token: base_url = get_base_url()
token = get_default_token(env)
base_url = get_base_url(env)
headers = { headers = {
'Authorization': f'Token {token}', 'Authorization': f'Token {token}',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -1018,11 +1010,30 @@ def api_ekart_router(request, action_name):
return JsonResponse({'status': False, 'error': str(e)}, status=500) return JsonResponse({'status': False, 'error': str(e)}, status=500)
def carrier_checker_view(request):
"""
Interactive web UI to test and compare first-mile and last-mile serviceability
across Shadowfax, Delhivery, and Ekart.
"""
default_sfx_token = get_default_token('production')
try:
from delhivery.client import DEFAULT_DELHIVERY_API_TOKEN
default_delhivery_token = DEFAULT_DELHIVERY_API_TOKEN
except Exception:
default_delhivery_token = 'e70dfd63665335ee6e0a1094f6d34e17a126f5a6'
context = {
'default_sfx_token': default_sfx_token,
'default_delhivery_token': default_delhivery_token,
}
return render(request, 'shipping/carrier_checker.html', context)
@csrf_exempt @csrf_exempt
def api_carrier_availability(request): def api_carrier_availability(request):
""" """
Unified REST API endpoint to check serviceability across all registered carriers (Shadowfax, Ekart, Delhivery). Unified REST API endpoint to check serviceability across all registered carriers (Shadowfax, Ekart, Delhivery).
POST /api/v1/carrier/check-availability/ POST or GET /api/v1/carrier/check-availability/
""" """
if request.method not in ['POST', 'GET']: if request.method not in ['POST', 'GET']:
return JsonResponse({'error': 'POST or GET method required'}, status=405) return JsonResponse({'error': 'POST or GET method required'}, status=405)
@ -1037,6 +1048,9 @@ def api_carrier_availability(request):
payment_type = data.get('payment_type') or request.GET.get('payment_type') or 'Prepaid' payment_type = data.get('payment_type') or request.GET.get('payment_type') or 'Prepaid'
weight = data.get('weight') or request.GET.get('weight') or 500 weight = data.get('weight') or request.GET.get('weight') or 500
env = data.get('env') or request.GET.get('env') or 'production' env = data.get('env') or request.GET.get('env') or 'production'
sfx_token = data.get('sfx_token') or request.GET.get('sfx_token')
delhivery_token = data.get('delhivery_token') or request.GET.get('delhivery_token')
use_mock = data.get('use_mock') is True or str(data.get('use_mock', '')).lower() == 'true' or request.GET.get('use_mock') == 'true'
from .carrier_allocator import check_all_carriers_availability from .carrier_allocator import check_all_carriers_availability
res = check_all_carriers_availability( res = check_all_carriers_availability(
@ -1044,7 +1058,10 @@ def api_carrier_availability(request):
drop_pincode=drop_pincode, drop_pincode=drop_pincode,
payment_type=payment_type, payment_type=payment_type,
weight=weight, weight=weight,
env=env env=env,
sfx_token=sfx_token,
delhivery_token=delhivery_token,
use_mock=use_mock
) )
return JsonResponse(res, status=200) return JsonResponse(res, status=200)

View file

@ -161,9 +161,7 @@ MAILERS = {
} }
# Shadowfax API Credentials & Endpoints # Shadowfax API Credentials & Endpoints
SFX_STAGING_TOKEN = os.getenv('SFX_STAGING_TOKEN', 'sample_staging_token_12345')
SFX_PRODUCTION_TOKEN = os.getenv('SFX_PRODUCTION_TOKEN', '7328f980a6eca01fedf37a63e39e44893481f856') SFX_PRODUCTION_TOKEN = os.getenv('SFX_PRODUCTION_TOKEN', '7328f980a6eca01fedf37a63e39e44893481f856')
SFX_STAGING_BASE_URL = os.getenv('SFX_STAGING_BASE_URL', 'https://dale.shadowfax.in/api')
SFX_PRODUCTION_BASE_URL = os.getenv('SFX_PRODUCTION_BASE_URL', 'https://dale.shadowfax.in/api') SFX_PRODUCTION_BASE_URL = os.getenv('SFX_PRODUCTION_BASE_URL', 'https://dale.shadowfax.in/api')
# Ekart API Credentials & Endpoints # Ekart API Credentials & Endpoints