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.
- **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.
- **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.1 Check All Carrier Availability
Evaluate pincode serviceability and COD availability across all integrated carriers (Shadowfax & Ekart) in parallel.
#### 0.1 Check All Carrier Availability (First-Mile & Last-Mile)
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/`
- **Request Body**:
```json
@ -66,7 +67,7 @@ Evaluate pincode serviceability and COD availability across all integrated carri
"drop_pincode": "110009",
"payment_type": "Prepaid",
"weight": 500,
"use_mock": true
"use_mock": false
}
```
- **Response (`200 OK`)**:
@ -76,9 +77,57 @@ Evaluate pincode serviceability and COD availability across all integrated carri
"pickup_pincode": "560077",
"drop_pincode": "110009",
"payment_type": "Prepaid",
"best_carrier": {
"selected": true,
"carrier": "DELHIVERY",
"cost": 35.0,
"reason": "Selected DELHIVERY as lowest cost carrier (Cost: ₹35.0)"
},
"carriers": {
"SHADOWFAX": { "carrier": "SHADOWFAX", "serviceable": true, "cod_available": true },
"EKART": { "carrier": "EKART", "serviceable": true, "cod_available": true }
"SHADOWFAX": {
"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",
"service": "express",
"env": "staging",
"env": "production",
"use_mock": true
}
```
@ -182,7 +231,7 @@ Create a new forward delivery order (Marketplace Seller Pickup or Warehouse Hub
- **Request Body Example**:
```json
{
"env": "staging",
"env": "production",
"use_mock": true,
"payload": {
"order_details": {

View file

@ -19,7 +19,7 @@
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">
<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 style="display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 1rem; align-items: end;">
<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')
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.
Provides detailed first-mile (pickup hub) and last-mile (doorstep delivery) status.
"""
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)
token = get_default_token(env)
headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"}
auth_token = str(token).strip() if token else get_default_token(env)
headers = {"Authorization": f"Token {auth_token}", "Content-Type": "application/json"}
pincodes_str = f"{pickup_pincode},{drop_pincode}"
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}"
@ -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', {})
data = parsed.get('data', {})
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)
if drop_info:
return {
"carrier": "SHADOWFAX",
"serviceable": drop_info.get('serviceable', True),
"cod_available": drop_info.get('cod_available', True),
"estimated_cost": estimated_rate,
"currency": "INR",
"details": drop_info
}
if str(pickup_pincode) == str(drop_pincode) and p_list:
pickup_info = drop_info = p_list[0]
fm_ok = pickup_info.get('serviceable', True) if pickup_info else True
lm_ok = drop_info.get('serviceable', True) if drop_info else True
cod_ok = 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)
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 {
"carrier": "SHADOWFAX",
"serviceable": True,
"cod_available": True,
"estimated_cost": estimated_rate,
"serviceable": overall_serviceable,
"cod_available": bool(cod_ok),
"estimated_cost": estimated_rate if overall_serviceable else None,
"currency": "INR",
"details": parsed
"first_mile": first_mile,
"last_mile": last_mile,
"details": drop_info or parsed
}
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 {
"carrier": "SHADOWFAX",
"serviceable": False,
"cod_available": False,
"estimated_cost": None,
"first_mile": first_mile,
"last_mile": last_mile,
"error": f"SFX API returned status {res.get('status_code')}"
}
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=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.
Provides detailed first-mile and last-mile breakdown.
"""
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:
@ -303,49 +434,133 @@ def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_t
except Exception:
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 {
"carrier": "EKART",
"serviceable": bool(serviceable),
"cod_available": bool(cod_available),
"estimated_cost": live_cost,
"estimated_cost": live_cost if serviceable else None,
"currency": "INR",
"first_mile": first_mile,
"last_mile": last_mile,
"details": 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=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
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)
estimated_rate = round(35.0 + (weight_slabs * 11.0), 2)
is_cod = (str(payment_type).upper() == 'COD')
# Step 1: Validate against official Delhivery serviceability matrix
route_ok, route_msg, drop_info, pickup_info = check_delhivery_route_serviceability(
pickup_pincode=pickup_pincode,
drop_pincode=drop_pincode,
is_cod=is_cod
)
# Step 1: Validate individual first-mile and last-mile against Delhivery 18,699 database
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)
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 {
"carrier": "DELHIVERY",
"serviceable": False,
"cod_available": False,
"cod_available": cod_avail,
"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": {
"route_message": route_msg,
"route_message": " | ".join(err_parts),
"drop_info": drop_info,
"pickup_info": pickup_info
}
@ -358,60 +573,68 @@ def check_delhivery_availability(pickup_pincode, drop_pincode, weight=500, payme
"pickup_info": pickup_info
}
try:
client = DelhiveryAPIClient()
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)
if not use_mock:
try:
client = DelhiveryAPIClient(api_token=api_token)
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)
return {
"carrier": "DELHIVERY",
"serviceable": True,
"cod_available": bool(drop_info.get('cod', True)),
"cod_available": cod_avail,
"estimated_cost": live_cost,
"currency": "INR",
"city": drop_info.get('city'),
"state": drop_info.get('state'),
"state_code": drop_info.get('state_code'),
"city": d_city,
"state": d_state,
"state_code": d_st_code,
"first_mile": first_mile,
"last_mile": last_mile,
"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(
pickup_pincode=pickup_pincode,
drop_pincode=drop_pincode,
weight=weight,
env=env
)
ekart_res = check_ekart_availability(
pickup_pincode=pickup_pincode,
drop_pincode=drop_pincode,
weight=weight,
payment_type=payment_type
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
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",
"pickup_pincode": str(pickup_pincode),
"drop_pincode": str(drop_pincode),
@ -419,11 +642,21 @@ def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type='
"weight": weight,
"carriers": {
"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 = {
'SHADOWFAX': 0,

View file

@ -4,11 +4,10 @@ import requests
from django.conf import settings
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')
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"):
@ -56,7 +55,7 @@ def execute_sfx_call(service_type, action_name, method, full_url, headers, paylo
response_body_str = json.dumps({
"error": "Upstream Connection Failed",
"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)
status_label = "ERROR"

View file

@ -19,6 +19,9 @@
Multi-Carrier Shipping Platform
</div>
<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 %}">
📦 Shadowfax Forward
</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 class="form-group" style="margin: 0;">
<label class="form-label">Target Environment</label>
<select id="cfg_env" class="form-select">
<option value="production" selected>Production (dale.shadowfax.in)</option>
<option value="staging">Staging (dale.staging.shadowfax.in)</option>
</select>
<input type="hidden" id="cfg_env" value="production">
<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;">
Production (dale.shadowfax.in)
</div>
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">Shadowfax Authorization API Token</label>
@ -295,18 +295,7 @@
{% block extra_scripts %}
<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) {
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 class="form-group" style="margin: 0;">
<label class="form-label">Target Environment</label>
<select id="cfg_env" class="form-select">
<option value="production" selected>Production (dale.shadowfax.in)</option>
<option value="staging">Staging (dale.staging.shadowfax.in)</option>
</select>
<input type="hidden" id="cfg_env" value="production">
<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;">
Production (dale.shadowfax.in)
</div>
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">Shadowfax Authorization API Token</label>
@ -208,18 +208,7 @@
{% block extra_scripts %}
<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) {
e.preventDefault();

View file

@ -241,6 +241,42 @@ class ShippingAPITestCase(TestCase):
res_invalid = check_delhivery_availability('560077', '999999')
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 = [
# Dashboard Web UI Routes
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('reverse/', views.reverse_view, name='reverse_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):
if request.method == 'POST':
action = request.POST.get('action_type')
env = request.POST.get('environment', 'production')
env = 'production'
token = request.POST.get('api_token', '').strip()
if not token or (token == 'sample_staging_token_12345' and env == 'production'):
token = get_default_token(env)
elif not token:
token = get_default_token(env)
base_url = get_base_url(env)
if not token:
token = get_default_token()
base_url = get_base_url()
headers = {
'Authorization': f'Token {token}',
@ -237,13 +235,11 @@ def forward_view(request):
def reverse_view(request):
if request.method == 'POST':
action = request.POST.get('action_type')
env = request.POST.get('environment', 'production')
env = 'production'
token = request.POST.get('api_token', '').strip()
if not token or (token == 'sample_staging_token_12345' and env == 'production'):
token = get_default_token(env)
elif not token:
token = get_default_token(env)
base_url = get_base_url(env)
if not token:
token = get_default_token()
base_url = get_base_url()
headers = {
'Authorization': f'Token {token}',
@ -660,11 +656,9 @@ def api_forward_router(request, action_name):
else:
is_mock = data.get('use_mock', False)
token = (data.get('token') or request.headers.get('Authorization', '').replace('Token ', '')).strip()
if not token or (token == 'sample_staging_token_12345' and env == 'production'):
token = get_default_token(env)
elif not token:
token = get_default_token(env)
base_url = get_base_url(env)
if not token:
token = get_default_token()
base_url = get_base_url()
headers = {
'Authorization': f'Token {token}',
'Content-Type': 'application/json',
@ -831,11 +825,9 @@ def api_reverse_router(request, action_name):
else:
is_mock = data.get('use_mock', False)
token = (data.get('token') or request.headers.get('Authorization', '').replace('Token ', '')).strip()
if not token or (token == 'sample_staging_token_12345' and env == 'production'):
token = get_default_token(env)
elif not token:
token = get_default_token(env)
base_url = get_base_url(env)
if not token:
token = get_default_token()
base_url = get_base_url()
headers = {
'Authorization': f'Token {token}',
'Content-Type': 'application/json',
@ -1018,11 +1010,30 @@ def api_ekart_router(request, action_name):
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
def api_carrier_availability(request):
"""
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']:
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'
weight = data.get('weight') or request.GET.get('weight') or 500
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
res = check_all_carriers_availability(
@ -1044,7 +1058,10 @@ def api_carrier_availability(request):
drop_pincode=drop_pincode,
payment_type=payment_type,
weight=weight,
env=env
env=env,
sfx_token=sfx_token,
delhivery_token=delhivery_token,
use_mock=use_mock
)
return JsonResponse(res, status=200)

View file

@ -161,9 +161,7 @@ MAILERS = {
}
# 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_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')
# Ekart API Credentials & Endpoints