#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Demo Tenant Seeding Script for Tenant Service Creates demo template tenants: Professional Bakery and Enterprise Chain 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) # Professional demo (merged from San Pablo + La Espiga) DEMO_TENANT_PROFESSIONAL = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Enterprise chain demo (parent + 3 children) DEMO_TENANT_ENTERPRISE_CHAIN = uuid.UUID("c3d4e5f6-a7b8-49c0-d1e2-f3a4b5c6d7e8") DEMO_TENANT_CHILD_1 = uuid.UUID("d4e5f6a7-b8c9-40d1-e2f3-a4b5c6d7e8f9") DEMO_TENANT_CHILD_2 = uuid.UUID("e5f6a7b8-c9d0-41e2-f3a4-b5c6d7e8f9a0") DEMO_TENANT_CHILD_3 = uuid.UUID("f6a7b8c9-d0e1-42f3-a4b5-c6d7e8f9a0b1") TENANTS_DATA = [ { "id": DEMO_TENANT_PROFESSIONAL, "name": "Panadería Artesana Madrid", "business_model": "individual_bakery", "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 de Fuencarral, 85", "city": "Madrid", "postal_code": "28004", "owner_id": uuid.UUID("c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6"), # Professional bakery owner "metadata_": { "type": "professional_bakery", "description": "Modern professional bakery combining artisan quality with operational efficiency", "characteristics": [ "Local artisan production with modern equipment", "Omnichannel sales: retail + online + B2B catering", "AI-driven demand forecasting and inventory optimization", "Professional recipes and standardized processes", "Strong local supplier relationships", "Digital POS with customer tracking", "Production planning with waste minimization" ], "location_type": "urban", "size": "medium", "employees": 12, "opening_hours": "07:00-21:00", "production_shifts": 1, "target_market": "b2c_and_local_b2b", "production_capacity_kg_day": 300, "sales_channels": ["retail", "online", "catering"] } }, { "id": DEMO_TENANT_ENTERPRISE_CHAIN, "name": "Panadería Central - Obrador Madrid", "business_model": "enterprise_chain", "is_demo": False, "is_demo_template": True, "is_active": True, "tenant_type": "parent", # Parent tenant for enterprise chain # Required fields "address": "Polígono Industrial de Vicálvaro, Calle 15, Nave 8", "city": "Madrid", "postal_code": "28052", "latitude": 40.3954, "longitude": -3.6121, "owner_id": uuid.UUID("e3f4a5b6-c7d8-49e0-f1a2-b3c4d5e6f7a8"), # Enterprise Chain owner "metadata_": { "type": "enterprise_chain", "description": "Central production facility serving retail network across Spain", "characteristics": [ "Central production facility with distributed retail network", "Multiple retail outlets across major Spanish cities", "Centralized planning and inventory management", "Standardized processes across all locations", "Shared procurement and supplier relationships", "Cross-location inventory optimization with internal transfers", "Corporate-level business intelligence and reporting", "VRP-optimized distribution logistics" ], "location_type": "industrial", "size": "large", "employees": 45, "opening_hours": "24/7", "production_shifts": 2, "retail_outlets_count": 3, "target_market": "chain_retail", "production_capacity_kg_day": 3000, "distribution_range_km": 400 } }, { "id": DEMO_TENANT_CHILD_1, "name": "Panadería Central - Madrid Centro", "business_model": "retail_outlet", "is_demo": False, "is_demo_template": True, "is_active": True, # Required fields "address": "Calle Mayor, 45", "city": "Madrid", "postal_code": "28013", "latitude": 40.4168, "longitude": -3.7038, "owner_id": uuid.UUID("e3f4a5b6-c7d8-49e0-f1a2-b3c4d5e6f7a8"), # Same owner as parent enterprise "parent_tenant_id": DEMO_TENANT_ENTERPRISE_CHAIN, # Link to parent "tenant_type": "child", "metadata_": { "type": "retail_outlet", "description": "Retail outlet in Madrid city center", "characteristics": [ "Consumer-facing retail location in high-traffic area", "Tri-weekly delivery from central production", "Standardized product offering from central catalog", "Brand-consistent customer experience", "Part of enterprise network with internal transfer capability" ], "location_type": "retail", "size": "medium", "employees": 8, "opening_hours": "07:00-21:00", "target_market": "local_consumers", "foot_traffic": "high", "zone": "Centro" } }, { "id": DEMO_TENANT_CHILD_2, "name": "Panadería Central - Barcelona Gràcia", "business_model": "retail_outlet", "is_demo": False, "is_demo_template": True, "is_active": True, # Required fields "address": "Carrer de Verdi, 32", "city": "Barcelona", "postal_code": "08012", "latitude": 41.4036, "longitude": 2.1561, "owner_id": uuid.UUID("e3f4a5b6-c7d8-49e0-f1a2-b3c4d5e6f7a8"), # Same owner as parent enterprise "parent_tenant_id": DEMO_TENANT_ENTERPRISE_CHAIN, # Link to parent "tenant_type": "child", "metadata_": { "type": "retail_outlet", "description": "Retail outlet in Barcelona Gràcia neighborhood", "characteristics": [ "Consumer-facing retail location in trendy neighborhood", "Tri-weekly delivery from central production", "Standardized product offering from central catalog", "Brand-consistent customer experience", "Part of enterprise network with internal transfer capability" ], "location_type": "retail", "size": "medium", "employees": 7, "opening_hours": "07:00-21:30", "target_market": "local_consumers", "foot_traffic": "medium_high", "zone": "Gràcia" } }, { "id": DEMO_TENANT_CHILD_3, "name": "Panadería Central - Valencia Ruzafa", "business_model": "retail_outlet", "is_demo": False, "is_demo_template": True, "is_active": True, # Required fields "address": "Carrer de Sueca, 51", "city": "Valencia", "postal_code": "46006", "latitude": 39.4623, "longitude": -0.3645, "owner_id": uuid.UUID("e3f4a5b6-c7d8-49e0-f1a2-b3c4d5e6f7a8"), # Same owner as parent enterprise "parent_tenant_id": DEMO_TENANT_ENTERPRISE_CHAIN, # Link to parent "tenant_type": "child", "metadata_": { "type": "retail_outlet", "description": "Retail outlet in Valencia Ruzafa district", "characteristics": [ "Consumer-facing retail location in vibrant district", "Tri-weekly delivery from central production", "Standardized product offering from central catalog", "Brand-consistent customer experience", "Part of enterprise network with internal transfer capability" ], "location_type": "retail", "size": "medium", "employees": 6, "opening_hours": "06:30-21:00", "target_market": "local_consumers", "foot_traffic": "medium", "zone": "Ruzafa" } } ] 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 # Flush to get tenant IDs before creating subscriptions await db.flush() # Create demo subscriptions for all tenants with proper tier assignments from app.models.tenants import Subscription # 'select' is already imported at the top of the file, so no need to import locally for tenant_data in TENANTS_DATA: tenant_id = tenant_data["id"] # Check if subscription already exists try: result = await db.execute( select(Subscription).where(Subscription.tenant_id == tenant_id) ) existing_subscription = result.scalars().first() except Exception as e: # If there's a column error (like missing cancellation_effective_date), # we need to ensure migrations are applied first if "does not exist" in str(e): logger.error("Database schema does not match model. Ensure migrations are applied first.") raise else: raise # Re-raise if it's a different error if not existing_subscription: # Determine subscription tier based on tenant type if tenant_id == DEMO_TENANT_PROFESSIONAL: plan = "professional" max_locations = 3 elif tenant_id in [DEMO_TENANT_ENTERPRISE_CHAIN, DEMO_TENANT_CHILD_1, DEMO_TENANT_CHILD_2, DEMO_TENANT_CHILD_3]: plan = "enterprise" max_locations = -1 # Unlimited else: plan = "starter" max_locations = 1 logger.info( "Creating demo subscription for tenant", tenant_id=str(tenant_id), plan=plan ) subscription = Subscription( tenant_id=tenant_id, plan=plan, status="active", monthly_price=0.0, # Free for demo billing_cycle="monthly", max_users=-1, # Unlimited for demo max_locations=max_locations, max_products=-1, # Unlimited for demo features={} ) db.add(subscription) # Commit the tenants and subscriptions first await db.commit() # Create TenantLocation records for enterprise template tenants from app.models.tenant_location import TenantLocation logger.info("Creating TenantLocation records for enterprise template tenants") # After committing tenants and subscriptions, create location records # Parent location - Central Production parent_location = TenantLocation( id=uuid.uuid4(), tenant_id=DEMO_TENANT_ENTERPRISE_CHAIN, name="Obrador Madrid - Central Production", location_type="central_production", address="Polígono Industrial de Vicálvaro, Calle 15, Nave 8", city="Madrid", postal_code="28052", latitude=40.3954, longitude=-3.6121, capacity=3000, # kg/day operational_hours={ "monday": "00:00-23:59", "tuesday": "00:00-23:59", "wednesday": "00:00-23:59", "thursday": "00:00-23:59", "friday": "00:00-23:59", "saturday": "00:00-23:59", "sunday": "00:00-23:59" }, # 24/7 delivery_schedule_config={ "delivery_days": ["monday", "wednesday", "friday"], "time_window": "07:00-10:00" }, is_active=True, metadata_={"type": "production_facility", "zone": "industrial", "size": "large"} ) db.add(parent_location) # Child 1 location - Madrid Centro child1_location = TenantLocation( id=uuid.uuid4(), tenant_id=DEMO_TENANT_CHILD_1, name="Madrid Centro - Retail Outlet", location_type="retail_outlet", address="Calle Mayor, 45", city="Madrid", postal_code="28013", latitude=40.4168, longitude=-3.7038, delivery_windows={ "monday": "07:00-10:00", "wednesday": "07:00-10:00", "friday": "07:00-10:00" }, operational_hours={ "monday": "07:00-21:00", "tuesday": "07:00-21:00", "wednesday": "07:00-21:00", "thursday": "07:00-21:00", "friday": "07:00-21:00", "saturday": "08:00-21:00", "sunday": "09:00-21:00" }, delivery_schedule_config={ "delivery_days": ["monday", "wednesday", "friday"], "time_window": "07:00-10:00" }, is_active=True, metadata_={"type": "retail_outlet", "zone": "center", "size": "medium", "foot_traffic": "high"} ) db.add(child1_location) # Child 2 location - Barcelona Gràcia child2_location = TenantLocation( id=uuid.uuid4(), tenant_id=DEMO_TENANT_CHILD_2, name="Barcelona Gràcia - Retail Outlet", location_type="retail_outlet", address="Carrer de Verdi, 32", city="Barcelona", postal_code="08012", latitude=41.4036, longitude=2.1561, delivery_windows={ "monday": "07:00-10:00", "wednesday": "07:00-10:00", "friday": "07:00-10:00" }, operational_hours={ "monday": "07:00-21:30", "tuesday": "07:00-21:30", "wednesday": "07:00-21:30", "thursday": "07:00-21:30", "friday": "07:00-21:30", "saturday": "08:00-21:30", "sunday": "09:00-21:00" }, delivery_schedule_config={ "delivery_days": ["monday", "wednesday", "friday"], "time_window": "07:00-10:00" }, is_active=True, metadata_={"type": "retail_outlet", "zone": "gracia", "size": "medium", "foot_traffic": "medium_high"} ) db.add(child2_location) # Child 3 location - Valencia Ruzafa child3_location = TenantLocation( id=uuid.uuid4(), tenant_id=DEMO_TENANT_CHILD_3, name="Valencia Ruzafa - Retail Outlet", location_type="retail_outlet", address="Carrer de Sueca, 51", city="Valencia", postal_code="46006", latitude=39.4623, longitude=-0.3645, delivery_windows={ "monday": "07:00-10:00", "wednesday": "07:00-10:00", "friday": "07:00-10:00" }, operational_hours={ "monday": "06:30-21:00", "tuesday": "06:30-21:00", "wednesday": "06:30-21:00", "thursday": "06:30-21:00", "friday": "06:30-21:00", "saturday": "07:00-21:00", "sunday": "08:00-21:00" }, delivery_schedule_config={ "delivery_days": ["monday", "wednesday", "friday"], "time_window": "07:00-10:00" }, is_active=True, metadata_={"type": "retail_outlet", "zone": "ruzafe", "size": "medium", "foot_traffic": "medium"} ) db.add(child3_location) # Commit the location records await db.commit() logger.info("Created 4 TenantLocation records for enterprise templates") 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)