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 c3868fc..0551ffa 100644 Binary files a/shipping/__pycache__/carrier_allocator.cpython-314.pyc and b/shipping/__pycache__/carrier_allocator.cpython-314.pyc differ diff --git a/shipping/__pycache__/sfx_client.cpython-314.pyc b/shipping/__pycache__/sfx_client.cpython-314.pyc index 5a09724..f33b251 100644 Binary files a/shipping/__pycache__/sfx_client.cpython-314.pyc and b/shipping/__pycache__/sfx_client.cpython-314.pyc differ diff --git a/shipping/__pycache__/tests.cpython-314.pyc b/shipping/__pycache__/tests.cpython-314.pyc index 1367802..dc7f942 100644 Binary files a/shipping/__pycache__/tests.cpython-314.pyc and b/shipping/__pycache__/tests.cpython-314.pyc differ diff --git a/shipping/__pycache__/urls.cpython-314.pyc b/shipping/__pycache__/urls.cpython-314.pyc index d682b10..a60c066 100644 Binary files a/shipping/__pycache__/urls.cpython-314.pyc and b/shipping/__pycache__/urls.cpython-314.pyc differ diff --git a/shipping/__pycache__/views.cpython-314.pyc b/shipping/__pycache__/views.cpython-314.pyc index bc8576f..fec5d26 100644 Binary files a/shipping/__pycache__/views.cpython-314.pyc and b/shipping/__pycache__/views.cpython-314.pyc differ 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