From e327ac8988b493b8d273da8e6fe5bd88152dec26 Mon Sep 17 00:00:00 2001 From: vignesh Date: Sun, 11 Oct 2026 20:49:03 +0530 Subject: [PATCH] feat: add shipping service application with forward and reverse logistics, carrier allocation, and dashboard templates --- README.md | 65 +- ekart/templates/ekart/dashboard.html | 2 +- .../carrier_allocator.cpython-314.pyc | Bin 26100 -> 33314 bytes .../__pycache__/sfx_client.cpython-314.pyc | Bin 4406 -> 4282 bytes shipping/__pycache__/tests.cpython-314.pyc | Bin 16015 -> 18067 bytes shipping/__pycache__/urls.cpython-314.pyc | Bin 1879 -> 1975 bytes shipping/__pycache__/views.cpython-314.pyc | Bin 58574 -> 59328 bytes shipping/carrier_allocator.py | 357 +++++++-- shipping/sfx_client.py | 5 +- shipping/templates/shipping/base.html | 3 + .../templates/shipping/carrier_checker.html | 675 ++++++++++++++++++ shipping/templates/shipping/forward.html | 21 +- shipping/templates/shipping/reverse.html | 21 +- shipping/tests.py | 36 + shipping/urls.py | 1 + shipping/views.py | 65 +- .../__pycache__/settings.cpython-314.pyc | Bin 5192 -> 4999 bytes shippingservice_project/settings.py | 2 - 18 files changed, 1121 insertions(+), 132 deletions(-) create mode 100644 shipping/templates/shipping/carrier_checker.html diff --git a/README.md b/README.md index bdbc656..8960690 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ sequenceDiagram - **Forward Logistics**: Pincode serviceability checks, pre-allocated AWB numbers, Marketplace seller pickup orders, Warehouse hub dispatch orders, V4 real-time tracking, order cancellations, support escalations, digital Proof of Delivery (POD), and dynamic doorstep COD UPI QR code generation. - **Reverse Logistics**: Reverse pincode serviceability, reverse AWB pre-allocation, customer doorstep return requests with configurable **Doorstep Quality Check (QC)** rules (brand match, IMEI/serial number validation, physical condition, photo requirements), V4 reverse tracking, and Proof of Pickup (POP). - **Automated Audit Logging**: Every outbound request to Shadowfax and every incoming webhook callback is stored in PostgreSQL (`ActivityLog`) capturing headers, request payload, status code, response time in ms, and response payload. -- **Visual Testing Dashboard**: Clean dark-mode web interface at `/forward/`, `/reverse/`, `/logs/`, and `/webhooks/` for interactive debugging and live log inspection. +- **Visual Testing Dashboard**: Clean dark-mode web interface at `/checker/` (Multi-Carrier Route Checker), `/forward/`, `/reverse/`, `/label/`, `/logs/`, and `/webhooks/` for interactive debugging and live inspection. --- @@ -55,9 +55,10 @@ External services consume these REST API endpoints using JSON payloads over HTTP ### 0. Dynamic Carrier Allocation & Routing APIs -#### 0.1 Check All Carrier Availability -Evaluate pincode serviceability and COD availability across all integrated carriers (Shadowfax & Ekart) in parallel. +#### 0.1 Check All Carrier Availability (First-Mile & Last-Mile) +Evaluate first-mile origin pickup hub and last-mile destination drop serviceability, COD availability, and freight rates across all integrated carriers (Shadowfax, Delhivery, Ekart) in parallel. +- **Interactive UI**: `http://localhost:8000/checker/` (or `https://beta-shipping.tradhox.com/checker/`) - **Endpoint**: `POST /api/v1/carrier/check-availability/` - **Request Body**: ```json @@ -66,7 +67,7 @@ Evaluate pincode serviceability and COD availability across all integrated carri "drop_pincode": "110009", "payment_type": "Prepaid", "weight": 500, - "use_mock": true + "use_mock": false } ``` - **Response (`200 OK`)**: @@ -76,9 +77,57 @@ Evaluate pincode serviceability and COD availability across all integrated carri "pickup_pincode": "560077", "drop_pincode": "110009", "payment_type": "Prepaid", + "best_carrier": { + "selected": true, + "carrier": "DELHIVERY", + "cost": 35.0, + "reason": "Selected DELHIVERY as lowest cost carrier (Cost: ₹35.0)" + }, "carriers": { - "SHADOWFAX": { "carrier": "SHADOWFAX", "serviceable": true, "cod_available": true }, - "EKART": { "carrier": "EKART", "serviceable": true, "cod_available": true } + "SHADOWFAX": { + "carrier": "SHADOWFAX", + "serviceable": true, + "cod_available": true, + "estimated_cost": 60.0, + "first_mile": { + "serviceable": true, + "pincode": "560077", + "city": "Bengaluru", + "state": "Karnataka", + "hub_active": true, + "message": "First-mile pickup serviceable in Bengaluru, Karnataka" + }, + "last_mile": { + "serviceable": true, + "pincode": "110009", + "city": "Delhi", + "state": "Delhi", + "cod_available": true, + "message": "Last-mile delivery serviceable in Delhi, Delhi" + } + }, + "DELHIVERY": { + "carrier": "DELHIVERY", + "serviceable": true, + "cod_available": true, + "estimated_cost": 35.0, + "first_mile": { + "serviceable": true, + "pincode": "560077", + "city": "Bengaluru", + "state": "Karnataka", + "reverse_pickup": true, + "message": "First-mile pickup serviceable in Bengaluru, Karnataka" + }, + "last_mile": { + "serviceable": true, + "pincode": "110009", + "city": "North Delhi", + "state": "Delhi", + "cod_available": true, + "message": "Last-mile delivery serviceable in North Delhi, Delhi" + } + } } } ``` @@ -146,7 +195,7 @@ Determine if Shadowfax serves specified destination/pickup pincodes for Express { "pincodes": "560077,110009", "service": "express", - "env": "staging", + "env": "production", "use_mock": true } ``` @@ -182,7 +231,7 @@ Create a new forward delivery order (Marketplace Seller Pickup or Warehouse Hub - **Request Body Example**: ```json { - "env": "staging", + "env": "production", "use_mock": true, "payload": { "order_details": { diff --git a/ekart/templates/ekart/dashboard.html b/ekart/templates/ekart/dashboard.html index c07231f..f2c461b 100644 --- a/ekart/templates/ekart/dashboard.html +++ b/ekart/templates/ekart/dashboard.html @@ -19,7 +19,7 @@
🌐 Ekart API Host & Credentials - Production / Staging Base: https://app.elite.ekartlogistics.in + Production Base: https://app.elite.ekartlogistics.in
diff --git a/shipping/__pycache__/carrier_allocator.cpython-314.pyc b/shipping/__pycache__/carrier_allocator.cpython-314.pyc index c3868fc1905f8009a84db2fb32befa22dde28b82..0551ffa6dc94d6cb637c35ec00cc689be9fc6dd8 100644 GIT binary patch delta 12675 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 GBJqFQ6(6Di delta 6019 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 delta 692 zcmdm`xJ`*yn~#@^0SM0OpUj#$kynzjW}>>MMJ&4_Lo7=WTM(-WGf0+!p@cJtD~Mg3 zA&0Sq3&`RCv$%t}gE+w~o*+hL22GxcQ|PN#L}FS_>%nW z)Vv}Qpg7;;J|-DPfz7L#8rTe^Knhv1iu3bsv7{uHBo;}71egOtLvFFTxcaz;xB^`b zQdA@hBBUorbNUH&2WKRv%{fx)iqz!NlGOO(w2JuT#GIU> zFrY4>&0BbGFfk@g?&WV33I_^k@3(iTG#gZ>D2!CN=WK3oJ!okRx$oP%JnURqVY%BoAw5aa@ diff --git a/shipping/__pycache__/tests.cpython-314.pyc b/shipping/__pycache__/tests.cpython-314.pyc index 136780242b56fbecd784efdab734e81200bdb1f7..dc7f94261b6c3f8b53d7c00359f02b5f23228408 100644 GIT binary patch delta 1544 zcma)*OKclO7{_AnqFBOieGzoL4K|=BYi5!7e0*Olz$4ZbWyCm`{oH;Zq6HM0DM&EX!9-hXKFl`z?tN&I z2c!&3b9O&KMvh^???=t}9qtzXyw%UkEM)sk^?)`8mmH#tg{G%1aTaoWmJ1f69yIsu zmd}}>1zMpE0%(U#&;gx?%{dOg%ZV6=eY|YCz#Bf2oW}H46_Ck+rf!;<3H4j7Ie)HZY_IseG zF+FFM@z1RRPLQp%k{kMEYeO!6)asAQRwy*)z(C{5*j*bV)97&$?Qit|YxKU2Kwo24 z&SvPc8MfEBFdufzgI}fBYJ0RTz=w8&2GP>WG8p_iF*ZKPpqI2b^HHu zQB9?%FQw|ziRfMZ3G%RmpBw==)oD3Sj^dxgo{(nw@5#%&$!z(=yMxPvcN5ErFXF4Q zm)AR9S@$LJjLVO2TFfQj+yE_Wprtal+Ida`Z*4Xj5*m2vDZ`KkhM!m&UuZSdx9;sP z+qRs~R3$)`j3m8FeauiW^=Z>9<2Jzb9rzZ3(esAb)w*TIPp$OajSPEPE{~ToxSQ6U| a0e(E3Wgp?xNSo)hnN3t8e=v01DfAak_;)A( delta 138 zcmbQ-%h+Gbr_IO9#Q+3WR;RP>8%*Te!Z>B);a*0b60tCb67e8OWd=>D%}GqzmRy>Q zMIe=ani7*wInSJ&;S$QIzj>dF1|!#Z2?bWu4-7zJ@;BEU!^c2Xj6hs04In~#@^0SMkLJDnB4#=!6x#DM`ZDC6^~jp}omjA9uT8G=+|nFBOn3J`RV zY7nbAGlZ`vSRxvvrp%zJKKUGzkz8^{YI1gJk^U`_t${FN*44F!Q;>;=5UcO@)zLSDc@ziMvP| Gr~&||_$h1v delta 110 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 diff --git a/shipping/carrier_allocator.py b/shipping/carrier_allocator.py index 04dbbbd..3c60c31 100644 --- a/shipping/carrier_allocator.py +++ b/shipping/carrier_allocator.py @@ -203,16 +203,47 @@ def resolve_city_state_by_pincode(pincode, default_city=None, default_state=None return ('Bengaluru', 'Karnataka') -def check_shadowfax_availability(pickup_pincode, drop_pincode, service='express', weight=500, env='production'): +def check_shadowfax_availability(pickup_pincode, drop_pincode, service='express', weight=500, env='production', token=None, use_mock=False): """ Checks Shadowfax pincode serviceability and logistics rate estimation directly from live Shadowfax API. + Provides detailed first-mile (pickup hub) and last-mile (doorstep delivery) status. """ weight_slabs = max(1, int(weight) // 500) estimated_rate = round(45.0 + (weight_slabs * 15.0), 2) + p_city, p_state = resolve_city_state_by_pincode(pickup_pincode) + d_city, d_state = resolve_city_state_by_pincode(drop_pincode) + + if use_mock: + return { + "carrier": "SHADOWFAX", + "serviceable": True, + "cod_available": True, + "estimated_cost": estimated_rate, + "currency": "INR", + "first_mile": { + "serviceable": True, + "pincode": str(pickup_pincode), + "city": p_city, + "state": p_state, + "hub_active": True, + "pickup_type": "Seller Store / Warehouse Sweep", + "message": f"First-mile pickup serviceable in {p_city}, {p_state} (Simulation)" + }, + "last_mile": { + "serviceable": True, + "pincode": str(drop_pincode), + "city": d_city, + "state": d_state, + "cod_available": True, + "delivery_type": "Doorstep Delivery (Express)", + "message": f"Last-mile delivery serviceable in {d_city}, {d_state} (Simulation)" + }, + "details": {"mock": True, "pincodes": [str(pickup_pincode), str(drop_pincode)]} + } base_url = get_base_url(env) - token = get_default_token(env) - headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"} + auth_token = str(token).strip() if token else get_default_token(env) + headers = {"Authorization": f"Token {auth_token}", "Content-Type": "application/json"} pincodes_str = f"{pickup_pincode},{drop_pincode}" sfx_service = "express" if service == "express" else "customer_delivery" url = f"{base_url}/v1/clients/serviceability/?service={sfx_service}&page=1&count=100&pincodes={pincodes_str}" @@ -223,49 +254,149 @@ def check_shadowfax_availability(pickup_pincode, drop_pincode, service='express' parsed = json.loads(res.get('response', '{}')) if isinstance(res.get('response'), str) else res.get('response', {}) data = parsed.get('data', {}) p_list = data.get('pincodes', []) if isinstance(data, dict) else [] + + pickup_info = next((p for p in p_list if str(p.get('pincode')) == str(pickup_pincode)), None) drop_info = next((p for p in p_list if str(p.get('pincode')) == str(drop_pincode)), None) - - if drop_info: - return { - "carrier": "SHADOWFAX", - "serviceable": drop_info.get('serviceable', True), - "cod_available": drop_info.get('cod_available', True), - "estimated_cost": estimated_rate, - "currency": "INR", - "details": drop_info - } + + if str(pickup_pincode) == str(drop_pincode) and p_list: + pickup_info = drop_info = p_list[0] + + fm_ok = pickup_info.get('serviceable', True) if pickup_info else True + lm_ok = drop_info.get('serviceable', True) if drop_info else True + cod_ok = drop_info.get('cod_available', True) if drop_info else True + + fm_city = (pickup_info.get('city') if pickup_info else None) or p_city + fm_state = (pickup_info.get('state') if pickup_info else None) or p_state + lm_city = (drop_info.get('city') if drop_info else None) or d_city + lm_state = (drop_info.get('state') if drop_info else None) or d_state + + overall_serviceable = bool(fm_ok and lm_ok) + + first_mile = { + "serviceable": bool(fm_ok), + "pincode": str(pickup_pincode), + "city": fm_city, + "state": fm_state, + "hub_active": bool(fm_ok), + "pickup_type": "Seller Store / Warehouse Sweep", + "message": f"First-mile pickup serviceable in {fm_city}, {fm_state}" if fm_ok else f"Pickup pincode {pickup_pincode} not serviceable by Shadowfax" + } + + last_mile = { + "serviceable": bool(lm_ok), + "pincode": str(drop_pincode), + "city": lm_city, + "state": lm_state, + "cod_available": bool(cod_ok), + "delivery_type": "Doorstep Delivery (Express)", + "message": f"Last-mile delivery serviceable in {lm_city}, {lm_state}" if lm_ok else f"Destination pincode {drop_pincode} not serviceable by Shadowfax" + } + return { "carrier": "SHADOWFAX", - "serviceable": True, - "cod_available": True, - "estimated_cost": estimated_rate, + "serviceable": overall_serviceable, + "cod_available": bool(cod_ok), + "estimated_cost": estimated_rate if overall_serviceable else None, "currency": "INR", - "details": parsed + "first_mile": first_mile, + "last_mile": last_mile, + "details": drop_info or parsed } else: + first_mile = { + "serviceable": False, + "pincode": str(pickup_pincode), + "city": p_city, + "state": p_state, + "hub_active": False, + "pickup_type": "Seller Store / Warehouse Sweep", + "message": f"SFX API returned status {res.get('status_code')}" + } + last_mile = { + "serviceable": False, + "pincode": str(drop_pincode), + "city": d_city, + "state": d_state, + "cod_available": False, + "delivery_type": "Doorstep Delivery (Express)", + "message": f"SFX API returned status {res.get('status_code')}" + } return { "carrier": "SHADOWFAX", "serviceable": False, "cod_available": False, "estimated_cost": None, + "first_mile": first_mile, + "last_mile": last_mile, "error": f"SFX API returned status {res.get('status_code')}" } except Exception as e: + first_mile = { + "serviceable": False, + "pincode": str(pickup_pincode), + "city": p_city, + "state": p_state, + "hub_active": False, + "pickup_type": "Seller Store / Warehouse Sweep", + "message": str(e) + } + last_mile = { + "serviceable": False, + "pincode": str(drop_pincode), + "city": d_city, + "state": d_state, + "cod_available": False, + "delivery_type": "Doorstep Delivery (Express)", + "message": str(e) + } return { "carrier": "SHADOWFAX", "serviceable": False, "cod_available": False, "estimated_cost": None, + "first_mile": first_mile, + "last_mile": last_mile, "error": str(e) } -def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid', invoice_amount=1000): +def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid', invoice_amount=1000, use_mock=False): """ Checks Ekart pincode serviceability and logistics rate estimation directly from live Ekart API. + Provides detailed first-mile and last-mile breakdown. """ weight_slabs = max(1, int(weight) // 500) estimated_rate = round(38.0 + (weight_slabs * 12.0), 2) + p_city, p_state = resolve_city_state_by_pincode(pickup_pincode) + d_city, d_state = resolve_city_state_by_pincode(drop_pincode) + + if use_mock: + return { + "carrier": "EKART", + "serviceable": True, + "cod_available": True, + "estimated_cost": estimated_rate, + "currency": "INR", + "first_mile": { + "serviceable": True, + "pincode": str(pickup_pincode), + "city": p_city, + "state": p_state, + "hub_active": True, + "pickup_type": "Ekart Hub Sweep", + "message": f"First-mile pickup serviceable in {p_city}, {p_state} (Simulation)" + }, + "last_mile": { + "serviceable": True, + "pincode": str(drop_pincode), + "city": d_city, + "state": d_state, + "cod_available": True, + "delivery_type": "Doorstep Surface/Air Delivery", + "message": f"Last-mile delivery serviceable in {d_city}, {d_state} (Simulation)" + }, + "details": {"mock": True} + } client = EkartAPIClient() try: @@ -303,49 +434,133 @@ def check_ekart_availability(pickup_pincode, drop_pincode, weight=500, payment_t except Exception: pass + first_mile = { + "serviceable": bool(serviceable), + "pincode": str(pickup_pincode), + "city": p_city, + "state": p_state, + "hub_active": bool(serviceable), + "pickup_type": "Ekart Hub Sweep", + "message": f"First-mile pickup serviceable in {p_city}, {p_state}" if serviceable else f"Pickup pincode {pickup_pincode} not serviceable" + } + + last_mile = { + "serviceable": bool(serviceable), + "pincode": str(drop_pincode), + "city": d_city, + "state": d_state, + "cod_available": bool(cod_available), + "delivery_type": "Doorstep Surface/Air Delivery", + "message": f"Last-mile delivery serviceable in {d_city}, {d_state}" if serviceable else f"Destination pincode {drop_pincode} not serviceable" + } + return { "carrier": "EKART", "serviceable": bool(serviceable), "cod_available": bool(cod_available), - "estimated_cost": live_cost, + "estimated_cost": live_cost if serviceable else None, "currency": "INR", + "first_mile": first_mile, + "last_mile": last_mile, "details": res } except Exception as e: + first_mile = { + "serviceable": False, + "pincode": str(pickup_pincode), + "city": p_city, + "state": p_state, + "hub_active": False, + "pickup_type": "Ekart Hub Sweep", + "message": str(e) + } + last_mile = { + "serviceable": False, + "pincode": str(drop_pincode), + "city": d_city, + "state": d_state, + "cod_available": False, + "delivery_type": "Doorstep Surface/Air Delivery", + "message": str(e) + } return { "carrier": "EKART", "serviceable": False, "cod_available": False, "estimated_cost": None, + "first_mile": first_mile, + "last_mile": last_mile, "error": str(e) } -def check_delhivery_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid'): +def check_delhivery_availability(pickup_pincode, drop_pincode, weight=500, payment_type='Prepaid', api_token=None, use_mock=False): """ Checks Delhivery pincode serviceability and logistics rate estimation directly against official Delhivery serviceability matrix (18,699 pincodes) and live Delhivery API. + Provides detailed first-mile (pickup hub) and last-mile (doorstep delivery) status. """ weight_slabs = max(1, int(weight) // 500) estimated_rate = round(35.0 + (weight_slabs * 11.0), 2) is_cod = (str(payment_type).upper() == 'COD') - # Step 1: Validate against official Delhivery serviceability matrix - route_ok, route_msg, drop_info, pickup_info = check_delhivery_route_serviceability( - pickup_pincode=pickup_pincode, - drop_pincode=drop_pincode, - is_cod=is_cod - ) + # Step 1: Validate individual first-mile and last-mile against Delhivery 18,699 database + p_ok, p_msg, pickup_info = is_delhivery_serviceable(pickup_pincode, is_cod=False) + d_ok, d_msg, drop_info = is_delhivery_serviceable(drop_pincode, is_cod=is_cod) - if not route_ok: + p_city = (pickup_info.get('city') if pickup_info else None) or resolve_city_state_by_pincode(pickup_pincode)[0] + p_state = (pickup_info.get('state') if pickup_info else None) or resolve_city_state_by_pincode(pickup_pincode)[1] + p_st_code = pickup_info.get('state_code', '') if pickup_info else '' + + d_city = (drop_info.get('city') if drop_info else None) or resolve_city_state_by_pincode(drop_pincode)[0] + d_state = (drop_info.get('state') if drop_info else None) or resolve_city_state_by_pincode(drop_pincode)[1] + d_st_code = drop_info.get('state_code', '') if drop_info else '' + cod_avail = bool(drop_info.get('cod', True)) if drop_info else False + + first_mile = { + "serviceable": bool(p_ok), + "pincode": str(pickup_pincode), + "city": p_city, + "state": p_state, + "state_code": p_st_code, + "reverse_pickup": bool(pickup_info.get('reverse_pickup', True)) if pickup_info else False, + "pickup_type": "B2C Surface / Express Hub Sweep", + "message": f"First-mile pickup serviceable in {p_city}, {p_state}" if p_ok else f"Pickup: {p_msg}" + } + + last_mile = { + "serviceable": bool(d_ok), + "pincode": str(drop_pincode), + "city": d_city, + "state": d_state, + "state_code": d_st_code, + "cod_available": cod_avail, + "cash_support": bool(drop_info.get('cash', True)) if drop_info else False, + "delivery_type": "Doorstep Express / Surface Delivery", + "message": f"Last-mile delivery serviceable in {d_city}, {d_state}" if d_ok else f"Destination: {d_msg}" + } + + route_serviceable = bool(p_ok and d_ok) + + if not route_serviceable: + err_parts = [] + if not p_ok: + err_parts.append(f"Pickup: {p_msg}") + if not d_ok: + err_parts.append(f"Destination: {d_msg}") return { "carrier": "DELHIVERY", "serviceable": False, - "cod_available": False, + "cod_available": cod_avail, "estimated_cost": None, - "error": route_msg, + "error": " | ".join(err_parts), + "city": d_city, + "state": d_state, + "state_code": d_st_code, + "first_mile": first_mile, + "last_mile": last_mile, "details": { - "route_message": route_msg, + "route_message": " | ".join(err_parts), "drop_info": drop_info, "pickup_info": pickup_info } @@ -358,60 +573,68 @@ def check_delhivery_availability(pickup_pincode, drop_pincode, weight=500, payme "pickup_info": pickup_info } - try: - client = DelhiveryAPIClient() - rate_res = client.get_rate_estimate( - origin_pin=pickup_pincode, - dest_pin=drop_pincode, - weight=weight, - mode="S", - payment_type=payment_type - ) - if isinstance(rate_res, dict) and rate_res.get('total_amount'): - live_cost = float(rate_res['total_amount']) - api_details['rate_estimate'] = rate_res - except Exception as e: - api_details['rate_api_warning'] = str(e) + if not use_mock: + try: + client = DelhiveryAPIClient(api_token=api_token) + rate_res = client.get_rate_estimate( + origin_pin=pickup_pincode, + dest_pin=drop_pincode, + weight=weight, + mode="S", + payment_type=payment_type + ) + if isinstance(rate_res, dict) and rate_res.get('total_amount'): + live_cost = float(rate_res['total_amount']) + api_details['rate_estimate'] = rate_res + except Exception as e: + api_details['rate_api_warning'] = str(e) return { "carrier": "DELHIVERY", "serviceable": True, - "cod_available": bool(drop_info.get('cod', True)), + "cod_available": cod_avail, "estimated_cost": live_cost, "currency": "INR", - "city": drop_info.get('city'), - "state": drop_info.get('state'), - "state_code": drop_info.get('state_code'), + "city": d_city, + "state": d_state, + "state_code": d_st_code, + "first_mile": first_mile, + "last_mile": last_mile, "details": api_details } -def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type='Prepaid', weight=500, env='production'): +def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type='Prepaid', weight=500, env='production', sfx_token=None, delhivery_token=None, use_mock=False): """ - Evaluates serviceability and rate estimates across all registered carriers (Shadowfax, Ekart, Delhivery) via live APIs. + Evaluates serviceability, first-mile, and last-mile delivery across all registered carriers (Shadowfax, Delhivery, Ekart). """ sfx_res = check_shadowfax_availability( pickup_pincode=pickup_pincode, drop_pincode=drop_pincode, weight=weight, - env=env - ) - - ekart_res = check_ekart_availability( - pickup_pincode=pickup_pincode, - drop_pincode=drop_pincode, - weight=weight, - payment_type=payment_type + env=env, + token=sfx_token, + use_mock=use_mock ) delhivery_res = check_delhivery_availability( pickup_pincode=pickup_pincode, drop_pincode=drop_pincode, weight=weight, - payment_type=payment_type + payment_type=payment_type, + api_token=delhivery_token, + use_mock=use_mock ) - return { + ekart_res = check_ekart_availability( + pickup_pincode=pickup_pincode, + drop_pincode=drop_pincode, + weight=weight, + payment_type=payment_type, + use_mock=use_mock + ) + + availability_dict = { "status": "success", "pickup_pincode": str(pickup_pincode), "drop_pincode": str(drop_pincode), @@ -419,11 +642,21 @@ def check_all_carriers_availability(pickup_pincode, drop_pincode, payment_type=' "weight": weight, "carriers": { "SHADOWFAX": sfx_res, - "EKART": ekart_res, - "DELHIVERY": delhivery_res + "DELHIVERY": delhivery_res, + "EKART": ekart_res } } + best_carrier = select_best_carrier( + availability_dict, + selection_strategy='CHEAPEST', + preferred_carrier='SHADOWFAX', + is_cod=(str(payment_type).upper() == 'COD') + ) + availability_dict["best_carrier"] = best_carrier + + return availability_dict + CARRIER_PRIORITY = { 'SHADOWFAX': 0, diff --git a/shipping/sfx_client.py b/shipping/sfx_client.py index c3449e9..4ff4e3e 100644 --- a/shipping/sfx_client.py +++ b/shipping/sfx_client.py @@ -4,11 +4,10 @@ import requests from django.conf import settings from .models import ActivityLog -STAGING_BASE_URL = getattr(settings, 'SFX_STAGING_BASE_URL', 'https://dale.shadowfax.in/api') PRODUCTION_BASE_URL = getattr(settings, 'SFX_PRODUCTION_BASE_URL', 'https://dale.shadowfax.in/api') def get_base_url(env="production"): - return getattr(settings, 'SFX_PRODUCTION_BASE_URL', 'https://dale.shadowfax.in/api').rstrip('/') + return PRODUCTION_BASE_URL.rstrip('/') def get_default_token(env="production"): @@ -56,7 +55,7 @@ def execute_sfx_call(service_type, action_name, method, full_url, headers, paylo response_body_str = json.dumps({ "error": "Upstream Connection Failed", "details": str(e), - "note": "Shadowfax Staging API host may be unreachable directly from your machine or blocked by Akamai WAF. Simulation mode fallback can be enabled." + "note": "Shadowfax Production API host may be unreachable directly from your machine or blocked by Akamai WAF. Simulation mode fallback can be enabled." }, indent=2) status_label = "ERROR" diff --git a/shipping/templates/shipping/base.html b/shipping/templates/shipping/base.html index 7209922..658e48f 100644 --- a/shipping/templates/shipping/base.html +++ b/shipping/templates/shipping/base.html @@ -19,6 +19,9 @@ Multi-Carrier Shipping Platform