#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Demo Inventory Seeding Script for Inventory Service Creates realistic Spanish ingredients for demo template tenants This script runs as a Kubernetes init job inside the inventory-service container. It populates the template tenants with a comprehensive catalog of ingredients. Usage: python /app/scripts/demo/seed_demo_inventory.py Environment Variables Required: INVENTORY_DATABASE_URL - PostgreSQL connection string for inventory database DEMO_MODE - Set to 'production' for production seeding LOG_LEVEL - Logging level (default: INFO) """ import asyncio import uuid import sys import os import json from datetime import datetime, timezone from pathlib import Path # 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.inventory import Ingredient # Configure logging structlog.configure( processors=[ structlog.stdlib.add_log_level, structlog.processors.TimeStamper(fmt="iso"), structlog.dev.ConsoleRenderer() ] ) logger = structlog.get_logger() # Fixed Demo Tenant IDs (must match tenant service) DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") def load_ingredients_data(): """Load ingredients data from JSON file""" # Look for data file in the same directory as this script data_file = Path(__file__).parent / "ingredientes_es.json" if not data_file.exists(): raise FileNotFoundError( f"Ingredients data file not found: {data_file}. " "Make sure ingredientes_es.json is in the same directory as this script." ) logger.info("Loading ingredients data", file=str(data_file)) with open(data_file, 'r', encoding='utf-8') as f: data = json.load(f) # Flatten all ingredient categories into a single list all_ingredients = [] for category_name, ingredients in data.items(): logger.debug(f"Loading category: {category_name} ({len(ingredients)} items)") all_ingredients.extend(ingredients) logger.info(f"Loaded {len(all_ingredients)} ingredients from JSON") return all_ingredients async def seed_ingredients_for_tenant( db: AsyncSession, tenant_id: uuid.UUID, tenant_name: str, ingredients_data: list ) -> dict: """ Seed ingredients for a specific tenant using pre-defined UUIDs Args: db: Database session tenant_id: UUID of the tenant tenant_name: Name of the tenant (for logging) ingredients_data: List of ingredient dictionaries with pre-defined IDs Returns: Dict with seeding statistics """ logger.info("─" * 80) logger.info(f"Seeding ingredients for: {tenant_name}") logger.info(f"Tenant ID: {tenant_id}") logger.info("─" * 80) created_count = 0 updated_count = 0 skipped_count = 0 for ing_data in ingredients_data: sku = ing_data["sku"] name = ing_data["name"] # Check if ingredient already exists for this tenant with this SKU result = await db.execute( select(Ingredient).where( Ingredient.tenant_id == tenant_id, Ingredient.sku == sku ) ) existing_ingredient = result.scalars().first() if existing_ingredient: logger.debug(f" ⏭️ Skipping (exists): {sku} - {name}") skipped_count += 1 continue # Generate tenant-specific UUID by combining base UUID with tenant ID # This ensures each tenant has unique IDs but they're deterministic (same on re-run) base_id = uuid.UUID(ing_data["id"]) # XOR the base ID with the tenant ID to create a tenant-specific ID tenant_int = int(tenant_id.hex, 16) base_int = int(base_id.hex, 16) ingredient_id = uuid.UUID(int=tenant_int ^ base_int) # Create new ingredient ingredient = Ingredient( id=ingredient_id, tenant_id=tenant_id, name=name, sku=sku, barcode=None, # Could generate EAN-13 barcodes if needed product_type=ing_data["product_type"], ingredient_category=ing_data["ingredient_category"], product_category=ing_data["product_category"], subcategory=ing_data.get("subcategory"), description=ing_data["description"], brand=ing_data.get("brand"), unit_of_measure=ing_data["unit_of_measure"], package_size=None, average_cost=ing_data["average_cost"], last_purchase_price=ing_data["average_cost"], standard_cost=ing_data["average_cost"], low_stock_threshold=ing_data.get("low_stock_threshold", 10.0), reorder_point=ing_data.get("reorder_point", 20.0), reorder_quantity=ing_data.get("reorder_point", 20.0) * 2, max_stock_level=ing_data.get("reorder_point", 20.0) * 5, shelf_life_days=ing_data.get("shelf_life_days"), is_perishable=ing_data.get("is_perishable", False), is_active=True, allergen_info=ing_data.get("allergen_info", []), # NEW: Local production support (Sprint 5) produced_locally=ing_data.get("produced_locally", False), recipe_id=uuid.UUID(ing_data["recipe_id"]) if ing_data.get("recipe_id") else None, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) db.add(ingredient) created_count += 1 logger.debug(f" ✅ Created: {sku} - {name}") # Commit all changes for this tenant await db.commit() logger.info(f" 📊 Created: {created_count}, Skipped: {skipped_count}") logger.info("") return { "tenant_id": str(tenant_id), "tenant_name": tenant_name, "created": created_count, "skipped": skipped_count, "total": len(ingredients_data) } async def seed_inventory(db: AsyncSession): """ Seed inventory for all demo template tenants Args: db: Database session Returns: Dict with overall seeding statistics """ logger.info("=" * 80) logger.info("📦 Starting Demo Inventory Seeding") logger.info("=" * 80) # Load ingredients data once try: ingredients_data = load_ingredients_data() except FileNotFoundError as e: logger.error(str(e)) raise results = [] # Seed for San Pablo (Traditional Bakery) logger.info("") result_san_pablo = await seed_ingredients_for_tenant( db, DEMO_TENANT_SAN_PABLO, "Panadería San Pablo (Traditional)", ingredients_data ) results.append(result_san_pablo) # Seed for La Espiga (Central Workshop) result_la_espiga = await seed_ingredients_for_tenant( db, DEMO_TENANT_LA_ESPIGA, "Panadería La Espiga (Central Workshop)", ingredients_data ) results.append(result_la_espiga) # Calculate totals total_created = sum(r["created"] for r in results) total_skipped = sum(r["skipped"] for r in results) logger.info("=" * 80) logger.info("✅ Demo Inventory Seeding Completed") logger.info("=" * 80) return { "service": "inventory", "tenants_seeded": len(results), "total_created": total_created, "total_skipped": total_skipped, "results": results } async def main(): """Main execution function""" logger.info("Demo Inventory Seeding Script Starting") logger.info("Mode: %s", os.getenv("DEMO_MODE", "development")) logger.info("Log Level: %s", os.getenv("LOG_LEVEL", "INFO")) # Get database URL from environment database_url = os.getenv("INVENTORY_DATABASE_URL") or os.getenv("DATABASE_URL") if not database_url: logger.error("❌ INVENTORY_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 inventory 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_inventory(session) logger.info("") logger.info("📊 Seeding Summary:") logger.info(f" ✅ Tenants seeded: {result['tenants_seeded']}") logger.info(f" ✅ Total created: {result['total_created']}") logger.info(f" ⏭️ Total skipped: {result['total_skipped']}") logger.info("") # Print per-tenant details for tenant_result in result['results']: logger.info( f" {tenant_result['tenant_name']}: " f"{tenant_result['created']} created, {tenant_result['skipped']} skipped" ) logger.info("") logger.info("🎉 Success! Ingredient catalog is ready for cloning.") logger.info("") logger.info("Ingredients by category:") logger.info(" • Harinas: 6 tipos (T55, T65, Fuerza, Integral, Centeno, Espelta)") logger.info(" • Lácteos: 4 tipos (Mantequilla, Leche, Nata, Huevos)") logger.info(" • Levaduras: 3 tipos (Fresca, Seca, Masa Madre)") logger.info(" • Básicos: 3 tipos (Sal, Azúcar, Agua)") logger.info(" • Especiales: 5 tipos (Chocolate, Almendras, etc.)") logger.info(" • Productos: 3 referencias") logger.info("") logger.info("Next steps:") logger.info(" 1. Run seed jobs for other services (recipes, suppliers, etc.)") logger.info(" 2. Verify ingredient data in database") logger.info(" 3. Test demo session creation with inventory cloning") logger.info("") return 0 except Exception as e: logger.error("=" * 80) logger.error("❌ Demo Inventory 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)