240 lines
9.6 KiB
Python
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()
|