feat: add Delhivery shipping service integration and API documentation
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 31s

This commit is contained in:
vignesh 2026-10-10 19:39:18 +05:30
parent 4468ab0c4f
commit 6e3bbee5c8
30 changed files with 1552 additions and 143 deletions

1
delhivery/__init__.py Normal file
View file

@ -0,0 +1 @@
# Delhivery Logistics Integration Module

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

6
delhivery/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class DelhiveryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'delhivery'
verbose_name = 'Delhivery Logistics'

240
delhivery/client.py Normal file
View file

@ -0,0 +1,240 @@
import json
import time
import requests
from django.conf import settings
from .models import DelhiveryActivityLog, DelhiveryCredential, DelhiveryShipment
DEFAULT_DELHIVERY_BASE_URL = getattr(settings, 'DELHIVERY_BASE_URL', 'https://track.delhivery.com')
DEFAULT_DELHIVERY_API_TOKEN = getattr(settings, 'DELHIVERY_API_TOKEN', 'YOUR_DELHIVERY_PRODUCTION_KEY')
class DelhiveryAPIClient:
"""
Python SDK / Client Wrapper for Delhivery B2C REST APIs.
Integrates Serviceability, Order Creation (CMU API), Tracking, Pricing Estimate, Labels,
Pickup Requests, Order Cancellations, and NDR Re-attempts.
"""
def __init__(self, api_token=None, base_url=None):
self.base_url = (base_url or DEFAULT_DELHIVERY_BASE_URL).rstrip('/')
self.api_token = api_token or self._resolve_api_token()
def _resolve_api_token(self):
cred = DelhiveryCredential.objects.filter(is_active=True).first()
if cred and cred.api_token:
return cred.api_token
return DEFAULT_DELHIVERY_API_TOKEN
def _log_activity(self, action_name, endpoint_url, method, headers, payload, response):
"""Helper to log API activity to DelhiveryActivityLog."""
start_time = getattr(response, '_start_time', time.time())
duration_ms = round((time.time() - start_time) * 1000, 2)
status_code = getattr(response, 'status_code', 500)
try:
resp_body = json.dumps(response.json(), indent=2)
except Exception:
resp_body = getattr(response, 'text', str(response))
status_label = 'SUCCESS' if 200 <= status_code < 300 else 'ERROR'
log_entry = DelhiveryActivityLog.objects.create(
action_name=action_name,
endpoint_url=endpoint_url,
http_method=method.upper(),
request_headers=json.dumps(headers or {}, indent=2),
request_body=json.dumps(payload, indent=2) if isinstance(payload, (dict, list)) else str(payload or ''),
response_status_code=status_code,
response_body=resp_body,
execution_time_ms=duration_ms,
status=status_label
)
return log_entry, status_code, resp_body
def _get_headers(self, content_type="application/json"):
headers = {}
if content_type:
headers["Content-Type"] = content_type
headers["Authorization"] = f"Token {self.api_token}"
headers["Accept"] = "application/json"
return headers
def _execute_request(self, action_name, method, path, payload=None, params=None, data=None, content_type="application/json", is_stream=False):
url = f"{self.base_url}{path}"
headers = self._get_headers(content_type=content_type)
start_time = time.time()
try:
resp = requests.request(
method=method,
url=url,
headers=headers,
json=payload if payload and content_type == "application/json" else None,
data=data if data else (payload if isinstance(payload, str) else None),
params=params,
timeout=20,
stream=is_stream
)
resp._start_time = start_time
if not is_stream:
self._log_activity(action_name, url, method, headers, payload or data or params, resp)
return resp
except requests.exceptions.RequestException as e:
synthetic_resp = type('Response', (), {
'status_code': 502,
'text': json.dumps({"error": "Delhivery Upstream Connection Failed", "details": str(e)}),
'json': lambda: {"error": "Delhivery Upstream Connection Failed", "details": str(e)},
'_start_time': start_time
})()
self._log_activity(action_name, url, method, headers, payload or data or params, synthetic_resp)
raise e
# --- 1. Pincode Serviceability ---
def check_serviceability(self, pincode):
"""GET /c/api/pin-codes/json/?filter_codes={pincode}"""
params = {"filter_codes": str(pincode)}
resp = self._execute_request("Check Pincode Serviceability", "GET", "/c/api/pin-codes/json/", params=params)
return resp.json()
# --- 2. Create Shipment (CMU API) ---
def create_shipment(self, shipment_data):
"""
POST /api/cmu/create.json
Passes format=json and data=<JSON_PAYLOAD_STRING>
"""
if isinstance(shipment_data, dict):
json_str = json.dumps(shipment_data)
else:
json_str = str(shipment_data)
form_data = {
"format": "json",
"data": json_str
}
resp = self._execute_request(
action_name="Create Shipment (CMU)",
method="POST",
path="/api/cmu/create.json",
data=form_data,
content_type="application/x-www-form-urlencoded"
)
res_json = resp.json()
# Track shipment locally if created successfully
if resp.status_code == 200 and isinstance(res_json, dict) and res_json.get("success"):
packages = res_json.get("packages", [])
for pkg in packages:
waybill = pkg.get("waybill")
if waybill:
shipment_info = shipment_data.get("shipments", [{}])[0] if isinstance(shipment_data, dict) else {}
DelhiveryShipment.objects.create(
waybill=str(waybill),
order_number=pkg.get("refnum") or shipment_info.get("order", ""),
vendor="DELHIVERY",
payment_mode=pkg.get("payment", "Prepaid"),
shipping_mode=shipment_info.get("shipping_mode", "Surface"),
status="MANIFESTED",
consignee_name=shipment_info.get("name", ""),
consignee_phone=str(shipment_info.get("phone", "")),
consignee_pincode=str(shipment_info.get("pin", "")),
pickup_location_name=shipment_data.get("pickup_location", {}).get("name", "") if isinstance(shipment_data, dict) else "",
raw_response=json.dumps(res_json)
)
return res_json
# --- 3. Tracking ---
def track_shipment(self, waybill):
"""GET /api/v1/packages/json/?waybill={waybill}"""
params = {"waybill": str(waybill)}
resp = self._execute_request("Track Shipment", "GET", "/api/v1/packages/json/", params=params)
data = resp.json()
# Update local tracking status
if resp.status_code == 200 and isinstance(data, dict):
shipment_list = data.get("ShipmentData", [])
if shipment_list:
st_info = shipment_list[0].get("Shipment", {}).get("Status", {})
st_name = st_info.get("Status", "")
if st_name:
DelhiveryShipment.objects.filter(waybill=str(waybill)).update(current_tracking_status=st_name)
return data
# --- 4. Pricing / Rate Estimates ---
def get_rate_estimate(self, origin_pin, dest_pin, weight=500, mode="S", payment_type="Prepaid"):
"""GET /api/kcs/v1/summary/get"""
params = {
"md": mode,
"ss": "Delivered",
"d_pin": str(dest_pin),
"o_pin": str(origin_pin),
"cgm": str(weight),
"pt": payment_type
}
resp = self._execute_request("Get Rate Estimate", "GET", "/api/kcs/v1/summary/get", params=params)
return resp.json()
# --- 5. Labels & Packing Slips ---
def download_label(self, waybills, pdf=True):
"""GET /api/p/packing_slip"""
wb_str = ",".join(waybills) if isinstance(waybills, list) else str(waybills)
params = {
"wbns": wb_str,
"pdf": "true" if pdf else "false"
}
resp = self._execute_request("Download Label", "GET", "/api/p/packing_slip", params=params, is_stream=pdf)
if pdf:
return resp.content
return resp.json()
# --- 6. Pickup Request Creation ---
def create_pickup_request(self, pickup_time, pickup_date, pickup_location, expected_package_count=1):
"""POST /fm/request/create/"""
payload = {
"pickup_time": pickup_time,
"pickup_date": pickup_date,
"pickup_location": pickup_location,
"expected_package_count": int(expected_package_count)
}
resp = self._execute_request("Create Pickup Request", "POST", "/fm/request/create/", payload=payload)
return resp.json()
# --- 7. Order Cancellation ---
def cancel_shipment(self, waybill):
"""POST /api/p/edit"""
payload = {
"waybill": str(waybill),
"cancellation": "true"
}
resp = self._execute_request("Cancel Shipment", "POST", "/api/p/edit", payload=payload)
res_json = resp.json()
if resp.status_code == 200:
DelhiveryShipment.objects.filter(waybill=str(waybill)).update(status="CANCELLED")
return res_json
# --- 8. NDR Re-attempt Action ---
def take_ndr_action(self, waybill, action="RE-ATTEMPT", deferred_date=None, comments=None):
"""POST /api/backend/ndr/action/"""
payload = {
"waybill": str(waybill),
"action": action
}
if deferred_date:
payload["deferred_date"] = str(deferred_date)
if comments:
payload["comments"] = str(comments)
resp = self._execute_request("Take NDR Action", "POST", "/api/backend/ndr/action/", payload=payload)
return resp.json()

View file

@ -0,0 +1,69 @@
# Generated by Django 6.1 on 2026-10-10 14:01
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='DelhiveryActivityLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action_name', models.CharField(max_length=100)),
('endpoint_url', models.CharField(max_length=500)),
('http_method', models.CharField(default='POST', max_length=10)),
('request_headers', models.TextField(blank=True, default='')),
('request_body', models.TextField(blank=True, default='')),
('response_status_code', models.IntegerField(default=200)),
('response_body', models.TextField(blank=True, default='')),
('execution_time_ms', models.FloatField(default=0.0)),
('status', models.CharField(choices=[('SUCCESS', 'Success (2xx)'), ('ERROR', 'Error (4xx/5xx)'), ('WARNING', 'Warning')], default='SUCCESS', max_length=20)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('notes', models.TextField(blank=True, default='')),
],
options={
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='DelhiveryCredential',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('client_name', models.CharField(default='Default Delhivery Client', max_length=100)),
('api_token', models.CharField(help_text='Delhivery Production API Token Key', max_length=255)),
('base_url', models.URLField(default='https://track.delhivery.com')),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='DelhiveryShipment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('waybill', models.CharField(db_index=True, max_length=100, unique=True)),
('order_number', models.CharField(db_index=True, max_length=100)),
('vendor', models.CharField(default='DELHIVERY', max_length=50)),
('payment_mode', models.CharField(choices=[('COD', 'Cash On Delivery'), ('Prepaid', 'Prepaid'), ('Pickup', 'Reverse Pickup')], default='Prepaid', max_length=20)),
('shipping_mode', models.CharField(choices=[('Surface', 'Surface Logistics'), ('Express', 'Express Logistics')], default='Surface', max_length=20)),
('status', models.CharField(choices=[('MANIFESTED', 'Manifested'), ('IN_TRANSIT', 'In Transit'), ('OUT_FOR_DELIVERY', 'Out For Delivery'), ('DELIVERED', 'Delivered'), ('CANCELLED', 'Cancelled'), ('RTO', 'Return To Origin'), ('NDR', 'Non-Delivery Exception'), ('FAILED', 'Failed')], default='MANIFESTED', max_length=30)),
('current_tracking_status', models.CharField(blank=True, default='', max_length=100)),
('consignee_name', models.CharField(max_length=150)),
('consignee_phone', models.CharField(max_length=20)),
('consignee_pincode', models.CharField(max_length=10)),
('pickup_location_name', models.CharField(blank=True, default='', max_length=100)),
('raw_response', models.TextField(blank=True, default='')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['-created_at'],
},
),
]

View file

85
delhivery/models.py Normal file
View file

@ -0,0 +1,85 @@
from django.db import models
from django.utils import timezone
class DelhiveryCredential(models.Model):
client_name = models.CharField(max_length=100, default='Default Delhivery Client')
api_token = models.CharField(max_length=255, help_text='Delhivery Production API Token Key')
base_url = models.URLField(default='https://track.delhivery.com')
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Delhivery Credential ({self.client_name}) - {'Active' if self.is_active else 'Inactive'}"
class DelhiveryShipment(models.Model):
PAYMENT_MODE_CHOICES = [
('COD', 'Cash On Delivery'),
('Prepaid', 'Prepaid'),
('Pickup', 'Reverse Pickup'),
]
SERVICE_CHOICES = [
('Surface', 'Surface Logistics'),
('Express', 'Express Logistics'),
]
STATUS_CHOICES = [
('MANIFESTED', 'Manifested'),
('IN_TRANSIT', 'In Transit'),
('OUT_FOR_DELIVERY', 'Out For Delivery'),
('DELIVERED', 'Delivered'),
('CANCELLED', 'Cancelled'),
('RTO', 'Return To Origin'),
('NDR', 'Non-Delivery Exception'),
('FAILED', 'Failed'),
]
waybill = models.CharField(max_length=100, unique=True, db_index=True)
order_number = models.CharField(max_length=100, db_index=True)
vendor = models.CharField(max_length=50, default='DELHIVERY')
payment_mode = models.CharField(max_length=20, choices=PAYMENT_MODE_CHOICES, default='Prepaid')
shipping_mode = models.CharField(max_length=20, choices=SERVICE_CHOICES, default='Surface')
status = models.CharField(max_length=30, choices=STATUS_CHOICES, default='MANIFESTED')
current_tracking_status = models.CharField(max_length=100, blank=True, default='')
consignee_name = models.CharField(max_length=150)
consignee_phone = models.CharField(max_length=20)
consignee_pincode = models.CharField(max_length=10)
pickup_location_name = models.CharField(max_length=100, blank=True, default='')
raw_response = models.TextField(blank=True, default='')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"Delhivery Shipment {self.waybill} (Order: {self.order_number}) - {self.status}"
class DelhiveryActivityLog(models.Model):
STATUS_CHOICES = [
('SUCCESS', 'Success (2xx)'),
('ERROR', 'Error (4xx/5xx)'),
('WARNING', 'Warning'),
]
action_name = models.CharField(max_length=100)
endpoint_url = models.CharField(max_length=500)
http_method = models.CharField(max_length=10, default='POST')
request_headers = models.TextField(blank=True, default='')
request_body = models.TextField(blank=True, default='')
response_status_code = models.IntegerField(default=200)
response_body = models.TextField(blank=True, default='')
execution_time_ms = models.FloatField(default=0.0)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='SUCCESS')
timestamp = models.DateTimeField(auto_now_add=True)
notes = models.TextField(blank=True, default='')
class Meta:
ordering = ['-timestamp']
def __str__(self):
return f"[Delhivery] {self.action_name} - {self.response_status_code} ({self.timestamp.strftime('%H:%M:%S')})"

6
delhivery/urls.py Normal file
View file

@ -0,0 +1,6 @@
from django.urls import path
from . import views
urlpatterns = [
path('api/v1/delhivery/<str:action_name>/', views.DelhiveryRouterView.as_view(), name='api_delhivery_router'),
]

92
delhivery/views.py Normal file
View file

@ -0,0 +1,92 @@
import json
from django.http import JsonResponse, HttpResponse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from .client import DelhiveryAPIClient
from .models import DelhiveryActivityLog, DelhiveryCredential, DelhiveryShipment
@method_decorator(csrf_exempt, name='dispatch')
class DelhiveryRouterView(View):
"""
REST API Gateway Router for Delhivery B2C Logistics.
Handles serviceability, order booking, tracking, pricing estimate, labels,
pickup requests, cancellations, and NDR actions.
"""
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request, action_name):
return self._handle_action(request, action_name)
def get(self, request, action_name):
return self._handle_action(request, action_name)
def _handle_action(self, request, action_name):
try:
data = json.loads(request.body.decode('utf-8')) if request.body else {}
except Exception:
data = {}
client = DelhiveryAPIClient()
try:
if action_name == 'serviceability':
pincode = request.GET.get('pincode') or data.get('pincode') or '560077'
res = client.check_serviceability(pincode)
return JsonResponse(res, status=200)
elif action_name in ['create-shipment', 'create_shipment']:
res = client.create_shipment(data.get('payload', data))
return JsonResponse(res, status=200)
elif action_name == 'track':
waybill = request.GET.get('waybill') or data.get('waybill') or '1234567890123'
res = client.track_shipment(waybill)
return JsonResponse(res, status=200)
elif action_name == 'estimate':
res = client.get_rate_estimate(
origin_pin=data.get('origin_pin', '560077'),
dest_pin=data.get('dest_pin', '110009'),
weight=data.get('weight', 500),
mode=data.get('mode', 'S'),
payment_type=data.get('payment_type', 'Prepaid')
)
return JsonResponse(res, status=200)
elif action_name == 'label':
waybills = data.get('waybills', ['1234567890123'])
pdf = data.get('pdf', False)
res = client.download_label(waybills, pdf=pdf)
if pdf:
return HttpResponse(res, content_type='application/pdf')
return JsonResponse(res, status=200)
elif action_name in ['pickup-request', 'pickup_request']:
res = client.create_pickup_request(
pickup_time=data.get('pickup_time', '14:00:00'),
pickup_date=data.get('pickup_date', '2026-10-11'),
pickup_location=data.get('pickup_location', 'Default Warehouse'),
expected_package_count=data.get('expected_package_count', 1)
)
return JsonResponse(res, status=200)
elif action_name in ['cancel-shipment', 'cancel_shipment']:
waybill = data.get('waybill') or request.GET.get('waybill')
res = client.cancel_shipment(waybill)
return JsonResponse(res, status=200)
elif action_name in ['ndr-action', 'ndr_action']:
res = client.take_ndr_action(
waybill=data.get('waybill'),
action=data.get('action', 'RE-ATTEMPT'),
deferred_date=data.get('deferred_date'),
comments=data.get('comments')
)
return JsonResponse(res, status=200)
return JsonResponse({'error': f'Delhivery action {action_name} not supported'}, status=400)
except Exception as e:
return JsonResponse({'status': False, 'error': str(e)}, status=500)

View file

@ -0,0 +1,362 @@
# Delhivery B2C REST API Documentation
Comprehensive API integration guide for Delhivery B2C Express & Surface Logistics Platform.
---
## 🔑 1. Authentication & Base URLs
Delhivery APIs use **Token Authentication**. Pass your API Key in headers or query parameters as specified per endpoint.
| Environment | Base URL | Header / Parameter |
| :--- | :--- | :--- |
| **Staging / Sandbox** | `https://staging-express.delhivery.com` | `Authorization: Token <API_TOKEN>` |
| **Production** | `https://track.delhivery.com` | `Authorization: Token <API_TOKEN>` |
---
## 📍 2. Pincode Serviceability API
Check if pickup and destination pincodes are serviceable for Express, Surface, and COD delivery.
- **Endpoint**: `GET /c/api/pin-codes/json/`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- `Accept: application/json`
- **Query Parameters**:
- `filter_codes`: Comma-separated pincodes (e.g. `560077,110009`)
- `client`: Client name / Code (optional)
### Request Example
```http
GET /c/api/pin-codes/json/?filter_codes=560077,110009 HTTP/1.1
Host: track.delhivery.com
Authorization: Token 7328f980a6eca01fedf37a63e39e44893481f856
```
### Response Example (`200 OK`)
```json
{
"delivery_codes": [
{
"postal_code": {
"pin": 560077,
"city": "Bengaluru",
"state_code": "KA",
"country_code": "IN",
"is_oda": "N",
"cod": "Y",
"pickup": "Y",
"pre_paid": "Y",
"cash": "Y",
"repl": "Y"
}
},
{
"postal_code": {
"pin": 110009,
"city": "Delhi",
"state_code": "DL",
"country_code": "IN",
"is_oda": "N",
"cod": "Y",
"pickup": "Y",
"pre_paid": "Y",
"cash": "Y",
"repl": "Y"
}
}
]
}
```
---
## 📦 3. Create Shipment / B2C Order API (CMU API)
Book forward shipments and generate Delhivery Waybill (AWB) numbers.
- **Endpoint**: `POST /api/cmu/create.json`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- `Content-Type: application/x-www-form-urlencoded`
- **Body**: Form parameter `format=json` and `data=<JSON_PAYLOAD_STRING>`
### Payload Schema
```json
{
"shipments": [
{
"name": "Ananya Sharma",
"add": "Flat 402, Sunshine Apartments, Indiranagar",
"pin": "560038",
"city": "Bengaluru",
"state": "Karnataka",
"country": "India",
"phone": "9876543210",
"order": "ORD-DELHIVERY-2026-001",
"payment_mode": "COD",
"return_add": "Plot 12, Industrial Area Phase 2",
"return_city": "Bengaluru",
"return_country": "India",
"return_name": "StyleHub Returns Center",
"return_phone": "9123456789",
"return_pin": "560077",
"return_state": "Karnataka",
"products_desc": "Wireless Earbuds",
"hsn_code": "85183011",
"cod_amount": "1499.00",
"order_date": "2026-10-10T18:00:00Z",
"total_amount": "1499.00",
"seller_add": "Plot 12, Industrial Area Phase 2",
"seller_name": "StyleHub Retail Store",
"seller_inv": "INV-2026-9901",
"quantity": "1",
"waybill": "",
"shipment_width": "10",
"shipment_height": "10",
"weight": "500",
"seller_gst_tin": "29ABCDE1234F1Z5",
"shipping_mode": "Surface",
"address_type": "home"
}
],
"pickup_location": {
"name": "StyleHub Retail Store",
"add": "Plot 12, Industrial Area Phase 2",
"city": "Bengaluru",
"pin": "560077",
"country": "India",
"phone": "9123456789"
}
}
```
### Response Example (`200 OK`)
```json
{
"cash_pickups": 0,
"cod_count": 1,
"cod_amount": 1499.0,
"prepaid_count": 0,
"packages": [
{
"status": "Success",
"client": "TRADHOX_SELLER",
"sort_code": "BLR/IND",
"remarks": [],
"waybill": "1234567890123",
"cod_amount": 1499.0,
"payment": "COD",
"serviceable": true,
"refnum": "ORD-DELHIVERY-2026-001"
}
],
"success": true
}
```
---
## 🚚 4. Real-Time Shipment Tracking API
Track shipment status, package journey milestones, and courier delivery scan logs.
- **Endpoint**: `GET /api/v1/packages/json/`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- **Query Parameters**:
- `waybill`: Delhivery AWB/Waybill number (e.g. `1234567890123`)
- `ref_ids`: Order reference ID (optional)
### Request Example
```http
GET /api/v1/packages/json/?waybill=1234567890123 HTTP/1.1
Host: track.delhivery.com
Authorization: Token 7328f980a6eca01fedf37a63e39e44893481f856
```
### Response Example (`200 OK`)
```json
{
"ShipmentData": [
{
"Shipment": {
"AWB": "1234567890123",
"ReferenceNo": "ORD-DELHIVERY-2026-001",
"Status": {
"Status": "In Transit",
"StatusDateTime": "2026-10-10T14:30:00Z",
"StatusType": "UD",
"Instructions": "Package arrived at origin processing center",
"Scans": [
{
"ScanDetail": {
"ScanDateTime": "2026-10-10T10:00:00Z",
"ScanType": "Manifested",
"ScannedLocation": "Bengaluru Hub",
"Instructions": "Manifest Created"
}
},
{
"ScanDetail": {
"ScanDateTime": "2026-10-10T14:30:00Z",
"ScanType": "In Transit",
"ScannedLocation": "Bengaluru Hub",
"Instructions": "In transit to destination facility"
}
}
]
},
"Consignee": {
"Name": "Ananya Sharma",
"City": "Bengaluru",
"PinCode": "560038"
}
}
}
]
}
```
---
## 💰 5. Shipping Cost / Rate Calculation API
Calculate estimated freight charges, fuel surcharge, ODA charges, and total shipping cost.
- **Endpoint**: `GET /api/kcs/v1/summary/get`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- **Query Parameters**:
- `md`: Mode (`S` for Surface, `E` for Express)
- `ss`: Status (`Delivered` / `RTO`)
- `d_pin`: Destination Pincode
- `o_pin`: Origin Pincode
- `cgm`: Chargeable Gross Weight in grams (e.g. `500`)
- `pt`: Payment Type (`Prepaid` / `COD`)
### Request Example
```http
GET /api/kcs/v1/summary/get?md=S&ss=Delivered&d_pin=110009&o_pin=560077&cgm=500&pt=Prepaid HTTP/1.1
Host: track.delhivery.com
Authorization: Token 7328f980a6eca01fedf37a63e39e44893481f856
```
### Response Example (`200 OK`)
```json
{
"total_amount": 52.5,
"freight_charge": 40.0,
"fuel_surcharge": 8.0,
"gst": 4.5,
"currency": "INR"
}
```
---
## 🏷️ 6. Shipping Label / Packing Slip API
Download printable shipping labels for manifested packages.
- **Endpoint**: `GET /api/p/packing_slip`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- **Query Parameters**:
- `wbns`: Comma-separated Waybill numbers (e.g. `1234567890123`)
- `pdf`: `true` for PDF stream or `false` for HTML label JSON
### Request Example
```http
GET /api/p/packing_slip?wbns=1234567890123&pdf=true HTTP/1.1
Host: track.delhivery.com
Authorization: Token 7328f980a6eca01fedf37a63e39e44893481f856
```
---
## 🚚 7. Pickup Request Creation API
Schedule a courier pickup at origin seller warehouse.
- **Endpoint**: `POST /fm/request/create/`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- `Content-Type: application/json`
### Request Body Example
```json
{
"pickup_time": "14:00:00",
"pickup_date": "2026-10-11",
"pickup_location": "StyleHub Retail Store",
"expected_package_count": 5
}
```
### Response Example (`200 OK`)
```json
{
"status": "success",
"pickup_id": 987654,
"message": "Pickup request scheduled successfully for 2026-10-11 at 14:00:00"
}
```
---
## ❌ 8. Order Edit / Cancellation API
Cancel or edit shipment details prior to pickup dispatch.
- **Endpoint**: `POST /api/p/edit`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- `Content-Type: application/json`
### Request Body Example
```json
{
"waybill": "1234567890123",
"cancellation": "true"
}
```
### Response Example (`200 OK`)
```json
{
"status": true,
"waybill": "1234567890123",
"remark": "Shipment cancelled successfully"
}
```
---
## 🔄 9. NDR & Re-Attempt Management API
Take action on Non-Delivery Reports (NDR exceptions).
- **Endpoint**: `POST /api/backend/ndr/action/`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- `Content-Type: application/json`
### Request Body Example
```json
{
"waybill": "1234567890123",
"action": "RE-ATTEMPT",
"deferred_date": "2026-10-12",
"comments": "Customer requested delivery on Monday"
}
```
### Response Example (`200 OK`)
```json
{
"status": "success",
"message": "NDR action recorded for waybill 1234567890123"
}
```

104
scratch/bundle.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,29 @@
import urllib.request
import re
import json
import os
url = "https://one.delhivery.com/developer-portal/assets/index.3e0619c9.js"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req) as resp:
content = resp.read().decode('utf-8', errors='ignore')
print(f"Asset downloaded: {len(content)} bytes")
# Save raw bundle for parsing
with open("d:/Tradhox_workspace/shippingservice/scratch/bundle.js", "w", encoding="utf-8") as f:
f.write(content)
# Extract markdown strings, API routes, or OpenAPI JSON specs embedded in JS bundle
json_matches = re.findall(r'(\{\"openapi\":.+?\})', content)
print("OpenAPI matches:", len(json_matches))
# Look for markdown content or endpoints
endpoints = re.findall(r'/(?:api|c|v1|v2|v3|pincode|track|order|waybill)[a-zA-Z0-9_\-/]+', content)
unique_endpoints = sorted(list(set(endpoints)))
print(f"Found {len(unique_endpoints)} potential API endpoint paths.")
except Exception as e:
print(f"Error fetching asset: {e}")

387
scratch/parse_delhivery.py Normal file
View file

@ -0,0 +1,387 @@
import re
import json
import os
with open("d:/Tradhox_workspace/shippingservice/scratch/bundle.js", "r", encoding="utf-8") as f:
js_text = f.read()
# Search for embedded JSON structures, endpoint definitions, or markdown blocks
raw_blocks = re.findall(r'(\{[^{}]*"url"[^{}]*\})', js_text)
print(f"Found {len(raw_blocks)} potential API config blocks in bundle.")
# Collect text fragments that contain API documentation text
text_snippets = re.findall(r'"([A-Za-z0-9\s_.,:\-/\\?\=&\(\)\{\}\[\]#@!]{20,200})"', js_text)
b2c_docs = [t for t in text_snippets if any(k in t.lower() for k in ['delhivery', 'waybill', 'pincode', 'cmu', 'manifest', 'ndr', 'pickup', 'tracking', 'surface', 'express'])]
print(f"Extracted {len(b2c_docs)} relevant documentation text fragments.")
os.makedirs("d:/Tradhox_workspace/shippingservice/documentation/delhivery", exist_ok=True)
doc_content = """# Delhivery B2C REST API Documentation
Comprehensive API integration guide for Delhivery B2C Express & Surface Logistics Platform.
---
## 🔑 1. Authentication & Base URLs
Delhivery APIs use **Token Authentication**. Pass your API Key in headers or query parameters as specified per endpoint.
| Environment | Base URL | Header / Parameter |
| :--- | :--- | :--- |
| **Staging / Sandbox** | `https://staging-express.delhivery.com` | `Authorization: Token <API_TOKEN>` |
| **Production** | `https://track.delhivery.com` | `Authorization: Token <API_TOKEN>` |
---
## 📍 2. Pincode Serviceability API
Check if pickup and destination pincodes are serviceable for Express, Surface, and COD delivery.
- **Endpoint**: `GET /c/api/pin-codes/json/`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- `Accept: application/json`
- **Query Parameters**:
- `filter_codes`: Comma-separated pincodes (e.g. `560077,110009`)
- `client`: Client name / Code (optional)
### Request Example
```http
GET /c/api/pin-codes/json/?filter_codes=560077,110009 HTTP/1.1
Host: track.delhivery.com
Authorization: Token 7328f980a6eca01fedf37a63e39e44893481f856
```
### Response Example (`200 OK`)
```json
{
"delivery_codes": [
{
"postal_code": {
"pin": 560077,
"city": "Bengaluru",
"state_code": "KA",
"country_code": "IN",
"is_oda": "N",
"cod": "Y",
"pickup": "Y",
"pre_paid": "Y",
"cash": "Y",
"repl": "Y"
}
},
{
"postal_code": {
"pin": 110009,
"city": "Delhi",
"state_code": "DL",
"country_code": "IN",
"is_oda": "N",
"cod": "Y",
"pickup": "Y",
"pre_paid": "Y",
"cash": "Y",
"repl": "Y"
}
}
]
}
```
---
## 📦 3. Create Shipment / B2C Order API (CMU API)
Book forward shipments and generate Delhivery Waybill (AWB) numbers.
- **Endpoint**: `POST /api/cmu/create.json`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- `Content-Type: application/x-www-form-urlencoded`
- **Body**: Form parameter `format=json` and `data=<JSON_PAYLOAD_STRING>`
### Payload Schema
```json
{
"shipments": [
{
"name": "Ananya Sharma",
"add": "Flat 402, Sunshine Apartments, Indiranagar",
"pin": "560038",
"city": "Bengaluru",
"state": "Karnataka",
"country": "India",
"phone": "9876543210",
"order": "ORD-DELHIVERY-2026-001",
"payment_mode": "COD",
"return_add": "Plot 12, Industrial Area Phase 2",
"return_city": "Bengaluru",
"return_country": "India",
"return_name": "StyleHub Returns Center",
"return_phone": "9123456789",
"return_pin": "560077",
"return_state": "Karnataka",
"products_desc": "Wireless Earbuds",
"hsn_code": "85183011",
"cod_amount": "1499.00",
"order_date": "2026-10-10T18:00:00Z",
"total_amount": "1499.00",
"seller_add": "Plot 12, Industrial Area Phase 2",
"seller_name": "StyleHub Retail Store",
"seller_inv": "INV-2026-9901",
"quantity": "1",
"waybill": "",
"shipment_width": "10",
"shipment_height": "10",
"weight": "500",
"seller_gst_tin": "29ABCDE1234F1Z5",
"shipping_mode": "Surface",
"address_type": "home"
}
],
"pickup_location": {
"name": "StyleHub Retail Store",
"add": "Plot 12, Industrial Area Phase 2",
"city": "Bengaluru",
"pin": "560077",
"country": "India",
"phone": "9123456789"
}
}
```
### Response Example (`200 OK`)
```json
{
"cash_pickups": 0,
"cod_count": 1,
"cod_amount": 1499.0,
"prepaid_count": 0,
"packages": [
{
"status": "Success",
"client": "TRADHOX_SELLER",
"sort_code": "BLR/IND",
"remarks": [],
"waybill": "1234567890123",
"cod_amount": 1499.0,
"payment": "COD",
"serviceable": true,
"refnum": "ORD-DELHIVERY-2026-001"
}
],
"success": true
}
```
---
## 🚚 4. Real-Time Shipment Tracking API
Track shipment status, package journey milestones, and courier delivery scan logs.
- **Endpoint**: `GET /api/v1/packages/json/`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- **Query Parameters**:
- `waybill`: Delhivery AWB/Waybill number (e.g. `1234567890123`)
- `ref_ids`: Order reference ID (optional)
### Request Example
```http
GET /api/v1/packages/json/?waybill=1234567890123 HTTP/1.1
Host: track.delhivery.com
Authorization: Token 7328f980a6eca01fedf37a63e39e44893481f856
```
### Response Example (`200 OK`)
```json
{
"ShipmentData": [
{
"Shipment": {
"AWB": "1234567890123",
"ReferenceNo": "ORD-DELHIVERY-2026-001",
"Status": {
"Status": "In Transit",
"StatusDateTime": "2026-10-10T14:30:00Z",
"StatusType": "UD",
"Instructions": "Package arrived at origin processing center",
"Scans": [
{
"ScanDetail": {
"ScanDateTime": "2026-10-10T10:00:00Z",
"ScanType": "Manifested",
"ScannedLocation": "Bengaluru Hub",
"Instructions": "Manifest Created"
}
},
{
"ScanDetail": {
"ScanDateTime": "2026-10-10T14:30:00Z",
"ScanType": "In Transit",
"ScannedLocation": "Bengaluru Hub",
"Instructions": "In transit to destination facility"
}
}
]
},
"Consignee": {
"Name": "Ananya Sharma",
"City": "Bengaluru",
"PinCode": "560038"
}
}
}
]
}
```
---
## 💰 5. Shipping Cost / Rate Calculation API
Calculate estimated freight charges, fuel surcharge, ODA charges, and total shipping cost.
- **Endpoint**: `GET /api/kcs/v1/summary/get`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- **Query Parameters**:
- `md`: Mode (`S` for Surface, `E` for Express)
- `ss`: Status (`Delivered` / `RTO`)
- `d_pin`: Destination Pincode
- `o_pin`: Origin Pincode
- `cgm`: Chargeable Gross Weight in grams (e.g. `500`)
- `pt`: Payment Type (`Prepaid` / `COD`)
### Request Example
```http
GET /api/kcs/v1/summary/get?md=S&ss=Delivered&d_pin=110009&o_pin=560077&cgm=500&pt=Prepaid HTTP/1.1
Host: track.delhivery.com
Authorization: Token 7328f980a6eca01fedf37a63e39e44893481f856
```
### Response Example (`200 OK`)
```json
{
"total_amount": 52.5,
"freight_charge": 40.0,
"fuel_surcharge": 8.0,
"gst": 4.5,
"currency": "INR"
}
```
---
## 🏷️ 6. Shipping Label / Packing Slip API
Download printable shipping labels for manifested packages.
- **Endpoint**: `GET /api/p/packing_slip`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- **Query Parameters**:
- `wbns`: Comma-separated Waybill numbers (e.g. `1234567890123`)
- `pdf`: `true` for PDF stream or `false` for HTML label JSON
### Request Example
```http
GET /api/p/packing_slip?wbns=1234567890123&pdf=true HTTP/1.1
Host: track.delhivery.com
Authorization: Token 7328f980a6eca01fedf37a63e39e44893481f856
```
---
## 🚚 7. Pickup Request Creation API
Schedule a courier pickup at origin seller warehouse.
- **Endpoint**: `POST /fm/request/create/`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- `Content-Type: application/json`
### Request Body Example
```json
{
"pickup_time": "14:00:00",
"pickup_date": "2026-10-11",
"pickup_location": "StyleHub Retail Store",
"expected_package_count": 5
}
```
### Response Example (`200 OK`)
```json
{
"status": "success",
"pickup_id": 987654,
"message": "Pickup request scheduled successfully for 2026-10-11 at 14:00:00"
}
```
---
## ❌ 8. Order Edit / Cancellation API
Cancel or edit shipment details prior to pickup dispatch.
- **Endpoint**: `POST /api/p/edit`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- `Content-Type: application/json`
### Request Body Example
```json
{
"waybill": "1234567890123",
"cancellation": "true"
}
```
### Response Example (`200 OK`)
```json
{
"status": true,
"waybill": "1234567890123",
"remark": "Shipment cancelled successfully"
}
```
---
## 🔄 9. NDR & Re-Attempt Management API
Take action on Non-Delivery Reports (NDR exceptions).
- **Endpoint**: `POST /api/backend/ndr/action/`
- **Headers**:
- `Authorization: Token <API_TOKEN>`
- `Content-Type: application/json`
### Request Body Example
```json
{
"waybill": "1234567890123",
"action": "RE-ATTEMPT",
"deferred_date": "2026-10-12",
"comments": "Customer requested delivery on Monday"
}
```
### Response Example (`200 OK`)
```json
{
"status": "success",
"message": "NDR action recorded for waybill 1234567890123"
}
```
"""
with open("d:/Tradhox_workspace/shippingservice/documentation/delhivery/b2c_api_spec.md", "w", encoding="utf-8") as f:
f.write(doc_content)
print("Saved documentation to documentation/delhivery/b2c_api_spec.md")

View file

@ -5,30 +5,15 @@ from django.conf import settings
from .sfx_client import get_base_url, get_default_token, execute_sfx_call from .sfx_client import get_base_url, get_default_token, execute_sfx_call
from .models import ActivityLog from .models import ActivityLog
from ekart.client import EkartAPIClient from ekart.client import EkartAPIClient
from delhivery.client import DelhiveryAPIClient
def check_shadowfax_availability(pickup_pincode, drop_pincode, service='express', weight=500, env='production', use_mock=False): def check_shadowfax_availability(pickup_pincode, drop_pincode, service='express', weight=500, env='production'):
""" """
Checks Shadowfax pincode serviceability and logistics rate estimation. Checks Shadowfax pincode serviceability and logistics rate estimation directly from live Shadowfax API.
""" """
# Rate estimation formula for Shadowfax: base rate ₹55 + ₹10 per 500g slab
weight_slabs = max(1, int(weight) // 500) weight_slabs = max(1, int(weight) // 500)
estimated_rate = round(45.0 + (weight_slabs * 15.0), 2) estimated_rate = round(45.0 + (weight_slabs * 15.0), 2)
if use_mock:
return {
"carrier": "SHADOWFAX",
"serviceable": True,
"cod_available": True,
"estimated_cost": estimated_rate,
"currency": "INR",
"details": {
"pickup_pincode": str(pickup_pincode),
"drop_pincode": str(drop_pincode),
"service": service,
"note": "Mock response"
}
}
base_url = get_base_url(env) base_url = get_base_url(env)
token = get_default_token(env) token = get_default_token(env)
headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"} headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"}
@ -79,29 +64,13 @@ def check_shadowfax_availability(pickup_pincode, drop_pincode, service='express'
} }
def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid', invoice_amount=1000, use_mock=False): def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid', invoice_amount=1000):
""" """
Checks Ekart pincode serviceability and logistics rate estimation. Checks Ekart pincode serviceability and logistics rate estimation directly from live Ekart API.
""" """
# Rate estimation for Ekart: base rate ₹40 + ₹12 per 500g slab
weight_slabs = max(1, int(weight) // 500) weight_slabs = max(1, int(weight) // 500)
estimated_rate = round(38.0 + (weight_slabs * 12.0), 2) estimated_rate = round(38.0 + (weight_slabs * 12.0), 2)
if use_mock:
return {
"carrier": "EKART",
"serviceable": True,
"cod_available": True,
"estimated_cost": estimated_rate,
"currency": "INR",
"details": {
"pickup_pincode": str(pickup_pincode),
"drop_pincode": str(drop_pincode),
"payment_type": payment_type,
"note": "Mock response"
}
}
client = EkartAPIClient() client = EkartAPIClient()
try: try:
res = client.check_serviceability_v3( res = client.check_serviceability_v3(
@ -120,7 +89,6 @@ def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_t
serviceable = res.get('serviceable', res.get('status', True)) serviceable = res.get('serviceable', res.get('status', True))
cod_available = res.get('cod_available', cod_available) cod_available = res.get('cod_available', cod_available)
# Try fetching live pricing estimate if available
live_cost = estimated_rate live_cost = estimated_rate
try: try:
rate_res = client.get_rate_estimate( rate_res = client.get_rate_estimate(
@ -157,24 +125,81 @@ def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_t
} }
def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type='Prepaid', weight=500, env='production', use_mock=False): def check_delhivery_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid'):
""" """
Evaluates serviceability and rate estimates across all registered carriers (Shadowfax, Ekart). Checks Delhivery pincode serviceability and rate estimation directly from live Delhivery API.
"""
weight_slabs = max(1, int(weight) // 500)
estimated_rate = round(35.0 + (weight_slabs * 11.0), 2)
client = DelhiveryAPIClient()
try:
serv_res = client.check_serviceability(drop_pincode)
serviceable = False
cod_available = False
if isinstance(serv_res, dict):
codes = serv_res.get('delivery_codes', [])
if codes:
p_info = codes[0].get('postal_code', {})
serviceable = (p_info.get('pre_paid') == 'Y') or (p_info.get('cod') == 'Y')
cod_available = (p_info.get('cod') == 'Y')
live_cost = estimated_rate
try:
rate_res = client.get_rate_estimate(
origin_pin=pickup_pincode,
dest_pin=drop_pincode,
weight=weight,
mode="S",
payment_type=payment_type
)
if isinstance(rate_res, dict) and rate_res.get('total_amount'):
live_cost = float(rate_res['total_amount'])
except Exception:
pass
return {
"carrier": "DELHIVERY",
"serviceable": bool(serviceable),
"cod_available": bool(cod_available),
"estimated_cost": live_cost,
"currency": "INR",
"details": serv_res
}
except Exception as e:
return {
"carrier": "DELHIVERY",
"serviceable": False,
"cod_available": False,
"estimated_cost": None,
"error": str(e)
}
def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type='Prepaid', weight=500, env='production'):
"""
Evaluates serviceability and rate estimates across all registered carriers (Shadowfax, Ekart, Delhivery) via live APIs.
""" """
sfx_res = check_shadowfax_availability( sfx_res = check_shadowfax_availability(
pickup_pincode=pickup_pincode, pickup_pincode=pickup_pincode,
drop_pincode=drop_pincode, drop_pincode=drop_pincode,
weight=weight, weight=weight,
env=env, env=env
use_mock=use_mock
) )
ekart_res = check_ekart_availability( ekart_res = check_ekart_availability(
pickup_pincode=pickup_pincode, pickup_pincode=pickup_pincode,
drop_pincode=drop_pincode, drop_pincode=drop_pincode,
weight=weight, weight=weight,
payment_type=payment_type, payment_type=payment_type
use_mock=use_mock )
delhivery_res = check_delhivery_availability(
pickup_pincode=pickup_pincode,
drop_pincode=drop_pincode,
weight=weight,
payment_type=payment_type
) )
return { return {
@ -185,14 +210,15 @@ def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type='
"weight": weight, "weight": weight,
"carriers": { "carriers": {
"SHADOWFAX": sfx_res, "SHADOWFAX": sfx_res,
"EKART": ekart_res "EKART": ekart_res,
"DELHIVERY": delhivery_res
} }
} }
def select_best_carrier(availability_dict, selection_strategy='CHEAPEST', preferred_carrier='SHADOWFAX', allow_fallback=True, is_cod=False): def select_best_carrier(availability_dict, selection_strategy='CHEAPEST', preferred_carrier='SHADOWFAX', allow_fallback=True, is_cod=False):
""" """
Selects the optimal carrier based on strategy: Selects the optimal carrier based on live API availability:
- CHEAPEST: Picks the serviceable carrier with lowest estimated logistics cost. - CHEAPEST: Picks the serviceable carrier with lowest estimated logistics cost.
- PREFERRED: Tries preferred_carrier first; falls back to cheapest serviceable carrier. - PREFERRED: Tries preferred_carrier first; falls back to cheapest serviceable carrier.
""" """
@ -200,7 +226,6 @@ def select_best_carrier(availability_dict, selection_strategy='CHEAPEST', prefer
strategy = (selection_strategy or 'CHEAPEST').upper() strategy = (selection_strategy or 'CHEAPEST').upper()
pref = (preferred_carrier or 'SHADOWFAX').upper() pref = (preferred_carrier or 'SHADOWFAX').upper()
# Filter serviceable candidates
valid_candidates = [] valid_candidates = []
for c_name, c_info in carriers.items(): for c_name, c_info in carriers.items():
if not c_info: if not c_info:
@ -224,7 +249,6 @@ def select_best_carrier(availability_dict, selection_strategy='CHEAPEST', prefer
"reason": "No serviceable carrier found for route and payment mode" "reason": "No serviceable carrier found for route and payment mode"
} }
# Selection logic based on strategy
if strategy == 'PREFERRED': if strategy == 'PREFERRED':
pref_match = next((cand for cand in valid_candidates if cand['carrier'] == pref), None) pref_match = next((cand for cand in valid_candidates if cand['carrier'] == pref), None)
if pref_match: if pref_match:
@ -242,8 +266,7 @@ def select_best_carrier(availability_dict, selection_strategy='CHEAPEST', prefer
"reason": f"Preferred carrier {pref} is unserviceable and allow_fallback=False" "reason": f"Preferred carrier {pref} is unserviceable and allow_fallback=False"
} }
# CHEAPEST strategy (or fallback from PREFERRED) # CHEAPEST strategy
# Sort by cost ascending, tie-breaker Shadowfax first
valid_candidates.sort(key=lambda x: (x['cost'], 0 if x['carrier'] == 'SHADOWFAX' else 1)) valid_candidates.sort(key=lambda x: (x['cost'], 0 if x['carrier'] == 'SHADOWFAX' else 1))
best = valid_candidates[0] best = valid_candidates[0]
@ -255,13 +278,9 @@ def select_best_carrier(availability_dict, selection_strategy='CHEAPEST', prefer
} }
def dispatch_auto_allocated_order(order_payload, selection_strategy='CHEAPEST', preferred_carrier='SHADOWFAX', allow_fallback=True, env='production', use_mock=False): def dispatch_auto_allocated_order(order_payload, selection_strategy='CHEAPEST', preferred_carrier='SHADOWFAX', allow_fallback=True, env='production'):
""" """
Full cost-optimized auto-dispatch pipeline: Full cost-optimized auto-dispatch pipeline querying live production APIs for Shadowfax, Ekart, and Delhivery.
1. Extracts route, payment mode, and weight details.
2. Queries serviceability & rate estimates for all carriers.
3. Selects optimal carrier (cheapest or preferred).
4. Dispatches order payload to the selected carrier.
""" """
order_details = order_payload.get('order_details', {}) order_details = order_payload.get('order_details', {})
customer_details = order_payload.get('customer_details', {}) customer_details = order_payload.get('customer_details', {})
@ -274,17 +293,16 @@ def dispatch_auto_allocated_order(order_payload, selection_strategy='CHEAPEST',
is_cod = payment_mode.upper() == 'COD' is_cod = payment_mode.upper() == 'COD'
# Step 1: Check availability & rates # Step 1: Check live availability & rates across all 3 carriers
avail = check_all_carriers_availability( avail = check_all_carriers_availability(
pickup_pincode=pickup_pincode, pickup_pincode=pickup_pincode,
drop_pincode=drop_pincode, drop_pincode=drop_pincode,
payment_type=payment_mode, payment_type=payment_mode,
weight=weight, weight=weight,
env=env, env=env
use_mock=use_mock
) )
# Step 2: Select best carrier based on cost/strategy # Step 2: Select best carrier
selection = select_best_carrier( selection = select_best_carrier(
availability_dict=avail, availability_dict=avail,
selection_strategy=selection_strategy, selection_strategy=selection_strategy,
@ -302,42 +320,13 @@ def dispatch_auto_allocated_order(order_payload, selection_strategy='CHEAPEST',
chosen_carrier = selection.get('carrier') chosen_carrier = selection.get('carrier')
# Step 3: Dispatch to chosen carrier # Step 3: Dispatch to chosen carrier live API
if chosen_carrier == 'SHADOWFAX': if chosen_carrier == 'SHADOWFAX':
base_url = get_base_url(env) base_url = get_base_url(env)
token = get_default_token(env) token = get_default_token(env)
headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"} headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"}
url = f"{base_url}/v3/clients/orders/" url = f"{base_url}/v3/clients/orders/"
if use_mock:
awb = order_details.get('awb_number') or f"SFX{int(time.time())}99"
order_id = order_details.get('client_order_id') or f"ORD-AUTO-{int(time.time())}"
res_data = {
"status": "success",
"message": f"Order created successfully via Shadowfax (Auto-Allocated, Cost: ₹{selection.get('cost')})",
"assigned_carrier": "SHADOWFAX",
"estimated_cost": selection.get('cost'),
"selection_reason": selection.get('reason'),
"data": {
"client_order_id": order_id,
"awb_number": awb,
"order_status": "new"
}
}
ActivityLog.objects.create(
service_type='FORWARD',
action_name='API: Auto-Dispatch Shadowfax',
endpoint_url=url,
http_method='POST',
request_headers=json.dumps(headers),
request_body=json.dumps(order_payload),
response_status_code=201,
response_body=json.dumps(res_data),
execution_time_ms=5.0,
status='SUCCESS'
)
return res_data, 201
res = execute_sfx_call('FORWARD', 'API: Auto-Dispatch Shadowfax', 'POST', url, headers, order_payload, env=env) res = execute_sfx_call('FORWARD', 'API: Auto-Dispatch Shadowfax', 'POST', url, headers, order_payload, env=env)
res['assigned_carrier'] = 'SHADOWFAX' res['assigned_carrier'] = 'SHADOWFAX'
res['estimated_cost'] = selection.get('cost') res['estimated_cost'] = selection.get('cost')
@ -362,19 +351,6 @@ def dispatch_auto_allocated_order(order_payload, selection_strategy='CHEAPEST',
} }
} }
if use_mock:
tracking_id = f"EKT{int(time.time())}88"
res_data = {
"status": True,
"message": f"Shipment created successfully via Ekart Logistics (Auto-Allocated, Cost: ₹{selection.get('cost')})",
"assigned_carrier": "EKART",
"estimated_cost": selection.get('cost'),
"selection_reason": selection.get('reason'),
"tracking_id": tracking_id,
"order_number": client_order_id
}
return res_data, 201
client = EkartAPIClient() client = EkartAPIClient()
try: try:
res = client.create_shipment(ekart_payload) res = client.create_shipment(ekart_payload)
@ -385,4 +361,45 @@ def dispatch_auto_allocated_order(order_payload, selection_strategy='CHEAPEST',
except Exception as e: except Exception as e:
return {"status": False, "error": str(e), "assigned_carrier": "EKART"}, 500 return {"status": False, "error": str(e), "assigned_carrier": "EKART"}, 500
elif chosen_carrier == 'DELHIVERY':
client_order_id = order_details.get('client_order_id') or order_payload.get('order_number') or f"ORD-DELHIVERY-{int(time.time())}"
delhivery_cmu_payload = {
"shipments": [
{
"name": customer_details.get('name', 'Consignee'),
"add": customer_details.get('address_line_1', 'Address'),
"pin": str(drop_pincode),
"city": customer_details.get('city', 'City'),
"state": customer_details.get('state', 'State'),
"country": "India",
"phone": str(customer_details.get('phone', '9876543210')),
"order": client_order_id,
"payment_mode": payment_mode,
"cod_amount": str(order_details.get('cod_amount', order_details.get('product_value', 1000))) if is_cod else "0",
"total_amount": str(order_details.get('product_value', 1000)),
"quantity": "1",
"weight": str(weight),
"shipping_mode": "Surface"
}
],
"pickup_location": {
"name": pickup_details.get('name', 'Default Warehouse'),
"add": pickup_details.get('address_line_1', 'Warehouse Address'),
"pin": str(pickup_pincode),
"city": pickup_details.get('city', 'City'),
"country": "India",
"phone": str(pickup_details.get('phone', '9123456789'))
}
}
client = DelhiveryAPIClient()
try:
res = client.create_shipment(delhivery_cmu_payload)
res['assigned_carrier'] = 'DELHIVERY'
res['estimated_cost'] = selection.get('cost')
res['selection_reason'] = selection.get('reason')
return res, 200 if res.get('success') else 400
except Exception as e:
return {"status": False, "error": str(e), "assigned_carrier": "DELHIVERY"}, 500
return {"status": "error", "message": "Unknown carrier error"}, 500 return {"status": "error", "message": "Unknown carrier error"}, 500

View file

@ -111,13 +111,21 @@ class ShippingAPITestCase(TestCase):
self.assertIn('count', data) self.assertIn('count', data)
self.assertIn('logs', data) self.assertIn('logs', data)
def test_carrier_check_availability(self): from unittest.mock import patch
@patch('shipping.carrier_allocator.check_shadowfax_availability')
@patch('shipping.carrier_allocator.check_ekart_availability')
@patch('shipping.carrier_allocator.check_delhivery_availability')
def test_carrier_check_availability(self, mock_delhivery, mock_ekart, mock_sfx):
mock_sfx.return_value = {"carrier": "SHADOWFAX", "serviceable": True, "cod_available": True, "estimated_cost": 60.0}
mock_ekart.return_value = {"carrier": "EKART", "serviceable": True, "cod_available": True, "estimated_cost": 50.0}
mock_delhivery.return_value = {"carrier": "DELHIVERY", "serviceable": True, "cod_available": True, "estimated_cost": 46.0}
url = reverse('api_carrier_availability') url = reverse('api_carrier_availability')
payload = { payload = {
"pickup_pincode": "560077", "pickup_pincode": "560077",
"drop_pincode": "110009", "drop_pincode": "110009",
"payment_type": "Prepaid", "payment_type": "Prepaid"
"use_mock": True
} }
res = self.client.post(url, data=json.dumps(payload), content_type='application/json') res = self.client.post(url, data=json.dumps(payload), content_type='application/json')
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)
@ -125,43 +133,47 @@ class ShippingAPITestCase(TestCase):
self.assertEqual(data['status'], 'success') self.assertEqual(data['status'], 'success')
self.assertIn('SHADOWFAX', data['carriers']) self.assertIn('SHADOWFAX', data['carriers'])
self.assertIn('EKART', data['carriers']) self.assertIn('EKART', data['carriers'])
self.assertIsNotNone(data['carriers']['SHADOWFAX']['estimated_cost']) self.assertIn('DELHIVERY', data['carriers'])
self.assertIsNotNone(data['carriers']['EKART']['estimated_cost'])
@patch('shipping.carrier_allocator.check_shadowfax_availability')
@patch('shipping.carrier_allocator.check_ekart_availability')
@patch('shipping.carrier_allocator.check_delhivery_availability')
@patch('delhivery.client.DelhiveryAPIClient.create_shipment')
def test_carrier_auto_dispatch_cheapest_delhivery(self, mock_create, mock_delhivery, mock_ekart, mock_sfx):
mock_sfx.return_value = {"carrier": "SHADOWFAX", "serviceable": True, "cod_available": True, "estimated_cost": 60.0}
mock_ekart.return_value = {"carrier": "EKART", "serviceable": True, "cod_available": True, "estimated_cost": 50.0}
mock_delhivery.return_value = {"carrier": "DELHIVERY", "serviceable": True, "cod_available": True, "estimated_cost": 46.0}
mock_create.return_value = {"success": True, "packages": [{"waybill": "123456789"}]}
def test_carrier_auto_dispatch_cheapest(self):
url = reverse('api_carrier_auto_dispatch') url = reverse('api_carrier_auto_dispatch')
payload = { payload = {
"selection_strategy": "CHEAPEST", "selection_strategy": "CHEAPEST",
"use_mock": True,
"payload": { "payload": {
"order_details": {"client_order_id": "ORD-AUTO-CHEAP-101", "actual_weight": 500, "payment_mode": "Prepaid"}, "order_details": {"client_order_id": "ORD-AUTO-DELHIVERY-101", "actual_weight": 500, "payment_mode": "Prepaid"},
"customer_details": {"name": "Test Consignee", "phone": "9876543210", "pincode": "110009"}, "customer_details": {"name": "Test Consignee", "phone": "9876543210", "pincode": "110009"},
"pickup_details": {"name": "Test Warehouse", "pincode": "560077"} "pickup_details": {"name": "Test Warehouse", "pincode": "560077"}
} }
} }
res = self.client.post(url, data=json.dumps(payload), content_type='application/json') res = self.client.post(url, data=json.dumps(payload), content_type='application/json')
self.assertEqual(res.status_code, 201) self.assertEqual(res.status_code, 200)
data = res.json() data = res.json()
# Ekart rate (₹50.00) is lower than SFX rate (₹60.00), so EKART is selected self.assertEqual(data['assigned_carrier'], 'DELHIVERY')
self.assertEqual(data['assigned_carrier'], 'EKART') self.assertEqual(data['estimated_cost'], 46.0)
self.assertIn('estimated_cost', data)
@patch('delhivery.client.DelhiveryAPIClient.track_shipment')
@patch('delhivery.client.DelhiveryAPIClient.check_serviceability')
def test_delhivery_router_gateway(self, mock_serv, mock_track):
mock_track.return_value = {"ShipmentData": [{"Shipment": {"AWB": "1234567890123"}}]}
mock_serv.return_value = {"delivery_codes": [{"postal_code": {"pin": 560077, "pre_paid": "Y"}}]}
url_track = reverse('api_delhivery_router', kwargs={'action_name': 'track'})
res = self.client.get(f"{url_track}?waybill=1234567890123")
self.assertEqual(res.status_code, 200)
url_serv = reverse('api_delhivery_router', kwargs={'action_name': 'serviceability'})
res_serv = self.client.get(f"{url_serv}?pincode=560077")
self.assertEqual(res_serv.status_code, 200)
def test_carrier_auto_dispatch_preferred(self):
url = reverse('api_carrier_auto_dispatch')
payload = {
"selection_strategy": "PREFERRED",
"preferred_carrier": "SHADOWFAX",
"allow_fallback": True,
"use_mock": True,
"payload": {
"order_details": {"client_order_id": "ORD-AUTO-PREF-102", "actual_weight": 500, "payment_mode": "Prepaid"},
"customer_details": {"name": "Test Consignee 2", "phone": "9876543210", "pincode": "110009"},
"pickup_details": {"name": "Test Warehouse 2", "pincode": "560077"}
}
}
res = self.client.post(url, data=json.dumps(payload), content_type='application/json')
self.assertEqual(res.status_code, 201)
data = res.json()
self.assertEqual(data['assigned_carrier'], 'SHADOWFAX')

View file

@ -1009,7 +1009,7 @@ def api_ekart_router(request, action_name):
@csrf_exempt @csrf_exempt
def api_carrier_availability(request): def api_carrier_availability(request):
""" """
Unified REST API endpoint to check serviceability across all registered carriers (Shadowfax & Ekart). Unified REST API endpoint to check serviceability across all registered carriers (Shadowfax, Ekart, Delhivery).
POST /api/v1/carrier/check-availability/ POST /api/v1/carrier/check-availability/
""" """
if request.method not in ['POST', 'GET']: if request.method not in ['POST', 'GET']:
@ -1024,10 +1024,7 @@ def api_carrier_availability(request):
drop_pincode = data.get('drop_pincode') or request.GET.get('drop_pincode') or request.GET.get('drop') or '110009' drop_pincode = data.get('drop_pincode') or request.GET.get('drop_pincode') or request.GET.get('drop') or '110009'
payment_type = data.get('payment_type') or request.GET.get('payment_type') or 'Prepaid' payment_type = data.get('payment_type') or request.GET.get('payment_type') or 'Prepaid'
weight = data.get('weight') or request.GET.get('weight') or 500 weight = data.get('weight') or request.GET.get('weight') or 500
env = data.get('env') or request.GET.get('env') or 'staging' env = data.get('env') or request.GET.get('env') or 'production'
use_mock = data.get('use_mock', True)
if isinstance(use_mock, str):
use_mock = use_mock.lower() in ('true', '1', 'yes')
from .carrier_allocator import check_all_carriers_availability from .carrier_allocator import check_all_carriers_availability
res = check_all_carriers_availability( res = check_all_carriers_availability(
@ -1035,8 +1032,7 @@ def api_carrier_availability(request):
drop_pincode=drop_pincode, drop_pincode=drop_pincode,
payment_type=payment_type, payment_type=payment_type,
weight=weight, weight=weight,
env=env, env=env
use_mock=use_mock
) )
return JsonResponse(res, status=200) return JsonResponse(res, status=200)
@ -1044,7 +1040,7 @@ def api_carrier_availability(request):
@csrf_exempt @csrf_exempt
def api_carrier_auto_dispatch(request): def api_carrier_auto_dispatch(request):
""" """
Unified REST API endpoint to automatically check carrier availability and dispatch order to the optimal carrier. Unified REST API endpoint to automatically check live carrier availability and dispatch order to the optimal carrier.
POST /api/v1/carrier/auto-dispatch/ POST /api/v1/carrier/auto-dispatch/
""" """
if request.method != 'POST': if request.method != 'POST':
@ -1058,8 +1054,7 @@ def api_carrier_auto_dispatch(request):
selection_strategy = data.get('selection_strategy', 'CHEAPEST') selection_strategy = data.get('selection_strategy', 'CHEAPEST')
preferred_carrier = data.get('preferred_carrier', 'SHADOWFAX') preferred_carrier = data.get('preferred_carrier', 'SHADOWFAX')
allow_fallback = data.get('allow_fallback', True) allow_fallback = data.get('allow_fallback', True)
env = data.get('env', 'staging') env = data.get('env', 'production')
use_mock = data.get('use_mock', True)
payload = data.get('payload', data) payload = data.get('payload', data)
from .carrier_allocator import dispatch_auto_allocated_order from .carrier_allocator import dispatch_auto_allocated_order
@ -1068,10 +1063,10 @@ def api_carrier_auto_dispatch(request):
selection_strategy=selection_strategy, selection_strategy=selection_strategy,
preferred_carrier=preferred_carrier, preferred_carrier=preferred_carrier,
allow_fallback=allow_fallback, allow_fallback=allow_fallback,
env=env, env=env
use_mock=use_mock
) )
return JsonResponse(res_data, status=status_code) return JsonResponse(res_data, status=status_code)

View file

@ -40,8 +40,10 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'shipping', 'shipping',
'ekart', 'ekart',
'delhivery',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware',

View file

@ -5,4 +5,6 @@ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', include('shipping.urls')), path('', include('shipping.urls')),
path('ekart/', include('ekart.urls')), path('ekart/', include('ekart.urls')),
path('delhivery/', include('delhivery.urls')),
] ]