#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Demo Distribution History Seeding Script for Distribution Service Creates 30 days of historical delivery routes and shipments for enterprise demo This is the CRITICAL missing piece that connects parent (Obrador) to children (retail outlets). It populates the template with realistic VRP-optimized delivery routes. Usage: python /app/scripts/demo/seed_demo_distribution_history.py Environment Variables Required: DISTRIBUTION_DATABASE_URL - PostgreSQL connection string DEMO_MODE - Set to 'production' for production seeding """ import asyncio import uuid import sys import os import random from datetime import datetime, timezone, timedelta from pathlib import Path from decimal import Decimal # Add app to path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) # Add shared to path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent)) from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from sqlalchemy import select import structlog from shared.utils.demo_dates import BASE_REFERENCE_DATE from app.models import DeliveryRoute, Shipment, DeliveryRouteStatus, ShipmentStatus structlog.configure( processors=[ structlog.stdlib.add_log_level, structlog.processors.TimeStamper(fmt="iso"), structlog.dev.ConsoleRenderer() ] ) logger = structlog.get_logger() # Fixed Demo Tenant IDs DEMO_TENANT_ENTERPRISE_CHAIN = uuid.UUID("c3d4e5f6-a7b8-49c0-d1e2-f3a4b5c6d7e8") # Parent (Obrador) DEMO_TENANT_CHILD_1 = uuid.UUID("d4e5f6a7-b8c9-40d1-e2f3-a4b5c6d7e8f9") # Madrid Centro DEMO_TENANT_CHILD_2 = uuid.UUID("e5f6a7b8-c9d0-41e2-f3a4-b5c6d7e8f9a0") # Barcelona Gràcia DEMO_TENANT_CHILD_3 = uuid.UUID("f6a7b8c9-d0e1-42f3-a4b5-c6d7e8f9a0b1") # Valencia Ruzafa CHILD_TENANTS = [ (DEMO_TENANT_CHILD_1, "Madrid Centro", 150.0), (DEMO_TENANT_CHILD_2, "Barcelona Gràcia", 120.0), (DEMO_TENANT_CHILD_3, "Valencia Ruzafa", 100.0) ] # Delivery schedule: Mon/Wed/Fri (as per distribution service) DELIVERY_WEEKDAYS = [0, 2, 4] # Monday, Wednesday, Friday async def seed_distribution_history(db: AsyncSession): """ Seed 30 days of distribution data (routes + shipments) centered around BASE_REFERENCE_DATE Creates delivery routes for Mon/Wed/Fri pattern spanning from 15 days before to 15 days after BASE_REFERENCE_DATE. This ensures data exists for today when BASE_REFERENCE_DATE is set to the current date. """ logger.info("=" * 80) logger.info("🚚 Starting Demo Distribution History Seeding") logger.info("=" * 80) logger.info(f"Parent Tenant: {DEMO_TENANT_ENTERPRISE_CHAIN} (Obrador Madrid)") logger.info(f"Child Tenants: {len(CHILD_TENANTS)}") logger.info(f"Delivery Pattern: Mon/Wed/Fri (3x per week)") logger.info(f"Date Range: {(BASE_REFERENCE_DATE - timedelta(days=15)).strftime('%Y-%m-%d')} to {(BASE_REFERENCE_DATE + timedelta(days=15)).strftime('%Y-%m-%d')}") logger.info(f"Reference Date (today): {BASE_REFERENCE_DATE.strftime('%Y-%m-%d')}") logger.info("") routes_created = 0 shipments_created = 0 # Generate 30 days of routes centered around BASE_REFERENCE_DATE (-15 to +15 days) # This ensures we have past data, current data, and future data # Range is inclusive of start, exclusive of end, so -15 to 16 gives -15..15 for days_offset in range(-15, 16): # -15 to +15 = 31 days total delivery_date = BASE_REFERENCE_DATE + timedelta(days=days_offset) # Only create routes for Mon/Wed/Fri if delivery_date.weekday() not in DELIVERY_WEEKDAYS: continue # Check if route already exists result = await db.execute( select(DeliveryRoute).where( DeliveryRoute.tenant_id == DEMO_TENANT_ENTERPRISE_CHAIN, DeliveryRoute.route_date == delivery_date ).limit(1) ) existing_route = result.scalar_one_or_none() if existing_route: logger.debug(f"Route already exists for {delivery_date.strftime('%Y-%m-%d')}, skipping") continue # Create delivery route route_number = f"DEMO-{delivery_date.strftime('%Y%m%d')}-001" # Realistic VRP metrics for 3-stop route # Distance: Madrid Centro (closest) + Barcelona Gràcia (medium) + Valencia Ruzafa (farthest) total_distance_km = random.uniform(75.0, 95.0) # Realistic for 3 retail outlets in region estimated_duration_minutes = random.randint(180, 240) # 3-4 hours for 3 stops # Route sequence (order of deliveries) with full GPS coordinates for map display # Determine status based on date is_past = delivery_date < BASE_REFERENCE_DATE point_status = "delivered" if is_past else "pending" route_sequence = [ { "tenant_id": str(DEMO_TENANT_CHILD_1), "name": "Madrid Centro", "address": "Calle Gran Vía 28, 28013 Madrid, Spain", "latitude": 40.4168, "longitude": -3.7038, "status": point_status, "id": str(uuid.uuid4()), "sequence": 1 }, { "tenant_id": str(DEMO_TENANT_CHILD_2), "name": "Barcelona Gràcia", "address": "Carrer Gran de Gràcia 15, 08012 Barcelona, Spain", "latitude": 41.4036, "longitude": 2.1561, "status": point_status, "id": str(uuid.uuid4()), "sequence": 2 }, { "tenant_id": str(DEMO_TENANT_CHILD_3), "name": "Valencia Ruzafa", "address": "Carrer de Sueca 51, 46006 Valencia, Spain", "latitude": 39.4647, "longitude": -0.3679, "status": point_status, "id": str(uuid.uuid4()), "sequence": 3 } ] # Route status (already determined is_past above) route_status = DeliveryRouteStatus.completed if is_past else DeliveryRouteStatus.planned route = DeliveryRoute( id=uuid.uuid4(), tenant_id=DEMO_TENANT_ENTERPRISE_CHAIN, route_number=route_number, route_date=delivery_date, total_distance_km=Decimal(str(round(total_distance_km, 2))), estimated_duration_minutes=estimated_duration_minutes, route_sequence=route_sequence, status=route_status, driver_id=uuid.uuid4(), # Use a random UUID for the driver_id vehicle_id=f"VEH-{random.choice(['001', '002', '003'])}", created_at=delivery_date - timedelta(days=1), # Routes created day before updated_at=delivery_date, created_by=uuid.uuid4(), # Add required audit field updated_by=uuid.uuid4() # Add required audit field ) db.add(route) routes_created += 1 # Create shipments for each child tenant on this route for child_tenant_id, child_name, avg_weight_kg in CHILD_TENANTS: # Vary weight slightly shipment_weight = avg_weight_kg * random.uniform(0.9, 1.1) shipment_number = f"DEMOSHP-{delivery_date.strftime('%Y%m%d')}-{child_name.split()[0].upper()[:3]}" # Determine shipment status based on date shipment_status = ShipmentStatus.delivered if is_past else ShipmentStatus.pending shipment = Shipment( id=uuid.uuid4(), tenant_id=DEMO_TENANT_ENTERPRISE_CHAIN, parent_tenant_id=DEMO_TENANT_ENTERPRISE_CHAIN, child_tenant_id=child_tenant_id, shipment_number=shipment_number, shipment_date=delivery_date, status=shipment_status, total_weight_kg=Decimal(str(round(shipment_weight, 2))), delivery_route_id=route.id, delivery_notes=f"Entrega regular a {child_name}", created_at=delivery_date - timedelta(days=1), updated_at=delivery_date, created_by=uuid.uuid4(), # Add required audit field updated_by=uuid.uuid4() # Add required audit field ) db.add(shipment) shipments_created += 1 logger.debug( f" ✅ {delivery_date.strftime('%a %Y-%m-%d')}: " f"Route {route_number} with {len(CHILD_TENANTS)} shipments" ) # Commit all changes await db.commit() logger.info("") logger.info("=" * 80) logger.info("✅ Demo Distribution History Seeding Completed") logger.info("=" * 80) logger.info(f" 📊 Routes created: {routes_created}") logger.info(f" 📦 Shipments created: {shipments_created}") logger.info("") logger.info("Distribution characteristics:") logger.info(" ✓ 30 days of historical data") logger.info(" ✓ Mon/Wed/Fri delivery schedule (3x per week)") logger.info(" ✓ VRP-optimized route sequencing") logger.info(" ✓ ~13 routes (30 days ÷ 7 days/week × 3 delivery days)") logger.info(" ✓ ~39 shipments (13 routes × 3 children)") logger.info(" ✓ Realistic distances and durations") logger.info("") return { "service": "distribution", "routes_created": routes_created, "shipments_created": shipments_created } async def main(): """Main execution function""" logger.info("Demo Distribution History Seeding Script Starting") logger.info("Mode: %s", os.getenv("DEMO_MODE", "development")) # Get database URL from environment database_url = os.getenv("DISTRIBUTION_DATABASE_URL") or os.getenv("DATABASE_URL") if not database_url: logger.error("❌ DISTRIBUTION_DATABASE_URL or DATABASE_URL environment variable must be set") return 1 # Convert to async URL if needed if database_url.startswith("postgresql://"): database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1) logger.info("Connecting to distribution database") # Create engine and session engine = create_async_engine( database_url, echo=False, pool_pre_ping=True, pool_size=5, max_overflow=10 ) async_session = sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) try: async with async_session() as session: result = await seed_distribution_history(session) logger.info("🎉 Success! Distribution history is ready for cloning.") logger.info("") logger.info("Next steps:") logger.info(" 1. Create Kubernetes job YAMLs for all child scripts") logger.info(" 2. Update kustomization.yaml with proper execution order") logger.info(" 3. Test enterprise demo end-to-end") logger.info("") return 0 except Exception as e: logger.error("=" * 80) logger.error("❌ Demo Distribution History Seeding Failed") logger.error("=" * 80) logger.error("Error: %s", str(e)) logger.error("", exc_info=True) return 1 finally: await engine.dispose() if __name__ == "__main__": exit_code = asyncio.run(main()) sys.exit(exit_code)