260 lines
9.5 KiB
Python
260 lines
9.5 KiB
Python
#!/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)
|