#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Demo Suppliers Seeding Script for Suppliers Service Creates realistic Spanish suppliers for demo template tenants using pre-defined UUIDs This script runs as a Kubernetes init job inside the suppliers-service container. It populates the template tenants with a comprehensive catalog of suppliers. Usage: python /app/scripts/demo/seed_demo_suppliers.py Environment Variables Required: SUPPLIERS_DATABASE_URL - PostgreSQL connection string for suppliers database DEMO_MODE - Set to 'production' for production seeding LOG_LEVEL - Logging level (default: INFO) Note: No database lookups needed - all IDs are pre-defined in the JSON file """ import asyncio import uuid import sys import os import json from datetime import datetime, timezone, timedelta from pathlib import Path import random 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, text import structlog from app.models.suppliers import ( Supplier, SupplierPriceList, SupplierType, SupplierStatus, PaymentTerms ) # 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") # Hardcoded SKU to Ingredient ID mapping (no database lookups needed!) INGREDIENT_ID_MAP = { "HAR-T55-001": "10000000-0000-0000-0000-000000000001", "HAR-T65-002": "10000000-0000-0000-0000-000000000002", "HAR-FUE-003": "10000000-0000-0000-0000-000000000003", "HAR-INT-004": "10000000-0000-0000-0000-000000000004", "HAR-CEN-005": "10000000-0000-0000-0000-000000000005", "HAR-ESP-006": "10000000-0000-0000-0000-000000000006", "LAC-MAN-001": "10000000-0000-0000-0000-000000000011", "LAC-LEC-002": "10000000-0000-0000-0000-000000000012", "LAC-NAT-003": "10000000-0000-0000-0000-000000000013", "LAC-HUE-004": "10000000-0000-0000-0000-000000000014", "LEV-FRE-001": "10000000-0000-0000-0000-000000000021", "LEV-SEC-002": "10000000-0000-0000-0000-000000000022", "BAS-SAL-001": "10000000-0000-0000-0000-000000000031", "BAS-AZU-002": "10000000-0000-0000-0000-000000000032", "ESP-CHO-001": "10000000-0000-0000-0000-000000000041", "ESP-ALM-002": "10000000-0000-0000-0000-000000000042", "ESP-VAI-004": "10000000-0000-0000-0000-000000000044", "ESP-CRE-005": "10000000-0000-0000-0000-000000000045", } # Ingredient costs (for price list generation) INGREDIENT_COSTS = { "HAR-T55-001": 0.85, "HAR-T65-002": 0.95, "HAR-FUE-003": 1.15, "HAR-INT-004": 1.20, "HAR-CEN-005": 1.30, "HAR-ESP-006": 2.45, "LAC-MAN-001": 6.50, "LAC-LEC-002": 0.95, "LAC-NAT-003": 3.20, "LAC-HUE-004": 0.25, "LEV-FRE-001": 4.80, "LEV-SEC-002": 12.50, "BAS-SAL-001": 0.60, "BAS-AZU-002": 0.90, "ESP-CHO-001": 15.50, "ESP-ALM-002": 8.90, "ESP-VAI-004": 3.50, "ESP-CRE-005": 7.20, } def load_suppliers_data(): """Load suppliers data from JSON file""" # Look for data file in the same directory as this script data_file = Path(__file__).parent / "proveedores_es.json" if not data_file.exists(): raise FileNotFoundError( f"Suppliers data file not found: {data_file}. " "Make sure proveedores_es.json is in the same directory as this script." ) logger.info("Loading suppliers data", file=str(data_file)) with open(data_file, 'r', encoding='utf-8') as f: data = json.load(f) suppliers = data.get("proveedores", []) logger.info(f"Loaded {len(suppliers)} suppliers from JSON") return suppliers async def seed_suppliers_for_tenant( db: AsyncSession, tenant_id: uuid.UUID, tenant_name: str, suppliers_data: list ) -> dict: """ Seed suppliers 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) suppliers_data: List of supplier dictionaries with pre-defined IDs Returns: Dict with seeding statistics """ logger.info("─" * 80) logger.info(f"Seeding suppliers for: {tenant_name}") logger.info(f"Tenant ID: {tenant_id}") logger.info("─" * 80) created_suppliers = 0 skipped_suppliers = 0 created_price_lists = 0 for supplier_data in suppliers_data: supplier_name = supplier_data["name"] # Generate tenant-specific UUID by combining base UUID with tenant ID base_supplier_id = uuid.UUID(supplier_data["id"]) tenant_int = int(tenant_id.hex, 16) supplier_id = uuid.UUID(int=tenant_int ^ int(base_supplier_id.hex, 16)) # Check if supplier already exists (using tenant-specific ID) result = await db.execute( select(Supplier).where( Supplier.tenant_id == tenant_id, Supplier.id == supplier_id ) ) existing_supplier = result.scalars().first() if existing_supplier: logger.debug(f" ⏭️ Supplier exists, ensuring price lists: {supplier_name}") skipped_suppliers += 1 # Don't skip - continue to create/update price lists below else: # Parse enums try: supplier_type = SupplierType(supplier_data.get("supplier_type", "ingredients")) except ValueError: supplier_type = SupplierType.INGREDIENTS try: status = SupplierStatus(supplier_data.get("status", "active")) except ValueError: status = SupplierStatus.ACTIVE try: payment_terms = PaymentTerms(supplier_data.get("payment_terms", "net_30")) except ValueError: payment_terms = PaymentTerms.NET_30 # Create supplier with pre-defined ID supplier = Supplier( id=supplier_id, tenant_id=tenant_id, name=supplier_name, supplier_code=f"SUP-{created_suppliers + 1:03d}", supplier_type=supplier_type, status=status, tax_id=supplier_data.get("tax_id"), contact_person=supplier_data.get("contact_person"), email=supplier_data.get("email"), phone=supplier_data.get("phone"), mobile=supplier_data.get("mobile"), website=supplier_data.get("website"), address_line1=supplier_data.get("address_line1"), address_line2=supplier_data.get("address_line2"), city=supplier_data.get("city"), state_province=supplier_data.get("state_province"), postal_code=supplier_data.get("postal_code"), country=supplier_data.get("country", "España"), payment_terms=payment_terms, credit_limit=Decimal(str(supplier_data.get("credit_limit", 0.0))), standard_lead_time=supplier_data.get("standard_lead_time", 3), quality_rating=supplier_data.get("quality_rating", 4.5), delivery_rating=supplier_data.get("delivery_rating", 4.5), notes=supplier_data.get("notes"), certifications=supplier_data.get("certifications", []), created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), created_by=uuid.UUID("00000000-0000-0000-0000-000000000000"), # System user updated_by=uuid.UUID("00000000-0000-0000-0000-000000000000") # System user ) db.add(supplier) created_suppliers += 1 logger.debug(f" ✅ Created supplier: {supplier_name}") # Create price lists for products using pre-defined ingredient IDs products = supplier_data.get("products", []) for product_sku in products: # Get ingredient ID from hardcoded mapping (no DB lookup!) ingredient_id_str = INGREDIENT_ID_MAP.get(product_sku) if not ingredient_id_str: logger.warning(f" ⚠️ Product SKU not in mapping: {product_sku}") continue # Generate tenant-specific ingredient ID (same as inventory seed) base_ingredient_id = uuid.UUID(ingredient_id_str) tenant_int = int(tenant_id.hex, 16) ingredient_id = uuid.UUID(int=tenant_int ^ int(base_ingredient_id.hex, 16)) # Check if price list already exists existing_price_list_result = await db.execute( select(SupplierPriceList).where( SupplierPriceList.tenant_id == tenant_id, SupplierPriceList.supplier_id == supplier_id, SupplierPriceList.inventory_product_id == ingredient_id ) ) existing_price_list = existing_price_list_result.scalars().first() if existing_price_list: # Price list already exists, skip continue # Get base cost from hardcoded costs base_cost = INGREDIENT_COSTS.get(product_sku, 1.0) # Calculate supplier price (slightly vary from base cost) price_variation = random.uniform(0.90, 1.10) unit_price = Decimal(str(base_cost * price_variation)) # price_per_unit is same as unit_price for base quantity price_per_unit = unit_price price_list = SupplierPriceList( id=uuid.uuid4(), tenant_id=tenant_id, supplier_id=supplier_id, inventory_product_id=ingredient_id, product_code=product_sku, unit_price=unit_price, price_per_unit=price_per_unit, minimum_order_quantity=random.choice([1, 5, 10]), unit_of_measure="kg", effective_date=datetime.now(timezone.utc) - timedelta(days=90), is_active=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), created_by=uuid.UUID("00000000-0000-0000-0000-000000000000"), # System user updated_by=uuid.UUID("00000000-0000-0000-0000-000000000000") # System user ) db.add(price_list) created_price_lists += 1 # Commit all changes for this tenant await db.commit() logger.info(f" 📊 Suppliers: {created_suppliers}, Price Lists: {created_price_lists}") logger.info("") return { "tenant_id": str(tenant_id), "tenant_name": tenant_name, "suppliers_created": created_suppliers, "suppliers_skipped": skipped_suppliers, "price_lists_created": created_price_lists, "total_suppliers": len(suppliers_data) } async def seed_suppliers(db: AsyncSession): """ Seed suppliers for all demo template tenants using pre-defined IDs Args: db: Database session Returns: Dict with overall seeding statistics """ logger.info("=" * 80) logger.info("🚚 Starting Demo Suppliers Seeding") logger.info("=" * 80) # Load suppliers data once try: suppliers_data = load_suppliers_data() except FileNotFoundError as e: logger.error(str(e)) raise results = [] # Seed for San Pablo (Traditional Bakery) logger.info("") result_san_pablo = await seed_suppliers_for_tenant( db, DEMO_TENANT_SAN_PABLO, "Panadería San Pablo (Traditional)", suppliers_data ) results.append(result_san_pablo) # Seed for La Espiga (Central Workshop) result_la_espiga = await seed_suppliers_for_tenant( db, DEMO_TENANT_LA_ESPIGA, "Panadería La Espiga (Central Workshop)", suppliers_data ) results.append(result_la_espiga) # Calculate totals total_suppliers = sum(r["suppliers_created"] for r in results) total_price_lists = sum(r["price_lists_created"] for r in results) total_skipped = sum(r["suppliers_skipped"] for r in results) logger.info("=" * 80) logger.info("✅ Demo Suppliers Seeding Completed") logger.info("=" * 80) return { "service": "suppliers", "tenants_seeded": len(results), "total_suppliers_created": total_suppliers, "total_price_lists_created": total_price_lists, "total_skipped": total_skipped, "results": results } async def main(): """Main execution function""" logger.info("Demo Suppliers 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("SUPPLIERS_DATABASE_URL") or os.getenv("DATABASE_URL") if not database_url: logger.error("❌ SUPPLIERS_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 suppliers database") # Create engine and session engine = create_async_engine( database_url, echo=False, pool_pre_ping=True, pool_size=5, max_overflow=10 ) session_maker = sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) try: async with session_maker() as session: result = await seed_suppliers(session) logger.info("") logger.info("📊 Seeding Summary:") logger.info(f" ✅ Tenants seeded: {result['tenants_seeded']}") logger.info(f" ✅ Suppliers created: {result['total_suppliers_created']}") logger.info(f" ✅ Price lists created: {result['total_price_lists_created']}") logger.info(f" ⏭️ 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['suppliers_created']} suppliers, " f"{tenant_result['price_lists_created']} price lists" ) logger.info("") logger.info("🎉 Success! Supplier catalog is ready for cloning.") logger.info("") logger.info("Suppliers created:") logger.info(" • Molinos San José S.L. (harinas)") logger.info(" • Lácteos del Valle S.A. (lácteos)") logger.info(" • Lesaffre Ibérica (levaduras)") logger.info(" • And 9 more suppliers...") logger.info("") logger.info("Note: All IDs are pre-defined and hardcoded for cross-service consistency") logger.info("") return 0 except Exception as e: logger.error("=" * 80) logger.error("❌ Demo Suppliers 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)