#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Demo Tenant Seeding Script for Tenant Service Creates the two demo template tenants: San Pablo and La Espiga This script runs as a Kubernetes init job inside the tenant-service container. It creates template tenants that will be cloned for demo sessions. Usage: python /app/scripts/demo/seed_demo_tenants.py Environment Variables Required: TENANT_DATABASE_URL - PostgreSQL connection string for tenant database AUTH_SERVICE_URL - URL of auth service (optional, for user creation) DEMO_MODE - Set to 'production' for production seeding LOG_LEVEL - Logging level (default: INFO) """ import asyncio import uuid import sys import os 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.tenants import Tenant # 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 (these are the template tenants that will be cloned) DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") TENANTS_DATA = [ { "id": DEMO_TENANT_SAN_PABLO, "name": "Panadería San Pablo", "business_model": "san_pablo", "subscription_tier": "demo_template", "is_demo": False, # Template tenants are not marked as demo "is_demo_template": True, # They are templates for cloning "is_active": True, # Required fields "address": "Calle Mayor 45", "city": "Madrid", "postal_code": "28013", "owner_id": uuid.UUID("c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6"), # María García López (San Pablo owner) "metadata_": { "type": "traditional_bakery", "description": "Panadería tradicional familiar con venta al público", "characteristics": [ "Producción en lotes pequeños adaptados a la demanda diaria", "Venta directa al consumidor final (walk-in customers)", "Ciclos de producción diarios comenzando de madrugada", "Variedad limitada de productos clásicos", "Proveedores locales de confianza", "Atención personalizada al cliente", "Ubicación en zona urbana residencial" ], "location_type": "urban", "size": "small", "employees": 8, "opening_hours": "07:00-21:00", "production_shifts": 1, "target_market": "local_consumers" } }, { "id": DEMO_TENANT_LA_ESPIGA, "name": "Panadería La Espiga - Obrador Central", "business_model": "la_espiga", "subscription_tier": "demo_template", "is_demo": False, "is_demo_template": True, "is_active": True, # Required fields "address": "Polígono Industrial Las Rozas, Nave 12", "city": "Las Rozas de Madrid", "postal_code": "28232", "owner_id": uuid.UUID("d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7"), # Carlos Martínez Ruiz (La Espiga owner) "metadata_": { "type": "central_workshop", "description": "Obrador central con distribución mayorista B2B", "characteristics": [ "Producción industrial en lotes grandes", "Distribución a clientes mayoristas (hoteles, restaurantes, supermercados)", "Operación 24/7 con múltiples turnos de producción", "Amplia variedad de productos estandarizados", "Proveedores regionales con contratos de volumen", "Logística de distribución optimizada", "Ubicación en polígono industrial" ], "location_type": "industrial", "size": "large", "employees": 25, "opening_hours": "24/7", "production_shifts": 3, "distribution_radius_km": 50, "target_market": "b2b_wholesale", "production_capacity_kg_day": 2000 } } ] async def seed_tenants(db: AsyncSession) -> dict: """ Seed the demo template tenants Returns: Dict with seeding statistics """ logger.info("=" * 80) logger.info("🏢 Starting Demo Tenant Seeding") logger.info("=" * 80) created_count = 0 updated_count = 0 for tenant_data in TENANTS_DATA: tenant_id = tenant_data["id"] tenant_name = tenant_data["name"] # Check if tenant already exists result = await db.execute( select(Tenant).where(Tenant.id == tenant_id) ) existing_tenant = result.scalars().first() if existing_tenant: logger.info( "Tenant already exists - updating", tenant_id=str(tenant_id), tenant_name=tenant_name ) # Update existing tenant for key, value in tenant_data.items(): if key != "id": # Don't update the ID setattr(existing_tenant, key, value) existing_tenant.updated_at = datetime.now(timezone.utc) updated_count += 1 else: logger.info( "Creating new tenant", tenant_id=str(tenant_id), tenant_name=tenant_name ) # Create new tenant tenant = Tenant(**tenant_data) db.add(tenant) created_count += 1 # Commit all changes await db.commit() logger.info("=" * 80) logger.info( "✅ Demo Tenant Seeding Completed", created=created_count, updated=updated_count, total=len(TENANTS_DATA) ) logger.info("=" * 80) return { "service": "tenant", "created": created_count, "updated": updated_count, "total": len(TENANTS_DATA) } async def main(): """Main execution function""" logger.info("Demo Tenant 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("TENANT_DATABASE_URL") or os.getenv("DATABASE_URL") if not database_url: logger.error("❌ TENANT_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 tenant 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_tenants(session) logger.info("") logger.info("📊 Seeding Summary:") logger.info(f" ✅ Created: {result['created']}") logger.info(f" 🔄 Updated: {result['updated']}") logger.info(f" 📦 Total: {result['total']}") logger.info("") logger.info("🎉 Success! Template tenants are ready for cloning.") logger.info("") logger.info("Next steps:") logger.info(" 1. Run seed jobs for other services (inventory, recipes, etc.)") logger.info(" 2. Verify tenant data in database") logger.info(" 3. Test demo session creation") logger.info("") return 0 except Exception as e: logger.error("=" * 80) logger.error("❌ Demo Tenant 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)