feat: add shipping service application with forward and reverse logistics, carrier allocation, and dashboard templates
This commit is contained in:
parent
ce484c4f11
commit
e327ac8988
18 changed files with 1121 additions and 132 deletions
65
README.md
65
README.md
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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;">
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
675
shipping/templates/shipping/carrier_checker.html
Normal file
675
shipping/templates/shipping/carrier_checker.html
Normal 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 %}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue