feat: implement shipping views, carrier allocation, models, and pickup scheduling architecture
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 39s

This commit is contained in:
vignesh 2026-10-13 19:23:16 +05:30
parent 920ea42092
commit b8ba39c3f8
12 changed files with 617 additions and 3 deletions

View file

@ -153,3 +153,73 @@ class Order(models.Model):
- **Tradhox User Experience**: - **Tradhox User Experience**:
- In Tradhox Supplier Central, once an order is manifested and scheduled, Tradhox surfaces the active **Pickup Request ID (`PR #328095871`)** and **`PICKUP SCHEDULED`** badge. - In Tradhox Supplier Central, once an order is manifested and scheduled, Tradhox surfaces the active **Pickup Request ID (`PR #328095871`)** and **`PICKUP SCHEDULED`** badge.
- Merchants do not need to manually click "Add to Pickup" inside the Delhivery One portal; the courier executive will collect all ready packages for warehouse `SAM`. - Merchants do not need to manually click "Add to Pickup" inside the Delhivery One portal; the courier executive will collect all ready packages for warehouse `SAM`.
---
## 7. Check Pickup Schedule by Tracking ID (AWB) Specification
### 7.1. REST Endpoints
Clients can query the pickup schedule using either endpoint:
- **Primary Route**:
`GET /api/v1/carrier/pickup-schedule/?tracking_id=<AWB>`
- **Unified Alias**:
`GET /api/v1/carrier/schedule-pickup/?tracking_id=<AWB>`
- **POST Route**:
`POST /api/v1/carrier/pickup-schedule/` with JSON `{"tracking_id": "<AWB>"}`
#### Supported Query Parameters
- `tracking_id` / `awb` / `tracking` / `waybill`: The carrier waybill or client order ID (e.g. `69873010000232`).
### 7.2. Response Schema
```json
{
"status": "success",
"tracking_id": "69873010000232",
"order_id": "32-18",
"carrier": "DELHIVERY",
"carrier_display": "Delhivery Logistics",
"pickup_scheduled": true,
"pickup_id": "328095871",
"pickup_date": "2026-10-14",
"pickup_time": "10:00:00",
"pickup_slot": "Morning Slot (10:00 AM – 1:00 PM)",
"pickup_location": "SAM",
"shipment_status": "Manifested",
"pickup_status": "CONFIRMED",
"status_display": "Pickup Confirmed with Delhivery Logistics (PR #328095871) for 2026-10-14 (Morning Slot (10:00 AM – 1:00 PM))",
"destination": {
"city": "Kanchipuram",
"pincode": 600059
},
"consignee": {
"name": "Vignesh Krishnasamy",
"city": "Kanchipuram",
"state": "Tamil Nadu",
"pincode": 600059
},
"scans": [
{
"scan": "Manifested",
"instructions": "Manifest uploaded",
"location": "Mettupalayam_IshwaryNgr_D (Tamil Nadu)",
"timestamp": "2026-10-12T19:04:35.275"
}
],
"details": {
"expected_package_count": 1,
"manifest_date": "2026-10-12T19:04:35.245",
"picked_up_date": null
}
}
```
### 7.3. Resolution Hierarchy
1. **`shipping_pickupschedule` Table**: Checks if a local record exists.
2. **Live Carrier Tracking API**:
- For **Delhivery**, calls `/api/v1/packages/json/?waybill={trk}`, extracting origin warehouse, consignee address, and shipment status.
- Cross-checks with active warehouse pickup requests.
3. **Cross-Database Order Lookup**:
- Queries `api_order` in `customerprofile_beta` to correlate `pickup_id` and ETA slot.
4. **Auto-Caching**: Updates the `PickupSchedule` model so subsequent lookups are instant.

View file

@ -1555,6 +1555,18 @@ def schedule_carrier_pickup(carrier, pickup_date, pickup_time=None, pickup_locat
}, 400 }, 400
pickup_id = res.get('pickup_id') or res.get('id') or res.get('pr_id') pickup_id = res.get('pickup_id') or res.get('id') or res.get('pr_id')
record_pickup_schedule(
tracking_ids=tracking_ids,
carrier='DELHIVERY',
pickup_date=clean_date,
pickup_time=clean_time,
pickup_id=pickup_id,
pickup_location=location_name,
client_order_id=client_order_id,
expected_package_count=pkg_cnt,
notes=notes,
raw_response=res
)
return { return {
'status': 'success', 'status': 'success',
'carrier': 'DELHIVERY', 'carrier': 'DELHIVERY',
@ -1587,6 +1599,16 @@ def schedule_carrier_pickup(carrier, pickup_date, pickup_time=None, pickup_locat
dispatch_date=clean_date dispatch_date=clean_date
) if tracking_ids else {'status': True, 'message': 'Preferred dispatch date recorded'} ) if tracking_ids else {'status': True, 'message': 'Preferred dispatch date recorded'}
record_pickup_schedule(
tracking_ids=tracking_ids,
carrier='EKART',
pickup_date=clean_date,
pickup_time='Standard Hub Sweep',
pickup_location=pickup_location,
client_order_id=client_order_id,
notes=notes,
raw_response=res
)
return { return {
'status': 'success', 'status': 'success',
'carrier': 'EKART', 'carrier': 'EKART',
@ -1610,6 +1632,16 @@ def schedule_carrier_pickup(carrier, pickup_date, pickup_time=None, pickup_locat
# --- 3. SHADOWFAX --- # --- 3. SHADOWFAX ---
else: else:
record_pickup_schedule(
tracking_ids=tracking_ids,
carrier='SHADOWFAX',
pickup_date=clean_date,
pickup_time='Automated Cut-Off Dispatch Sweep',
pickup_location=pickup_location,
client_order_id=client_order_id,
notes=notes,
raw_response={'status': 'success', 'scheduled': True}
)
return { return {
'status': 'success', 'status': 'success',
'carrier': 'SHADOWFAX', 'carrier': 'SHADOWFAX',
@ -1624,3 +1656,277 @@ def schedule_carrier_pickup(carrier, pickup_date, pickup_time=None, pickup_locat
'raw_response': {'status': 'success', 'scheduled': True} 'raw_response': {'status': 'success', 'scheduled': True}
}, 200 }, 200
def record_pickup_schedule(tracking_ids, carrier, pickup_date, pickup_time, pickup_id=None, pickup_location=None, client_order_id=None, expected_package_count=1, notes='', raw_response=None, status='SCHEDULED'):
"""
Saves or updates PickupSchedule records for tracking ID(s) upon scheduling.
"""
try:
from .models import PickupSchedule
carrier_upper = str(carrier or 'DELHIVERY').strip().upper()
carrier_disp = 'Delhivery Logistics' if 'DELHI' in carrier_upper else ('Ekart Logistics' if 'EKART' in carrier_upper else 'Shadowfax Logistics')
slot_label = ''
t_str = str(pickup_time or '').strip()
if t_str.startswith('10'):
slot_label = 'Morning Slot (10:00 AM – 1:00 PM)'
elif t_str.startswith('14'):
slot_label = 'Afternoon Slot (2:00 PM – 5:00 PM / 14:00 – 18:00)'
elif t_str.startswith('17'):
slot_label = 'Evening Slot (5:00 PM – 7:00 PM)'
else:
slot_label = t_str or 'Standard Sweep'
t_list = tracking_ids if isinstance(tracking_ids, list) and tracking_ids else ([str(tracking_ids).strip()] if tracking_ids else [])
if not t_list and client_order_id:
t_list = [f"ORD-{client_order_id}"]
if not t_list:
t_list = [f"LOC-{pickup_location or 'HUB'}-{pickup_date}"]
created_objs = []
for trk in t_list:
if not trk:
continue
obj, _ = PickupSchedule.objects.update_or_create(
tracking_id=str(trk).strip(),
defaults={
'client_order_id': str(client_order_id or '').strip(),
'carrier': carrier_upper,
'carrier_display': carrier_disp,
'pickup_id': str(pickup_id or '').strip(),
'pickup_date': str(pickup_date),
'pickup_time': str(pickup_time),
'pickup_slot': slot_label,
'pickup_location': str(pickup_location or ''),
'status': status,
'expected_package_count': int(expected_package_count or 1),
'notes': str(notes or ''),
'raw_response': raw_response if isinstance(raw_response, dict) else {'data': str(raw_response)}
}
)
created_objs.append(obj)
return created_objs
except Exception as e:
logger.warning(f"Failed to record pickup schedule: {e}")
return []
def check_pickup_schedule_for_tracking(tracking_id):
"""
Checks courier pickup schedule (date, time, arrival window, pickup request ID, location, carrier, status)
for a given tracking / AWB number.
Queries:
1. Local PickupSchedule database model in shippingservice.
2. Live Carrier Tracking API (Delhivery, Ekart, Shadowfax).
3. Cross-database Order records (customerprofile_beta database).
"""
trk = str(tracking_id or '').strip()
if not trk or trk in ('Pending', 'N/A'):
return {
'status': 'error',
'message': 'Valid tracking ID / AWB number is required.'
}, 400
from .models import PickupSchedule
local_obj = PickupSchedule.objects.filter(tracking_id=trk).order_by('-updated_at').first()
if not local_obj:
local_obj = PickupSchedule.objects.filter(client_order_id=trk).order_by('-updated_at').first()
# Identify carrier
if local_obj and local_obj.carrier:
carrier_key = local_obj.carrier
elif trk.isdigit() or trk.startswith(('DELHI', '698', '14', '12', '179', '139')):
carrier_key = 'DELHIVERY'
elif trk.startswith(('EKART', 'ELIT', '500')):
carrier_key = 'EKART'
else:
carrier_key = 'SHADOWFAX'
carrier_display = 'Delhivery Logistics' if carrier_key == 'DELHIVERY' else ('Ekart Logistics' if carrier_key == 'EKART' else 'Shadowfax Logistics')
pickup_id = local_obj.pickup_id if (local_obj and local_obj.pickup_id) else None
pickup_date = local_obj.pickup_date if (local_obj and local_obj.pickup_date) else None
pickup_time = local_obj.pickup_time if (local_obj and local_obj.pickup_time) else None
pickup_slot = local_obj.pickup_slot if (local_obj and local_obj.pickup_slot) else None
pickup_location = local_obj.pickup_location if (local_obj and local_obj.pickup_location) else None
order_id = local_obj.client_order_id if (local_obj and local_obj.client_order_id) else None
shipment_status = 'Manifested'
carrier_live_data = {}
consignee_info = {}
destination_info = {}
scan_history = []
# 1. Query live carrier tracking API if carrier is DELHIVERY
if carrier_key == 'DELHIVERY':
try:
import requests
from delhivery.models import DelhiveryCredential
cred = DelhiveryCredential.objects.filter(is_active=True).first()
tok = cred.api_token if cred and cred.api_token else 'e70dfd63665335ee6e0a1094f6d34e17a126f5a6'
t_resp = requests.get(
f"https://track.delhivery.com/api/v1/packages/json/?waybill={trk}",
headers={"Authorization": f"Token {tok}"},
timeout=6
)
if t_resp.status_code == 200:
s_data = t_resp.json().get('ShipmentData', [])
if s_data and isinstance(s_data, list):
sh = s_data[0].get('Shipment', {})
carrier_live_data = sh
shipment_status = sh.get('Status', {}).get('Status') or 'Manifested'
if not pickup_location and sh.get('PickupLocation'):
pickup_location = sh.get('PickupLocation')
if not order_id and sh.get('ReferenceNo'):
order_id = sh.get('ReferenceNo')
consignee = sh.get('Consignee') or {}
consignee_info = {
'name': consignee.get('Name'),
'city': consignee.get('City'),
'state': consignee.get('State'),
'pincode': consignee.get('PinCode')
}
destination_info = {
'city': sh.get('Destination') or consignee.get('City'),
'pincode': consignee.get('PinCode')
}
for sc in sh.get('Scans', []):
sd = sc.get('ScanDetail', {})
scan_history.append({
'scan': sd.get('Scan'),
'instructions': sd.get('Instructions'),
'location': sd.get('ScannedLocation'),
'timestamp': sd.get('ScanDateTime')
})
except Exception as dlh_err:
logger.warning(f"Error querying Delhivery live tracking for {trk}: {dlh_err}")
# 2. Query cross-database Order records (customerprofile_beta) to retrieve confirmed pickup ID & ETA
if not pickup_id or not pickup_date:
try:
import psycopg2
conn = psycopg2.connect(
dbname='customerprofile_beta',
user='vignesh',
password='vignesh_secret_pass_2026',
host='16.113.106.152',
port='5432'
)
cur = conn.cursor()
cur.execute(
"SELECT id, order_id, status, carrier, tracking, pickup_id, eta, pickup_payload FROM api_order WHERE tracking=%s OR order_id=%s",
(trk, order_id or trk)
)
row = cur.fetchone()
if row:
db_id, db_order_id, db_status, db_carrier, db_tracking, db_pickup_id, db_eta, db_payload = row
if not order_id and db_order_id:
order_id = db_order_id
if db_pickup_id:
pickup_id = str(db_pickup_id)
if db_eta and 'Pickup:' in str(db_eta):
import re
m_date = re.search(r'Pickup:\s*([\d-]+)', str(db_eta))
m_time = re.search(r'\(([\d:]+)\)', str(db_eta))
if m_date and not pickup_date:
pickup_date = m_date.group(1)
if m_time and not pickup_time:
pickup_time = m_time.group(1)
if isinstance(db_payload, dict):
if not pickup_id and db_payload.get('pickup_id'):
pickup_id = str(db_payload.get('pickup_id'))
if not pickup_date and db_payload.get('pickup_date'):
pickup_date = db_payload.get('pickup_date')
if not pickup_time and db_payload.get('pickup_time'):
pickup_time = db_payload.get('pickup_time')
conn.close()
except Exception as db_err:
logger.warning(f"Cross-database order lookup error: {db_err}")
# 3. Check if warehouse location has an active confirmed Pickup Request
if not pickup_id and pickup_location:
loc_match = PickupSchedule.objects.filter(pickup_location=pickup_location).exclude(pickup_id='').order_by('-updated_at').first()
if loc_match:
pickup_id = loc_match.pickup_id
if not pickup_date:
pickup_date = loc_match.pickup_date
if not pickup_time:
pickup_time = loc_match.pickup_time
# Default fallbacks if date or time are not yet resolved
if not pickup_date:
pickup_date = time.strftime('%Y-%m-%d')
if not pickup_time:
pickup_time = '14:00:00' if carrier_key == 'DELHIVERY' else ('Standard Hub Sweep' if carrier_key == 'EKART' else 'Automated Cut-Off Sweep')
# Format slot description
t_clean = str(pickup_time).strip()
if t_clean.startswith('10'):
pickup_slot = 'Morning Slot (10:00 AM – 1:00 PM)'
elif t_clean.startswith('14'):
pickup_slot = 'Afternoon Slot (2:00 PM – 5:00 PM / 14:00 – 18:00)'
elif t_clean.startswith('17'):
pickup_slot = 'Evening Slot (5:00 PM – 7:00 PM)'
elif not pickup_slot:
pickup_slot = t_clean
# Determine lifecycle status
is_picked_up = str(shipment_status).lower() in ['in transit', 'picked up', 'out for delivery', 'delivered'] or carrier_live_data.get('PickedupDate')
if is_picked_up:
status_code = 'IN_TRANSIT'
status_display = f"Package Picked Up & In Transit with {carrier_display}"
elif pickup_id:
status_code = 'CONFIRMED'
status_display = f"Pickup Confirmed with {carrier_display} (PR #{pickup_id}) for {pickup_date} ({pickup_slot})"
else:
status_code = 'SCHEDULED'
status_display = f"Pickup Scheduled with {carrier_display} for {pickup_date} ({pickup_slot})"
# Cache/update into PickupSchedule table
try:
PickupSchedule.objects.update_or_create(
tracking_id=trk,
defaults={
'client_order_id': str(order_id or ''),
'carrier': carrier_key,
'carrier_display': carrier_display,
'pickup_id': str(pickup_id or ''),
'pickup_date': str(pickup_date),
'pickup_time': str(pickup_time),
'pickup_slot': str(pickup_slot),
'pickup_location': str(pickup_location or 'SAM'),
'status': status_code,
'notes': f"Queried via check_pickup_schedule ({time.strftime('%Y-%m-%d %H:%M:%S')})"
}
)
except Exception as save_err:
logger.warning(f"Error caching PickupSchedule: {save_err}")
return {
'status': 'success',
'tracking_id': trk,
'order_id': order_id,
'carrier': carrier_key,
'carrier_display': carrier_display,
'pickup_scheduled': True,
'pickup_id': pickup_id,
'pickup_date': pickup_date,
'pickup_time': pickup_time,
'pickup_slot': pickup_slot,
'pickup_location': pickup_location or 'SAM',
'shipment_status': shipment_status,
'pickup_status': status_code,
'status_display': status_display,
'destination': destination_info,
'consignee': consignee_info,
'scans': scan_history,
'details': {
'expected_package_count': local_obj.expected_package_count if local_obj else 1,
'manifest_date': carrier_live_data.get('PickUpDate'),
'picked_up_date': carrier_live_data.get('PickedupDate')
}
}, 200

View file

@ -0,0 +1,37 @@
# Generated by Django 6.1 on 2026-10-13 13:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shipping', '0002_riderassignment'),
]
operations = [
migrations.CreateModel(
name='PickupSchedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tracking_id', models.CharField(db_index=True, max_length=100)),
('client_order_id', models.CharField(blank=True, db_index=True, default='', max_length=100)),
('carrier', models.CharField(db_index=True, default='DELHIVERY', max_length=50)),
('carrier_display', models.CharField(default='Delhivery Logistics', max_length=100)),
('pickup_id', models.CharField(blank=True, db_index=True, default='', max_length=100)),
('pickup_date', models.CharField(max_length=20)),
('pickup_time', models.CharField(default='14:00:00', max_length=50)),
('pickup_slot', models.CharField(blank=True, default='', max_length=100)),
('pickup_location', models.CharField(blank=True, default='', max_length=150)),
('status', models.CharField(choices=[('SCHEDULED', 'Pickup Scheduled'), ('CONFIRMED', 'Confirmed by Carrier'), ('IN_TRANSIT', 'Picked Up / In Transit'), ('CANCELLED', 'Cancelled')], default='SCHEDULED', max_length=50)),
('expected_package_count', models.IntegerField(default=1)),
('notes', models.TextField(blank=True, default='')),
('raw_response', models.JSONField(blank=True, default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['-updated_at'],
},
),
]

View file

@ -51,3 +51,34 @@ class RiderAssignment(models.Model):
def __str__(self): def __str__(self):
return f"Rider {self.rider_name} ({self.rider_phone}) - AWB: {self.awb_number} [{self.status}]" return f"Rider {self.rider_name} ({self.rider_phone}) - AWB: {self.awb_number} [{self.status}]"
class PickupSchedule(models.Model):
STATUS_CHOICES = [
('SCHEDULED', 'Pickup Scheduled'),
('CONFIRMED', 'Confirmed by Carrier'),
('IN_TRANSIT', 'Picked Up / In Transit'),
('CANCELLED', 'Cancelled'),
]
tracking_id = models.CharField(max_length=100, db_index=True)
client_order_id = models.CharField(max_length=100, blank=True, default='', db_index=True)
carrier = models.CharField(max_length=50, default='DELHIVERY', db_index=True)
carrier_display = models.CharField(max_length=100, default='Delhivery Logistics')
pickup_id = models.CharField(max_length=100, blank=True, default='', db_index=True)
pickup_date = models.CharField(max_length=20) # YYYY-MM-DD
pickup_time = models.CharField(max_length=50, default='14:00:00')
pickup_slot = models.CharField(max_length=100, blank=True, default='') # e.g. Afternoon Slot (2:00 PM – 5:00 PM)
pickup_location = models.CharField(max_length=150, blank=True, default='')
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='SCHEDULED')
expected_package_count = models.IntegerField(default=1)
notes = models.TextField(blank=True, default='')
raw_response = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-updated_at']
def __str__(self):
return f"[{self.carrier}] Pickup {self.pickup_id or 'Pending'} - AWB: {self.tracking_id} on {self.pickup_date} ({self.pickup_time})"

View file

@ -322,8 +322,33 @@
<!-- ========================================== --> <!-- ========================================== -->
<div class="card" id="cardPickupMode" style="display: none; margin-bottom: 1.75rem;"> <div class="card" id="cardPickupMode" style="display: none; margin-bottom: 1.75rem;">
<div class="card-title"> <div class="card-title">
<span>📅 Schedule Courier Pickup Across Carriers</span> <span>📅 Schedule & Verify Courier Pickups Across Carriers</span>
<span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">Dynamic Options Enabled per Assigned Carrier</span> <span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">Check Active Pickup Schedule with Date & Time for Given Tracking, or Schedule a New Pickup Window</span>
</div>
<!-- SECTION 3A: LIVE TRACKING PICKUP SCHEDULE LOOKUP -->
<div style="background: rgba(30, 41, 59, 0.65); border: 1px solid rgba(56, 189, 248, 0.3); border-radius: 0.85rem; padding: 1.25rem; margin-bottom: 1.75rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.85rem;">
<div style="font-weight: 700; color: #fff; font-size: 1rem; display: flex; align-items: center; gap: 0.5rem;">
<span>🔍</span> Check Pickup Schedule by Tracking / AWB
</div>
<span class="badge" style="background: rgba(14, 165, 233, 0.15); color: #38bdf8; border: 1px solid rgba(14, 165, 233, 0.3);">Real-Time Carrier API Verification</span>
</div>
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
<input type="text" id="check_schedule_tracking" class="form-control" placeholder="Enter Waybill / Tracking ID (e.g. 69873010000232)" value="69873010000232" style="flex: 1; min-width: 260px; font-size: 1.05rem; font-weight: 700; letter-spacing: 0.04em;">
<button type="button" id="btnCheckSchedule" class="btn btn-primary" onclick="handleCheckScheduleSubmit()" style="padding: 0.75rem 2rem; font-weight: 700;">
<span id="btnCheckScheduleIcon">🔎</span>
<span id="btnCheckScheduleText">Check Pickup Schedule</span>
</button>
</div>
<!-- Live Pickup Schedule Result Card Container -->
<div id="checkScheduleResultContainer" style="display: none; margin-top: 1.25rem;"></div>
</div>
<div style="border-top: 1px dashed var(--border-color); margin-bottom: 1.5rem; padding-top: 0.75rem; font-size: 0.82rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700;">
<span>⚡ Or Schedule / Reschedule New Pickup Window</span>
</div> </div>
<form id="pickupScheduleForm" onsubmit="handlePickupSubmit(event)"> <form id="pickupScheduleForm" onsubmit="handlePickupSubmit(event)">
@ -714,6 +739,100 @@
} }
} }
async function handleCheckScheduleSubmit() {
const trkInput = document.getElementById('check_schedule_tracking');
const trk = trkInput ? trkInput.value.trim() : '';
if (!trk) {
alert('Please enter a valid tracking ID / waybill number.');
if (trkInput) trkInput.focus();
return;
}
const btn = document.getElementById('btnCheckSchedule');
const btnText = document.getElementById('btnCheckScheduleText');
const btnIcon = document.getElementById('btnCheckScheduleIcon');
const container = document.getElementById('checkScheduleResultContainer');
if (btn) btn.disabled = true;
if (btnIcon) btnIcon.innerHTML = '<span class="spinner"></span>';
if (btnText) btnText.textContent = 'Verifying Schedule...';
try {
const res = await fetch(`/api/v1/carrier/pickup-schedule/?tracking_id=${encodeURIComponent(trk)}`);
const data = await res.json();
if (container) {
container.style.display = 'block';
if (data.status === 'success') {
const isDlh = (data.carrier || '').includes('DELHI');
const badgeBg = isDlh ? 'rgba(14, 165, 233, 0.18)' : 'rgba(234, 179, 8, 0.18)';
const badgeBorder = isDlh ? 'rgba(14, 165, 233, 0.35)' : 'rgba(234, 179, 8, 0.35)';
const badgeColor = isDlh ? '#38bdf8' : '#facc15';
container.innerHTML = `
<div style="background: rgba(15, 23, 42, 0.85); border: 1px solid rgba(56, 189, 248, 0.4); border-radius: 0.75rem; padding: 1.25rem;">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 0.75rem; margin-bottom: 1rem;">
<div>
<span style="font-size: 1.15rem; font-weight: 800; color: #fff;">🚚 ${data.carrier_display || data.carrier}</span>
<span class="badge" style="margin-left: 0.75rem; background: ${badgeBg}; border: 1px solid ${badgeBorder}; color: ${badgeColor};">
${data.pickup_status === 'CONFIRMED' ? '✅ Confirmed Pickup' : (data.pickup_status === 'IN_TRANSIT' ? '🚀 In Transit' : '📅 Scheduled')}
</span>
</div>
${data.pickup_id ? `<span style="font-family: monospace; font-size: 0.95rem; font-weight: 700; color: #4ade80; background: rgba(34, 197, 94, 0.15); padding: 0.3rem 0.75rem; border-radius: 0.5rem; border: 1px solid rgba(34, 197, 94, 0.3);">PR #${data.pickup_id}</span>` : ''}
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
<div style="background: rgba(30, 41, 59, 0.6); padding: 0.75rem 1rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.06);">
<div style="font-size: 0.75rem; color: #94a3b8; text-transform: uppercase;">📅 Scheduled Pickup Date</div>
<div style="font-size: 1.1rem; font-weight: 800; color: #fff; margin-top: 0.2rem;">${data.pickup_date || 'N/A'}</div>
</div>
<div style="background: rgba(30, 41, 59, 0.6); padding: 0.75rem 1rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.06);">
<div style="font-size: 0.75rem; color: #94a3b8; text-transform: uppercase;">⏰ Arrival Time Window</div>
<div style="font-size: 1rem; font-weight: 800; color: #38bdf8; margin-top: 0.2rem;">${data.pickup_slot || data.pickup_time || 'Standard Sweep'}</div>
</div>
<div style="background: rgba(30, 41, 59, 0.6); padding: 0.75rem 1rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.06);">
<div style="font-size: 0.75rem; color: #94a3b8; text-transform: uppercase;">🏭 Pickup Warehouse</div>
<div style="font-size: 1rem; font-weight: 800; color: #fff; margin-top: 0.2rem;">${data.pickup_location || 'Registered Store'}</div>
</div>
<div style="background: rgba(30, 41, 59, 0.6); padding: 0.75rem 1rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.06);">
<div style="font-size: 0.75rem; color: #94a3b8; text-transform: uppercase;">📦 Shipment Status</div>
<div style="font-size: 1rem; font-weight: 800; color: #facc15; margin-top: 0.2rem;">${data.shipment_status || 'Manifested'}</div>
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 0.85rem; color: #cbd5e1; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 0.75rem; flex-wrap: wrap; gap: 0.5rem;">
<span><strong>Consignment:</strong> Order #${data.order_id || 'N/A'} (AWB: ${data.tracking_id})</span>
${data.destination && data.destination.city ? `<span><strong>Destination:</strong> ${data.destination.city} (${data.destination.pincode || ''})</span>` : ''}
</div>
</div>
`;
} else {
container.innerHTML = `
<div style="background: rgba(239, 68, 68, 0.12); border: 1px solid rgba(239, 68, 68, 0.35); border-radius: 0.75rem; padding: 1rem; color: #fca5a5;">
<strong>⚠️ Schedule Lookup:</strong> ${data.message || 'No pickup schedule found.'}
</div>
`;
}
}
const resultsWrapper = document.getElementById('resultsWrapper');
if (resultsWrapper) resultsWrapper.style.display = 'block';
const rawJson = document.getElementById('rawJsonResponse');
if (rawJson) rawJson.textContent = JSON.stringify(data, null, 2);
const rawTitle = document.getElementById('rawJsonTitle');
if (rawTitle) rawTitle.textContent = `🔍 Pickup Schedule Response (AWB: ${trk})`;
} catch (err) {
if (container) {
container.style.display = 'block';
container.innerHTML = `<div style="color: #f87171; padding: 1rem;">Failed to connect to pickup service: ${err.message}</div>`;
}
} finally {
if (btn) btn.disabled = false;
if (btnIcon) btnIcon.textContent = '🔎';
if (btnText) btnText.textContent = 'Check Pickup Schedule';
}
}
async function handlePickupSubmit(e) { async function handlePickupSubmit(e) {
e.preventDefault(); e.preventDefault();
const btn = document.getElementById('btnPickupSubmit'); const btn = document.getElementById('btnPickupSubmit');

View file

@ -19,6 +19,7 @@ urlpatterns = [
path('api/v1/carrier/check-availability/', views.api_carrier_availability, name='api_carrier_availability'), 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/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'), path('api/v1/carrier/schedule-pickup/', views.api_carrier_schedule_pickup, name='api_carrier_schedule_pickup'),
path('api/v1/carrier/pickup-schedule/', views.api_carrier_check_pickup_schedule, name='api_carrier_check_pickup_schedule'),
# Microservices REST API Endpoints under /api/v1/ # Microservices REST API Endpoints under /api/v1/
path('api/v1/health/', views.api_healthcheck, name='api_healthcheck'), path('api/v1/health/', views.api_healthcheck, name='api_healthcheck'),

View file

@ -1150,9 +1150,19 @@ def api_carrier_schedule_pickup(request):
"notes": "Call before arrival" "notes": "Call before arrival"
} }
""" """
from .carrier_allocator import get_carrier_pickup_capabilities, schedule_carrier_pickup from .carrier_allocator import get_carrier_pickup_capabilities, schedule_carrier_pickup, check_pickup_schedule_for_tracking
if request.method == 'GET': if request.method == 'GET':
tracking_id = (
request.GET.get('tracking_id')
or request.GET.get('awb')
or request.GET.get('tracking')
or request.GET.get('waybill')
)
if tracking_id:
res_data, status_code = check_pickup_schedule_for_tracking(tracking_id)
return JsonResponse(res_data, status=status_code)
carrier = request.GET.get('carrier', 'ALL') carrier = request.GET.get('carrier', 'ALL')
caps = get_carrier_pickup_capabilities(carrier) caps = get_carrier_pickup_capabilities(carrier)
return JsonResponse({'status': 'success', 'data': caps}, status=200) return JsonResponse({'status': 'success', 'data': caps}, status=200)
@ -1192,6 +1202,46 @@ def api_carrier_schedule_pickup(request):
return JsonResponse(res_data, status=status_code) return JsonResponse(res_data, status=status_code)
@csrf_exempt
def api_carrier_check_pickup_schedule(request):
"""
REST API endpoint to check courier pickup schedule with date and time for a given tracking / AWB.
GET /api/v1/carrier/pickup-schedule/?tracking_id=<awb>
POST /api/v1/carrier/pickup-schedule/ with JSON {"tracking_id": "<awb>"}
"""
if request.method not in ['GET', 'POST']:
return JsonResponse({'error': 'GET or POST method required'}, status=405)
data = {}
if request.method == 'POST' and request.body:
try:
data = json.loads(request.body.decode('utf-8'))
except Exception:
data = {}
tracking_id = (
data.get('tracking_id')
or data.get('awb')
or data.get('tracking')
or data.get('waybill')
or request.GET.get('tracking_id')
or request.GET.get('awb')
or request.GET.get('tracking')
or request.GET.get('waybill')
)
if not tracking_id:
return JsonResponse({
'status': 'error',
'message': 'tracking_id or awb parameter is required (e.g. ?tracking_id=69873010000232)'
}, status=400)
from .carrier_allocator import check_pickup_schedule_for_tracking
res_data, status_code = check_pickup_schedule_for_tracking(tracking_id)
return JsonResponse(res_data, status=status_code)