Files
bakery-ia/services/distribution/scripts/demo/seed_demo_distribution_history.py

301 lines
11 KiB
Python
Raw Normal View History

2025-11-30 09:12:40 +01:00
#!/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):
"""
2025-12-05 20:07:01 +01:00
Seed 30 days of distribution data (routes + shipments) centered around BASE_REFERENCE_DATE
2025-11-30 09:12:40 +01:00
2025-12-05 20:07:01 +01:00
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.
2025-11-30 09:12:40 +01:00
"""
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)")
2025-12-05 20:07:01 +01:00
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')}")
2025-11-30 09:12:40 +01:00
logger.info("")
routes_created = 0
shipments_created = 0
2025-12-05 20:07:01 +01:00
# 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)
2025-11-30 09:12:40 +01:00
# 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
2025-12-09 10:21:41 +01:00
# 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"
2025-11-30 09:12:40 +01:00
route_sequence = [
2025-12-09 10:21:41 +01:00
{
"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
}
2025-11-30 09:12:40 +01:00
]
2025-12-09 10:21:41 +01:00
# Route status (already determined is_past above)
2025-12-05 20:07:01 +01:00
route_status = DeliveryRouteStatus.completed if is_past else DeliveryRouteStatus.planned
2025-11-30 09:12:40 +01:00
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,
2025-12-05 20:07:01 +01:00
status=route_status,
2025-11-30 09:12:40 +01:00
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]}"
2025-12-05 20:07:01 +01:00
# Determine shipment status based on date
shipment_status = ShipmentStatus.delivered if is_past else ShipmentStatus.pending
2025-11-30 09:12:40 +01:00
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,
2025-12-05 20:07:01 +01:00
status=shipment_status,
2025-11-30 09:12:40 +01:00
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)