shippingservice/delhivery/client.py
vignesh 6e3bbee5c8
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 31s
feat: add Delhivery shipping service integration and API documentation
2026-10-10 19:39:18 +05:30

240 lines
9.6 KiB
Python

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()