feat: add Delhivery shipping service integration and API documentation
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 31s
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 31s
This commit is contained in:
parent
4468ab0c4f
commit
6e3bbee5c8
30 changed files with 1552 additions and 143 deletions
1
delhivery/__init__.py
Normal file
1
delhivery/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Delhivery Logistics Integration Module
|
||||||
BIN
delhivery/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
delhivery/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
delhivery/__pycache__/apps.cpython-314.pyc
Normal file
BIN
delhivery/__pycache__/apps.cpython-314.pyc
Normal file
Binary file not shown.
BIN
delhivery/__pycache__/client.cpython-314.pyc
Normal file
BIN
delhivery/__pycache__/client.cpython-314.pyc
Normal file
Binary file not shown.
BIN
delhivery/__pycache__/models.cpython-314.pyc
Normal file
BIN
delhivery/__pycache__/models.cpython-314.pyc
Normal file
Binary file not shown.
BIN
delhivery/__pycache__/urls.cpython-314.pyc
Normal file
BIN
delhivery/__pycache__/urls.cpython-314.pyc
Normal file
Binary file not shown.
BIN
delhivery/__pycache__/views.cpython-314.pyc
Normal file
BIN
delhivery/__pycache__/views.cpython-314.pyc
Normal file
Binary file not shown.
6
delhivery/apps.py
Normal file
6
delhivery/apps.py
Normal 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
240
delhivery/client.py
Normal 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()
|
||||||
69
delhivery/migrations/0001_initial.py
Normal file
69
delhivery/migrations/0001_initial.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
delhivery/migrations/__init__.py
Normal file
0
delhivery/migrations/__init__.py
Normal file
BIN
delhivery/migrations/__pycache__/0001_initial.cpython-314.pyc
Normal file
BIN
delhivery/migrations/__pycache__/0001_initial.cpython-314.pyc
Normal file
Binary file not shown.
BIN
delhivery/migrations/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
delhivery/migrations/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
85
delhivery/models.py
Normal file
85
delhivery/models.py
Normal 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
6
delhivery/urls.py
Normal 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
92
delhivery/views.py
Normal 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)
|
||||||
362
documentation/delhivery/b2c_api_spec.md
Normal file
362
documentation/delhivery/b2c_api_spec.md
Normal 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
104
scratch/bundle.js
Normal file
File diff suppressed because one or more lines are too long
29
scratch/fetch_delhivery.py
Normal file
29
scratch/fetch_delhivery.py
Normal 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
387
scratch/parse_delhivery.py
Normal 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")
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue