"""Logistics Engine: shipment tracking and courier management. Supports multiple couriers: DHL, FedEx, Estafeta, 99 Minutos, Uber Direct. Provides tracking URL generation, status updates, and history logging. """ from datetime import datetime def create_shipment(conn, data): """Create a new shipment record. data: { tenant_id, branch_id, shipment_type, related_type, related_id, courier_id, tracking_number, origin_address, destination_address, recipient_name, recipient_phone, estimated_delivery, shipping_cost, weight_kg, dimensions_cm, notes, created_by } """ cur = conn.cursor() # Generate tracking URL if template exists tracking_url = None if data.get('courier_id') and data.get('tracking_number'): cur.execute("SELECT tracking_url_template FROM couriers WHERE id = %s", (data['courier_id'],)) row = cur.fetchone() if row and row[0]: tracking_url = row[0].replace('{tracking_number}', data['tracking_number']) cur.execute(""" INSERT INTO shipments (tenant_id, branch_id, shipment_type, related_type, related_id, courier_id, tracking_number, tracking_url, status, origin_address, destination_address, recipient_name, recipient_phone, estimated_delivery, shipping_cost, weight_kg, dimensions_cm, notes, created_by) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'pending', %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, ( data.get('tenant_id'), data.get('branch_id'), data.get('shipment_type', 'outbound'), data.get('related_type'), data.get('related_id'), data.get('courier_id'), data.get('tracking_number'), tracking_url, data.get('origin_address'), data.get('destination_address'), data.get('recipient_name'), data.get('recipient_phone'), data.get('estimated_delivery'), data.get('shipping_cost', 0), data.get('weight_kg'), data.get('dimensions_cm'), data.get('notes'), data.get('created_by'), )) shipment_id = cur.fetchone()[0] # Log initial tracking entry if tracking_url: cur.execute(""" INSERT INTO shipment_tracking (shipment_id, status, description) VALUES (%s, 'pending', 'EnvĂ­o registrado') """, (shipment_id,)) conn.commit() cur.close() return {'shipment_id': shipment_id, 'tracking_url': tracking_url} def get_shipment(conn, shipment_id): cur = conn.cursor() cur.execute(""" SELECT s.id, s.shipment_type, s.related_type, s.related_id, s.courier_id, c.name as courier_name, s.tracking_number, s.tracking_url, s.status, s.origin_address, s.destination_address, s.recipient_name, s.recipient_phone, s.estimated_delivery, s.actual_delivery, s.shipping_cost, s.weight_kg, s.dimensions_cm, s.notes, s.created_at, s.updated_at FROM shipments s LEFT JOIN couriers c ON s.courier_id = c.id WHERE s.id = %s """, (shipment_id,)) row = cur.fetchone() if not row: cur.close() return None shipment = { 'id': row[0], 'shipment_type': row[1], 'related_type': row[2], 'related_id': row[3], 'courier_id': row[4], 'courier_name': row[5], 'tracking_number': row[6], 'tracking_url': row[7], 'status': row[8], 'origin_address': row[9], 'destination_address': row[10], 'recipient_name': row[11], 'recipient_phone': row[12], 'estimated_delivery': str(row[13]) if row[13] else None, 'actual_delivery': str(row[14]) if row[14] else None, 'shipping_cost': float(row[15]) if row[15] else 0, 'weight_kg': float(row[16]) if row[16] else None, 'dimensions_cm': row[17], 'notes': row[18], 'created_at': str(row[19]), 'updated_at': str(row[20]), } # Tracking history cur.execute(""" SELECT id, status, location, description, tracked_at FROM shipment_tracking WHERE shipment_id = %s ORDER BY tracked_at DESC """, (shipment_id,)) shipment['tracking_history'] = [] for r in cur.fetchall(): shipment['tracking_history'].append({ 'id': r[0], 'status': r[1], 'location': r[2], 'description': r[3], 'tracked_at': str(r[4]), }) cur.close() return shipment def list_shipments(conn, tenant_id, status=None, courier_id=None, related_type=None, related_id=None, page=1, per_page=50): cur = conn.cursor() where = ["tenant_id = %s"] params = [tenant_id] if status: where.append("status = %s") params.append(status) if courier_id: where.append("courier_id = %s") params.append(courier_id) if related_type: where.append("related_type = %s") params.append(related_type) if related_id: where.append("related_id = %s") params.append(related_id) where_str = " AND ".join(where) cur.execute(f"SELECT count(*) FROM shipments WHERE {where_str}", params) total = cur.fetchone()[0] extra_where = "" if len(where) > 1: extra_where = " AND " + " AND ".join(where[1:]) cur.execute(f""" SELECT s.id, s.shipment_type, s.related_type, s.related_id, c.name as courier_name, s.tracking_number, s.status, s.recipient_name, s.estimated_delivery, s.created_at FROM shipments s LEFT JOIN couriers c ON s.courier_id = c.id WHERE s.tenant_id = %s {extra_where} ORDER BY s.created_at DESC LIMIT %s OFFSET %s """, params + [per_page, (page - 1) * per_page]) shipments = [] for r in cur.fetchall(): shipments.append({ 'id': r[0], 'shipment_type': r[1], 'related_type': r[2], 'related_id': r[3], 'courier_name': r[4], 'tracking_number': r[5], 'status': r[6], 'recipient_name': r[7], 'estimated_delivery': str(r[8]) if r[8] else None, 'created_at': str(r[9]), }) cur.close() return { 'data': shipments, 'pagination': {'page': page, 'per_page': per_page, 'total': total} } def update_shipment_status(conn, shipment_id, new_status, location=None, description=None, raw_response=None): """Update shipment status and log tracking history.""" cur = conn.cursor() cur.execute("SELECT status FROM shipments WHERE id = %s", (shipment_id,)) row = cur.fetchone() if not row: cur.close() raise ValueError("Shipment not found") old_status = row[0] # Update shipment extra_sets = [] extra_vals = [] if new_status == 'delivered': extra_sets.append("actual_delivery = NOW()") set_clause = ", ".join(["status = %s"] + extra_sets) cur.execute(f""" UPDATE shipments SET {set_clause} WHERE id = %s """, [new_status] + extra_vals + [shipment_id]) # Log tracking history cur.execute(""" INSERT INTO shipment_tracking (shipment_id, status, location, description, raw_response) VALUES (%s, %s, %s, %s, %s) """, (shipment_id, new_status, location, description, raw_response)) conn.commit() cur.close() return {'old_status': old_status, 'new_status': new_status} def get_couriers(conn, tenant_id): cur = conn.cursor() cur.execute(""" SELECT id, name, code, tracking_url_template, api_endpoint, is_active FROM couriers WHERE tenant_id = %s ORDER BY name """, (tenant_id,)) couriers = [] for r in cur.fetchall(): couriers.append({ 'id': r[0], 'name': r[1], 'code': r[2], 'tracking_url_template': r[3], 'api_endpoint': r[4], 'is_active': r[5], }) cur.close() return couriers def add_courier(conn, tenant_id, name, code, tracking_url_template=None, api_endpoint=None, is_active=True): cur = conn.cursor() cur.execute(""" INSERT INTO couriers (tenant_id, name, code, tracking_url_template, api_endpoint, is_active) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id """, (tenant_id, name, code, tracking_url_template, api_endpoint, is_active)) cid = cur.fetchone()[0] conn.commit() cur.close() return cid