#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Demo Orders Seeding Script for Orders Service Creates realistic orders with order lines for demo template tenants This script runs as a Kubernetes init job inside the orders-service container. """ import asyncio import uuid import sys import os import json 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)) from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from sqlalchemy import select import structlog from app.models.order import CustomerOrder, OrderItem from app.models.customer import Customer from app.models.enums import OrderStatus, PaymentMethod, PaymentStatus, DeliveryMethod # Add shared path for demo utilities sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) from shared.utils.demo_dates import BASE_REFERENCE_DATE # Configure logging logger = structlog.get_logger() # Base demo tenant IDs DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # Central bakery def load_orders_config(): """Load orders configuration from JSON file""" config_file = Path(__file__).parent / "pedidos_config_es.json" if not config_file.exists(): raise FileNotFoundError(f"Orders config file not found: {config_file}") with open(config_file, 'r', encoding='utf-8') as f: return json.load(f) def load_customers_data(): """Load customers data from JSON file""" customers_file = Path(__file__).parent / "clientes_es.json" if not customers_file.exists(): raise FileNotFoundError(f"Customers file not found: {customers_file}") with open(customers_file, 'r', encoding='utf-8') as f: data = json.load(f) return data.get("clientes", []) def calculate_date_from_offset(offset_days: int) -> datetime: """Calculate a date based on offset from BASE_REFERENCE_DATE""" return BASE_REFERENCE_DATE + timedelta(days=offset_days) # Model uses simple strings, no need for enum mapping functions # (OrderPriority, DeliveryType don't exist in enums.py) def weighted_choice(choices: list) -> dict: """Make a weighted random choice from list of dicts with 'peso' key""" total_weight = sum(c.get("peso", 1.0) for c in choices) r = random.uniform(0, total_weight) cumulative = 0 for choice in choices: cumulative += choice.get("peso", 1.0) if r <= cumulative: return choice return choices[-1] def generate_order_number(tenant_id: uuid.UUID, index: int) -> str: """Generate a unique order number""" tenant_prefix = "SP" if tenant_id == DEMO_TENANT_SAN_PABLO else "LE" return f"ORD-{tenant_prefix}-{BASE_REFERENCE_DATE.year}-{index:04d}" async def generate_orders_for_tenant( db: AsyncSession, tenant_id: uuid.UUID, tenant_name: str, config: dict, customers_data: list ): """Generate orders for a specific tenant""" logger.info(f"Generating orders for: {tenant_name}", tenant_id=str(tenant_id)) # Check if orders already exist result = await db.execute( select(CustomerOrder).where(CustomerOrder.tenant_id == tenant_id).limit(1) ) existing = result.scalar_one_or_none() if existing: logger.info(f"Orders already exist for {tenant_name}, skipping seed") return {"tenant_id": str(tenant_id), "orders_created": 0, "order_lines_created": 0, "skipped": True} # Get customers for this tenant result = await db.execute( select(Customer).where(Customer.tenant_id == tenant_id) ) customers = list(result.scalars().all()) if not customers: logger.warning(f"No customers found for {tenant_name}, cannot generate orders") return {"tenant_id": str(tenant_id), "orders_created": 0, "order_lines_created": 0, "error": "no_customers"} orders_config = config["configuracion_pedidos"] total_orders = orders_config["total_pedidos_por_tenant"] orders_created = 0 lines_created = 0 for i in range(total_orders): # Select random customer customer = random.choice(customers) # Determine temporal distribution rand_temporal = random.random() cumulative = 0 temporal_category = None for category, details in orders_config["distribucion_temporal"].items(): cumulative += details["porcentaje"] if rand_temporal <= cumulative: temporal_category = details break if not temporal_category: temporal_category = orders_config["distribucion_temporal"]["completados_antiguos"] # Calculate order date offset_days = random.randint( temporal_category["offset_dias_min"], temporal_category["offset_dias_max"] ) order_date = calculate_date_from_offset(offset_days) # Select status based on temporal category (use strings directly) status = random.choice(temporal_category["estados"]) # Select priority (use strings directly) priority_rand = random.random() cumulative_priority = 0 priority = "normal" for p, weight in orders_config["distribucion_prioridad"].items(): cumulative_priority += weight if priority_rand <= cumulative_priority: priority = p break # Select payment method (use strings directly) payment_method_choice = weighted_choice(orders_config["metodos_pago"]) payment_method = payment_method_choice["metodo"] # Select delivery type (use strings directly) delivery_type_choice = weighted_choice(orders_config["tipos_entrega"]) delivery_method = delivery_type_choice["tipo"] # Calculate delivery date (1-7 days after order date typically) delivery_offset = random.randint(1, 7) delivery_date = order_date + timedelta(days=delivery_offset) # Select delivery time delivery_time = random.choice(orders_config["horarios_entrega"]) # Generate order number order_number = generate_order_number(tenant_id, i + 1) # Select notes notes = random.choice(orders_config["notas_pedido"]) if random.random() < 0.6 else None # Create order (using only actual model fields) order = CustomerOrder( id=uuid.uuid4(), tenant_id=tenant_id, order_number=order_number, customer_id=customer.id, status=status, order_type="standard", priority=priority, order_date=order_date, requested_delivery_date=delivery_date, confirmed_delivery_date=delivery_date if status != "pending" else None, actual_delivery_date=delivery_date if status in ["delivered", "completed"] else None, delivery_method=delivery_method, delivery_address={"address": customer.address_line1, "city": customer.city, "postal_code": customer.postal_code} if customer.address_line1 else None, payment_method=payment_method, payment_status="paid" if status in ["delivered", "completed"] else "pending", payment_terms="immediate", subtotal=Decimal("0.00"), # Will calculate discount_percentage=Decimal("0.00"), # Will set discount_amount=Decimal("0.00"), # Will calculate tax_amount=Decimal("0.00"), # Will calculate delivery_fee=Decimal("0.00"), total_amount=Decimal("0.00"), # Will calculate special_instructions=notes, order_source="manual", sales_channel="direct", created_at=order_date, updated_at=order_date ) db.add(order) await db.flush() # Get order ID # Generate order lines num_lines = random.randint( orders_config["lineas_por_pedido"]["min"], orders_config["lineas_por_pedido"]["max"] ) # Select random products selected_products = random.sample( orders_config["productos_demo"], min(num_lines, len(orders_config["productos_demo"])) ) subtotal = Decimal("0.00") for line_num, product in enumerate(selected_products, 1): quantity = random.randint( orders_config["cantidad_por_linea"]["min"], orders_config["cantidad_por_linea"]["max"] ) # Use base price with some variation unit_price = Decimal(str(product["precio_base"])) * Decimal(str(random.uniform(0.95, 1.05))) unit_price = unit_price.quantize(Decimal("0.01")) line_total = unit_price * quantity order_line = OrderItem( id=uuid.uuid4(), order_id=order.id, product_id=uuid.uuid4(), # Generate placeholder product ID product_name=product["nombre"], product_sku=product["codigo"], quantity=Decimal(str(quantity)), unit_of_measure="each", unit_price=unit_price, line_discount=Decimal("0.00"), line_total=line_total, status="pending" ) db.add(order_line) subtotal += line_total lines_created += 1 # Apply order-level discount discount_rand = random.random() if discount_rand < 0.70: discount_percentage = Decimal("0.00") elif discount_rand < 0.85: discount_percentage = Decimal("5.00") elif discount_rand < 0.95: discount_percentage = Decimal("10.00") else: discount_percentage = Decimal("15.00") discount_amount = (subtotal * discount_percentage / 100).quantize(Decimal("0.01")) amount_after_discount = subtotal - discount_amount tax_amount = (amount_after_discount * Decimal("0.10")).quantize(Decimal("0.01")) total_amount = amount_after_discount + tax_amount # Update order totals order.subtotal = subtotal order.discount_percentage = discount_percentage order.discount_amount = discount_amount order.tax_amount = tax_amount order.total_amount = total_amount orders_created += 1 await db.commit() logger.info(f"Successfully created {orders_created} orders with {lines_created} lines for {tenant_name}") return { "tenant_id": str(tenant_id), "orders_created": orders_created, "order_lines_created": lines_created, "skipped": False } async def seed_all(db: AsyncSession): """Seed all demo tenants with orders""" logger.info("Starting demo orders seed process") # Load configuration config = load_orders_config() customers_data = load_customers_data() results = [] # Seed San Pablo (Individual Bakery) result_san_pablo = await generate_orders_for_tenant( db, DEMO_TENANT_SAN_PABLO, "San Pablo - Individual Bakery", config, customers_data ) results.append(result_san_pablo) # Seed La Espiga (Central Bakery) result_la_espiga = await generate_orders_for_tenant( db, DEMO_TENANT_LA_ESPIGA, "La Espiga - Central Bakery", config, customers_data ) results.append(result_la_espiga) total_orders = sum(r["orders_created"] for r in results) total_lines = sum(r["order_lines_created"] for r in results) return { "results": results, "total_orders_created": total_orders, "total_lines_created": total_lines, "status": "completed" } async def main(): """Main execution function""" # Get database URL from environment database_url = os.getenv("ORDERS_DATABASE_URL") if not database_url: logger.error("ORDERS_DATABASE_URL environment variable must be set") return 1 # Ensure asyncpg driver if database_url.startswith("postgresql://"): database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1) # Create async engine engine = create_async_engine(database_url, echo=False) async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) try: async with async_session() as session: result = await seed_all(session) logger.info( "Orders seed completed successfully!", total_orders=result["total_orders_created"], total_lines=result["total_lines_created"], status=result["status"] ) # Print summary print("\n" + "="*60) print("DEMO ORDERS SEED SUMMARY") print("="*60) for tenant_result in result["results"]: tenant_id = tenant_result["tenant_id"] orders = tenant_result["orders_created"] lines = tenant_result["order_lines_created"] skipped = tenant_result.get("skipped", False) status = "SKIPPED (already exists)" if skipped else f"CREATED {orders} orders, {lines} lines" print(f"Tenant {tenant_id}: {status}") print(f"\nTotal Orders: {result['total_orders_created']}") print(f"Total Order Lines: {result['total_lines_created']}") print("="*60 + "\n") return 0 except Exception as e: logger.error(f"Orders seed failed: {str(e)}", exc_info=True) return 1 finally: await engine.dispose() if __name__ == "__main__": exit_code = asyncio.run(main()) sys.exit(exit_code)