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 )