Files
bakery-ia/services/distribution/scripts/demo/seed_demo_distribution_history.py
2025-11-30 09:12:40 +01:00

260 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)