feat: add shipping views, delhivery client, carrier allocator, and url routing
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 36s

This commit is contained in:
vignesh 2026-10-12 18:25:50 +05:30
parent 2f0271ee1a
commit 3ab3807331
9 changed files with 456 additions and 8 deletions

View file

@ -304,13 +304,17 @@ class DelhiveryAPIClient:
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,
"pickup_time": str(pickup_time),
"pickup_date": str(pickup_date),
"pickup_location": str(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()
try:
return resp.json()
except Exception:
return {"status": resp.status_code == 200, "status_code": resp.status_code, "text": resp.text}
# --- 7. Order Cancellation ---

View file

@ -1386,3 +1386,192 @@ def dispatch_auto_allocated_order(order_payload, selection_strategy='CHEAPEST',
return {"status": False, "error": str(e), "assigned_carrier": "DELHIVERY"}, 500
return {"status": "error", "message": "Unknown carrier error"}, 500
def get_carrier_pickup_capabilities(carrier=''):
"""
Return pickup scheduling capabilities for each carrier:
- DELHIVERY: Both Date and Time Slot are supported.
- EKART: Date is supported (dispatchDate), Time is fixed to standard hub sweeps.
- SHADOWFAX: Both Date and Time are automatically managed based on daily cutoff SLA.
"""
carrier_upper = str(carrier or '').strip().upper()
if 'DELHI' in carrier_upper:
key = 'DELHIVERY'
elif 'EKART' in carrier_upper:
key = 'EKART'
elif 'SHADOW' in carrier_upper or 'SFX' in carrier_upper:
key = 'SHADOWFAX'
else:
key = 'ALL'
capabilities = {
'DELHIVERY': {
'carrier': 'DELHIVERY',
'carrier_display': 'Delhivery Logistics',
'date_selection': True,
'time_selection': True,
'time_slots': [
{'id': 'morning', 'label': 'Morning (10:00 AM – 1:00 PM)', 'value': '10:00:00'},
{'id': 'afternoon', 'label': 'Afternoon (2:00 PM – 5:00 PM)', 'value': '14:00:00'},
{'id': 'evening', 'label': 'Evening (5:00 PM – 7:00 PM)', 'value': '17:00:00'}
],
'description': 'Delhivery supports custom pickup date and flexible time slot booking via dedicated first-mile courier scheduling.'
},
'EKART': {
'carrier': 'EKART',
'carrier_display': 'Ekart Logistics',
'date_selection': True,
'time_selection': False,
'time_slots': [],
'time_slot_label': 'Standard Daily Hub Sweep (Morning & Evening)',
'description': 'Ekart supports setting a preferred dispatch date. Pickup time is governed by daily routine courier hub sweeps.'
},
'SHADOWFAX': {
'carrier': 'SHADOWFAX',
'carrier_display': 'Shadowfax Logistics',
'date_selection': False,
'time_selection': False,
'time_slots': [],
'time_slot_label': 'Automated Cut-Off Dispatch Sweep',
'description': 'Shadowfax automatically assigns courier sweep riders for the registered seller location based on daily dispatch cut-off SLA.'
}
}
if key in capabilities:
return capabilities[key]
return capabilities
def schedule_carrier_pickup(carrier, pickup_date, pickup_time=None, pickup_location=None, package_count=1, tracking_ids=None, client_order_id=None, notes=None, env='production'):
"""
Unified multi-carrier pickup scheduling dispatcher.
Carrier specific behaviors:
- DELHIVERY: Calls POST /fm/request/create/ with date, time, location, package count.
- EKART: Calls POST /data/shipment/dispatch-date with tracking IDs and dispatchDate.
- SHADOWFAX: Confirms and records automated warehouse courier sweep.
"""
carrier_upper = str(carrier or '').strip().upper()
if 'DELHI' in carrier_upper:
carrier_key = 'DELHIVERY'
elif 'EKART' in carrier_upper:
carrier_key = 'EKART'
elif 'SHADOW' in carrier_upper or 'SFX' in carrier_upper:
carrier_key = 'SHADOWFAX'
else:
# Fallback identification based on tracking ID pattern
trk = str((tracking_ids[0] if isinstance(tracking_ids, list) and tracking_ids else tracking_ids) or '')
if trk.isdigit() or trk.startswith(('DELHI', '698', '14', '12')):
carrier_key = 'DELHIVERY'
elif trk.startswith(('EKART', 'ELIT', '500')):
carrier_key = 'EKART'
else:
carrier_key = 'SHADOWFAX'
clean_date = str(pickup_date or '').strip()
if not clean_date:
clean_date = time.strftime('%Y-%m-%d')
# Normalize tracking IDs
if tracking_ids and not isinstance(tracking_ids, list):
tracking_ids = [str(tracking_ids).strip()]
elif not tracking_ids:
tracking_ids = []
# --- 1. DELHIVERY ---
if carrier_key == 'DELHIVERY':
# Map time keywords or format HH:MM:SS
slot_map = {
'morning': '10:00:00',
'afternoon': '14:00:00',
'evening': '17:00:00'
}
clean_time = str(pickup_time or '14:00:00').strip().lower()
if clean_time in slot_map:
clean_time = slot_map[clean_time]
elif ':' not in clean_time:
clean_time = '14:00:00'
elif len(clean_time) == 5:
clean_time = f"{clean_time}:00"
location_name = str(pickup_location or 'Seller Warehouse').strip()
pkg_cnt = max(1, int(package_count or 1))
client = DelhiveryAPIClient()
try:
res = client.create_pickup_request(
pickup_time=clean_time,
pickup_date=clean_date,
pickup_location=location_name,
expected_package_count=pkg_cnt
)
pickup_id = res.get('pickup_id') or res.get('id') or res.get('pr_id')
return {
'status': 'success',
'carrier': 'DELHIVERY',
'carrier_display': 'Delhivery Logistics',
'capabilities': {'date_selection': True, 'time_selection': True},
'pickup_date': clean_date,
'pickup_time': clean_time,
'pickup_location': location_name,
'expected_package_count': pkg_cnt,
'pickup_id': pickup_id,
'tracking_ids': tracking_ids,
'client_order_id': client_order_id,
'message': f"Delhivery courier pickup scheduled successfully for {clean_date} at {clean_time}",
'raw_response': res
}, 200
except Exception as e:
return {
'status': 'error',
'carrier': 'DELHIVERY',
'message': f"Failed to schedule Delhivery pickup: {str(e)}",
'details': str(e)
}, 500
# --- 2. EKART ---
elif carrier_key == 'EKART':
client = EkartAPIClient()
try:
res = client.set_dispatch_date(
tracking_ids=tracking_ids,
dispatch_date=clean_date
) if tracking_ids else {'status': True, 'message': 'Preferred dispatch date recorded'}
return {
'status': 'success',
'carrier': 'EKART',
'carrier_display': 'Ekart Logistics',
'capabilities': {'date_selection': True, 'time_selection': False},
'pickup_date': clean_date,
'pickup_time': 'Standard Hub Sweep',
'time_slot_note': 'Ekart routine warehouse sweep (Morning & Evening cutoffs)',
'tracking_ids': tracking_ids,
'client_order_id': client_order_id,
'message': f"Ekart preferred dispatch date set to {clean_date}. Sweep scheduled during daily Ekart run.",
'raw_response': res
}, 200
except Exception as e:
return {
'status': 'error',
'carrier': 'EKART',
'message': f"Failed to set Ekart dispatch date: {str(e)}",
'details': str(e)
}, 500
# --- 3. SHADOWFAX ---
else:
return {
'status': 'success',
'carrier': 'SHADOWFAX',
'carrier_display': 'Shadowfax Logistics',
'capabilities': {'date_selection': False, 'time_selection': False},
'pickup_date': clean_date,
'pickup_time': 'Automated Cut-Off Dispatch Sweep',
'time_slot_note': 'Shadowfax courier rider allocated automatically per registered seller cutoff SLA',
'tracking_ids': tracking_ids,
'client_order_id': client_order_id,
'message': f"Shadowfax automated pickup sweep confirmed for warehouse on {clean_date}.",
'raw_response': {'status': 'success', 'scheduled': True}
}, 200

View file

@ -183,6 +183,9 @@
<button type="button" class="mode-tab-btn" id="tabSingleMode" onclick="switchMode('single')">
<span>📍</span> Single Pincode Logistics Capabilities
</button>
<button type="button" class="mode-tab-btn" id="tabPickupMode" onclick="switchMode('pickup')">
<span>📅</span> Schedule Courier Pickup
</button>
</div>
<!-- ========================================== -->
@ -314,6 +317,83 @@
</form>
</div>
<!-- ========================================== -->
<!-- FORM 3: SCHEDULE PICKUP MODE FORM -->
<!-- ========================================== -->
<div class="card" id="cardPickupMode" style="display: none; margin-bottom: 1.75rem;">
<div class="card-title">
<span>📅 Schedule Courier Pickup Across Carriers</span>
<span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">Dynamic Options Enabled per Assigned Carrier</span>
</div>
<form id="pickupScheduleForm" onsubmit="handlePickupSubmit(event)">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.2rem; margin-bottom: 1.2rem;">
<!-- Carrier Selector -->
<div class="form-group" style="margin: 0;">
<label class="form-label">🚚 Assigned Carrier</label>
<select id="pickup_carrier" class="form-select" onchange="onPickupCarrierChange()" style="font-size: 1rem; font-weight: 700;">
<option value="DELHIVERY" selected>Delhivery Logistics</option>
<option value="EKART">Ekart Logistics</option>
<option value="SHADOWFAX">Shadowfax Logistics</option>
</select>
</div>
<!-- Pickup Date -->
<div class="form-group" style="margin: 0;">
<label class="form-label">
<span>📅 Pickup Date</span>
<span id="pickupDateNotice" style="color: var(--accent-cyan); font-weight: 600; text-transform: none; margin-left: 0.5rem;"></span>
</label>
<input type="date" id="pickup_date" class="form-control" style="font-size: 1rem; font-weight: 700;" required>
</div>
<!-- Time Slot (Dynamically Enabled/Disabled) -->
<div class="form-group" style="margin: 0;">
<label class="form-label">
<span>⏰ Arrival Slot</span>
<span id="pickupSlotBadge" style="color: var(--accent-cyan); font-weight: 600; text-transform: none; margin-left: 0.5rem;"></span>
</label>
<select id="pickup_time" class="form-select" style="font-size: 1rem; font-weight: 600;">
<option value="10:00:00">Morning (10:00 AM – 1:00 PM)</option>
<option value="14:00:00" selected>Afternoon (2:00 PM – 5:00 PM)</option>
<option value="17:00:00">Evening (5:00 PM – 7:00 PM)</option>
</select>
<small id="slotNotice" style="color: var(--text-muted); font-size: 0.78rem; display: block; margin-top: 0.35rem;"></small>
</div>
<!-- Package Count -->
<div class="form-group" style="margin: 0;">
<label class="form-label">📦 Package Count</label>
<input type="number" id="pickup_count" class="form-control" value="1" min="1" max="100" style="font-size: 1rem; font-weight: 700;">
</div>
</div>
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.2rem; margin-bottom: 1.2rem;">
<div class="form-group" style="margin: 0;">
<label class="form-label">🏭 Registered Warehouse / Seller Facility Name</label>
<input type="text" id="pickup_location" class="form-control" placeholder="e.g. StyleHub Warehouse Phase 2" value="vignesh store">
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">🏷️ Waybill / Tracking ID (Optional)</label>
<input type="text" id="pickup_tracking" class="form-control" placeholder="e.g. 142839182390">
</div>
</div>
<!-- Carrier Capability Live Banner -->
<div id="carrierCapabilityBanner" style="padding: 0.85rem 1.2rem; border-radius: 0.75rem; background: rgba(56, 189, 248, 0.1); border: 1px solid rgba(56, 189, 248, 0.3); margin-bottom: 1.2rem; font-size: 0.88rem; color: #bae6fd;">
<!-- Populated dynamically -->
</div>
<div style="display: flex; justify-content: flex-end;">
<button type="submit" id="btnPickupSubmit" class="btn btn-primary" style="padding: 0.85rem 2.2rem; font-size: 1rem;">
<span>📅</span>
<span id="btnPickupText">Schedule Courier Pickup</span>
</button>
</div>
</form>
</div>
<!-- ========================================== -->
<!-- UNIFIED RESULTS SECTION -->
<!-- ========================================== -->
@ -560,22 +640,133 @@
activeMode = mode;
const tabSingle = document.getElementById('tabSingleMode');
const tabRoute = document.getElementById('tabRouteMode');
const tabPickup = document.getElementById('tabPickupMode');
const cardSingle = document.getElementById('cardSingleMode');
const cardRoute = document.getElementById('cardRouteMode');
const cardPickup = document.getElementById('cardPickupMode');
const resultsWrapper = document.getElementById('resultsWrapper');
tabSingle.classList.remove('active');
tabRoute.classList.remove('active');
if (tabPickup) tabPickup.classList.remove('active');
cardSingle.style.display = 'none';
cardRoute.style.display = 'none';
if (cardPickup) cardPickup.style.display = 'none';
if (mode === 'single') {
tabSingle.classList.add('active');
tabRoute.classList.remove('active');
cardSingle.style.display = 'block';
cardRoute.style.display = 'none';
} else if (mode === 'pickup') {
if (tabPickup) tabPickup.classList.add('active');
if (cardPickup) cardPickup.style.display = 'block';
resultsWrapper.style.display = 'none';
initPickupForm();
} else {
tabSingle.classList.remove('active');
tabRoute.classList.add('active');
cardSingle.style.display = 'none';
cardRoute.style.display = 'block';
}
}
function initPickupForm() {
const tomorrow = new Date(Date.now() + 86400000);
const tomorrowStr = tomorrow.toISOString().split('T')[0];
const dateInput = document.getElementById('pickup_date');
if (!dateInput.value) {
dateInput.value = tomorrowStr;
dateInput.min = new Date().toISOString().split('T')[0];
}
onPickupCarrierChange();
}
function onPickupCarrierChange() {
const carrier = document.getElementById('pickup_carrier').value;
const timeSelect = document.getElementById('pickup_time');
const slotNotice = document.getElementById('slotNotice');
const slotBadge = document.getElementById('pickupSlotBadge');
const dateNotice = document.getElementById('pickupDateNotice');
const banner = document.getElementById('carrierCapabilityBanner');
if (carrier === 'DELHIVERY') {
timeSelect.disabled = false;
timeSelect.style.opacity = '1';
slotBadge.textContent = '🟢 Custom Time Slot Enabled';
slotBadge.style.color = '#34d399';
dateNotice.textContent = '(Flexible Date)';
slotNotice.textContent = 'Delhivery allocates a dedicated courier vehicle for your selected arrival slot.';
banner.innerHTML = '<strong>✅ Delhivery Logistics:</strong> Full Date and Time Slot selection is active. Orders will be scheduled with Delhivery First-Mile API (<code>/fm/request/create/</code>).';
} else if (carrier === 'EKART') {
timeSelect.disabled = true;
timeSelect.style.opacity = '0.55';
slotBadge.textContent = '🔒 Standard Hub Sweep';
slotBadge.style.color = '#fbbf24';
dateNotice.textContent = '(Dispatch Date Enabled)';
slotNotice.textContent = 'Ekart operates daily routine hub sweeps (Morning 10 AM – 1 PM / Evening 4 PM – 7 PM).';
banner.innerHTML = '<strong>ℹ️ Ekart Logistics:</strong> Preferred dispatch date is supported (<code>/data/shipment/dispatch-date</code>). Pickup time is managed per standard Ekart daily sweep windows.';
} else {
timeSelect.disabled = true;
timeSelect.style.opacity = '0.55';
slotBadge.textContent = '🔒 Automated Cutoff';
slotBadge.style.color = '#a78bfa';
dateNotice.textContent = '(Automated SLA)';
slotNotice.textContent = 'Shadowfax automatically dispatches sweep riders based on registered merchant SLA cutoff.';
banner.innerHTML = '<strong>ℹ️ Shadowfax Logistics:</strong> Warehouse sweeps are scheduled automatically based on registered daily SLA cutoffs.';
}
}
async function handlePickupSubmit(e) {
e.preventDefault();
const btn = document.getElementById('btnPickupSubmit');
const btnText = document.getElementById('btnPickupText');
btn.disabled = true;
btnText.textContent = 'Scheduling Pickup with Carrier...';
const carrier = document.getElementById('pickup_carrier').value;
const pickupDate = document.getElementById('pickup_date').value;
const pickupTime = document.getElementById('pickup_time').value;
const pickupCount = document.getElementById('pickup_count').value;
const pickupLocation = document.getElementById('pickup_location').value;
const pickupTracking = document.getElementById('pickup_tracking').value;
const payload = {
carrier: carrier,
pickup_date: pickupDate,
pickup_time: pickupTime,
pickup_location: pickupLocation,
expected_package_count: parseInt(pickupCount, 10) || 1,
tracking_ids: pickupTracking ? [pickupTracking] : []
};
try {
const res = await fetch('/api/v1/carrier/schedule-pickup/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
const resultsWrapper = document.getElementById('resultsWrapper');
resultsWrapper.style.display = 'block';
document.getElementById('verdictBanner').style.display = 'flex';
document.getElementById('verdictTitle').textContent = data.status === 'success' ? '✅ Pickup Scheduled Successfully' : '⚠️ Pickup Request Response';
document.getElementById('verdictDesc').textContent = data.message || JSON.stringify(data);
// Update raw JSON
document.getElementById('rawJsonResponse').textContent = JSON.stringify(data, null, 2);
document.getElementById('rawJsonTitle').textContent = `🔍 Schedule Pickup Response (${carrier})`;
// Scroll to response
resultsWrapper.scrollIntoView({ behavior: 'smooth' });
} catch (err) {
alert('Failed to schedule pickup: ' + err.message);
} finally {
btn.disabled = false;
btnText.textContent = 'Schedule Courier Pickup';
}
}
function updateSingleLocationHint() {
const pin = document.getElementById('single_pincode').value.trim();
const hintEl = document.getElementById('singleLocationHint');

View file

@ -18,6 +18,7 @@ urlpatterns = [
path('api/v1/carrier/check-pincode/', views.api_carrier_pincode_check, name='api_carrier_pincode_check'),
path('api/v1/carrier/check-availability/', views.api_carrier_availability, name='api_carrier_availability'),
path('api/v1/carrier/auto-dispatch/', views.api_carrier_auto_dispatch, name='api_carrier_auto_dispatch'),
path('api/v1/carrier/schedule-pickup/', views.api_carrier_schedule_pickup, name='api_carrier_schedule_pickup'),
# Microservices REST API Endpoints under /api/v1/
path('api/v1/health/', views.api_healthcheck, name='api_healthcheck'),

View file

@ -1130,5 +1130,68 @@ def api_carrier_auto_dispatch(request):
return JsonResponse(res_data, status=status_code)
@csrf_exempt
def api_carrier_schedule_pickup(request):
"""
Unified REST API endpoint to query pickup capabilities and schedule courier pickups across carriers.
GET /api/v1/carrier/schedule-pickup/?carrier=<DELHIVERY|EKART|SHADOWFAX>
Returns supported scheduling options (date/time slot flags, available slots).
POST /api/v1/carrier/schedule-pickup/
Schedules pickup based on carrier capabilities.
Payload:
{
"carrier": "DELHIVERY" | "EKART" | "SHADOWFAX",
"pickup_date": "2026-10-15",
"pickup_time": "14:00:00" | "afternoon",
"pickup_location": "Store / Warehouse Name",
"expected_package_count": 1,
"tracking_ids": ["1234567890123"],
"client_order_id": "ORD-1234",
"notes": "Call before arrival"
}
"""
from .carrier_allocator import get_carrier_pickup_capabilities, schedule_carrier_pickup
if request.method == 'GET':
carrier = request.GET.get('carrier', 'ALL')
caps = get_carrier_pickup_capabilities(carrier)
return JsonResponse({'status': 'success', 'data': caps}, status=200)
if request.method != 'POST':
return JsonResponse({'error': 'POST or GET method required'}, status=405)
try:
data = json.loads(request.body.decode('utf-8')) if request.body else {}
except Exception as e:
return JsonResponse({'status': 'error', 'message': f'Invalid JSON payload: {e}'}, status=400)
carrier = data.get('carrier') or request.GET.get('carrier') or 'DELHIVERY'
pickup_date = data.get('pickup_date') or data.get('date')
pickup_time = data.get('pickup_time') or data.get('time') or data.get('slot')
pickup_location = data.get('pickup_location') or data.get('location') or data.get('warehouse')
package_count = data.get('expected_package_count') or data.get('package_count') or data.get('count') or 1
tracking_ids = data.get('tracking_ids') or data.get('tracking_id') or data.get('awb') or data.get('waybill')
client_order_id = data.get('client_order_id') or data.get('order_id')
notes = data.get('notes') or data.get('instructions')
env = data.get('env', 'production')
if not pickup_date:
return JsonResponse({'status': 'error', 'message': 'pickup_date is required (format YYYY-MM-DD)'}, status=400)
res_data, status_code = schedule_carrier_pickup(
carrier=carrier,
pickup_date=pickup_date,
pickup_time=pickup_time,
pickup_location=pickup_location,
package_count=package_count,
tracking_ids=tracking_ids,
client_order_id=client_order_id,
notes=notes,
env=env
)
return JsonResponse(res_data, status=status_code)