#!/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 historical distribution data (routes + shipments) Creates delivery routes for Mon/Wed/Fri pattern going back 30 days from BASE_REFERENCE_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"History: 30 days from {BASE_REFERENCE_DATE}") logger.info("") routes_created = 0 shipments_created = 0 # Generate 30 days of historical routes (working backwards from BASE_REFERENCE_DATE) for days_ago in range(30, 0, -1): delivery_date = BASE_REFERENCE_DATE - timedelta(days=days_ago) # 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) route_sequence = [ {"stop": 1, "tenant_id": str(DEMO_TENANT_CHILD_1), "location": "Madrid Centro"}, {"stop": 2, "tenant_id": str(DEMO_TENANT_CHILD_2), "location": "Barcelona Gràcia"}, {"stop": 3, "tenant_id": str(DEMO_TENANT_CHILD_3), "location": "Valencia Ruzafa"} ] 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=DeliveryRouteStatus.completed if days_ago > 1 else DeliveryRouteStatus.planned, # Recent routes are planned, old ones completed 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]}" 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=ShipmentStatus.delivered if days_ago > 1 else ShipmentStatus.pending, 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)