#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Demo Tenant Members Seeding Script for Tenant Service Links demo staff users to their respective template tenants This script creates TenantMember records that link the demo staff users (created by auth service) to the demo template tenants. Without these links, staff users won't appear in the "Gestión de equipos" (team management) section. Usage: python /app/scripts/demo/seed_demo_tenant_members.py Environment Variables Required: TENANT_DATABASE_URL - PostgreSQL connection string for tenant database 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 import json from app.models.tenants import TenantMember, 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 (must match seed_demo_tenants.py) DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # Owner user IDs (must match seed_demo_users.py) OWNER_SAN_PABLO = uuid.UUID("c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6") # María García López OWNER_LA_ESPIGA = uuid.UUID("d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7") # Carlos Martínez Ruiz def get_permissions_for_role(role: str) -> str: """Get default permissions JSON string for a role""" permission_map = { "owner": ["read", "write", "admin", "delete"], "admin": ["read", "write", "admin"], "production_manager": ["read", "write"], "baker": ["read", "write"], "sales": ["read", "write"], "quality_control": ["read", "write"], "warehouse": ["read", "write"], "logistics": ["read", "write"], "procurement": ["read", "write"], "maintenance": ["read", "write"], "member": ["read", "write"], "viewer": ["read"] } permissions = permission_map.get(role, ["read"]) return json.dumps(permissions) # Tenant Members Data # These IDs and roles must match usuarios_staff_es.json TENANT_MEMBERS_DATA = [ # San Pablo Members (Panadería Individual) { "tenant_id": DEMO_TENANT_SAN_PABLO, "user_id": uuid.UUID("c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6"), # María García López "role": "owner", "invited_by": uuid.UUID("c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6"), "is_owner": True }, { "tenant_id": DEMO_TENANT_SAN_PABLO, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000001"), # Juan Pérez Moreno - Panadero Senior "role": "baker", "invited_by": OWNER_SAN_PABLO, "is_owner": False }, { "tenant_id": DEMO_TENANT_SAN_PABLO, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000002"), # Ana Rodríguez Sánchez - Responsable de Ventas "role": "sales", "invited_by": OWNER_SAN_PABLO, "is_owner": False }, { "tenant_id": DEMO_TENANT_SAN_PABLO, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000003"), # Luis Fernández García - Inspector de Calidad "role": "quality_control", "invited_by": OWNER_SAN_PABLO, "is_owner": False }, { "tenant_id": DEMO_TENANT_SAN_PABLO, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000004"), # Carmen López Martínez - Administradora "role": "admin", "invited_by": OWNER_SAN_PABLO, "is_owner": False }, { "tenant_id": DEMO_TENANT_SAN_PABLO, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000005"), # Pedro González Torres - Encargado de Almacén "role": "warehouse", "invited_by": OWNER_SAN_PABLO, "is_owner": False }, { "tenant_id": DEMO_TENANT_SAN_PABLO, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000006"), # Isabel Romero Díaz - Jefa de Producción "role": "production_manager", "invited_by": OWNER_SAN_PABLO, "is_owner": False }, # La Espiga Members (Obrador Central) { "tenant_id": DEMO_TENANT_LA_ESPIGA, "user_id": uuid.UUID("d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7"), # Carlos Martínez Ruiz "role": "owner", "invited_by": uuid.UUID("d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7"), "is_owner": True }, { "tenant_id": DEMO_TENANT_LA_ESPIGA, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000011"), # Roberto Sánchez Vargas - Director de Producción "role": "production_manager", "invited_by": OWNER_LA_ESPIGA, "is_owner": False }, { "tenant_id": DEMO_TENANT_LA_ESPIGA, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000012"), # Sofía Jiménez Ortega - Responsable de Control de Calidad "role": "quality_control", "invited_by": OWNER_LA_ESPIGA, "is_owner": False }, { "tenant_id": DEMO_TENANT_LA_ESPIGA, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000013"), # Miguel Herrera Castro - Coordinador de Logística "role": "logistics", "invited_by": OWNER_LA_ESPIGA, "is_owner": False }, { "tenant_id": DEMO_TENANT_LA_ESPIGA, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000014"), # Elena Morales Ruiz - Directora Comercial "role": "sales", "invited_by": OWNER_LA_ESPIGA, "is_owner": False }, { "tenant_id": DEMO_TENANT_LA_ESPIGA, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000015"), # Javier Navarro Prieto - Responsable de Compras "role": "procurement", "invited_by": OWNER_LA_ESPIGA, "is_owner": False }, { "tenant_id": DEMO_TENANT_LA_ESPIGA, "user_id": uuid.UUID("50000000-0000-0000-0000-000000000016"), # Laura Delgado Santos - Técnica de Mantenimiento "role": "maintenance", "invited_by": OWNER_LA_ESPIGA, "is_owner": False }, ] async def seed_tenant_members(db: AsyncSession) -> dict: """ Seed tenant members for demo template tenants Returns: Dict with seeding statistics """ logger.info("=" * 80) logger.info("👥 Starting Demo Tenant Members Seeding") logger.info("=" * 80) created_count = 0 updated_count = 0 skipped_count = 0 # First, verify that template tenants exist for tenant_id in [DEMO_TENANT_SAN_PABLO, DEMO_TENANT_LA_ESPIGA]: result = await db.execute( select(Tenant).where(Tenant.id == tenant_id) ) tenant = result.scalars().first() if not tenant: logger.error( f"Template tenant not found: {tenant_id}", tenant_id=str(tenant_id) ) logger.error("Please run seed_demo_tenants.py first!") return { "service": "tenant_members", "created": 0, "updated": 0, "skipped": 0, "error": "Template tenants not found" } logger.info( f"✓ Template tenant found: {tenant.name}", tenant_id=str(tenant_id), tenant_name=tenant.name ) # Now seed the tenant members for member_data in TENANT_MEMBERS_DATA: tenant_id = member_data["tenant_id"] user_id = member_data["user_id"] role = member_data["role"] invited_by = member_data["invited_by"] is_owner = member_data.get("is_owner", False) # Check if member already exists result = await db.execute( select(TenantMember).where( TenantMember.tenant_id == tenant_id, TenantMember.user_id == user_id ) ) existing_member = result.scalars().first() if existing_member: # Member exists - check if update needed needs_update = ( existing_member.role != role or existing_member.is_active != True or existing_member.invited_by != invited_by ) if needs_update: logger.info( "Tenant member exists - updating", tenant_id=str(tenant_id), user_id=str(user_id), old_role=existing_member.role, new_role=role ) existing_member.role = role existing_member.is_active = True existing_member.invited_by = invited_by existing_member.permissions = get_permissions_for_role(role) existing_member.updated_at = datetime.now(timezone.utc) updated_count += 1 else: logger.debug( "Tenant member already exists - skipping", tenant_id=str(tenant_id), user_id=str(user_id), role=role ) skipped_count += 1 continue # Create new tenant member logger.info( "Creating tenant member", tenant_id=str(tenant_id), user_id=str(user_id), role=role, is_owner=is_owner ) tenant_member = TenantMember( tenant_id=tenant_id, user_id=user_id, role=role, permissions=get_permissions_for_role(role), is_active=True, invited_by=invited_by, invited_at=datetime.now(timezone.utc), joined_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc) ) db.add(tenant_member) created_count += 1 # Commit all changes await db.commit() logger.info("=" * 80) logger.info( "✅ Demo Tenant Members Seeding Completed", created=created_count, updated=updated_count, skipped=skipped_count, total=len(TENANT_MEMBERS_DATA) ) logger.info("=" * 80) return { "service": "tenant_members", "created": created_count, "updated": updated_count, "skipped": skipped_count, "total": len(TENANT_MEMBERS_DATA) } async def main(): """Main execution function""" logger.info("Demo Tenant Members Seeding Script Starting") 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_tenant_members(session) if "error" in result: logger.error(f"❌ Seeding failed: {result['error']}") return 1 logger.info("") logger.info("📊 Seeding Summary:") logger.info(f" ✅ Created: {result['created']}") logger.info(f" 🔄 Updated: {result['updated']}") logger.info(f" ⏭️ Skipped: {result['skipped']}") logger.info(f" 📦 Total: {result['total']}") logger.info("") logger.info("🎉 Success! Demo staff users are now linked to their tenants.") logger.info("") logger.info("Next steps:") logger.info(" 1. Verify tenant members in database") logger.info(" 2. Test 'Gestión de equipos' in the frontend") logger.info(" 3. All staff users should now be visible!") logger.info("") return 0 except Exception as e: logger.error("=" * 80) logger.error("❌ Demo Tenant Members 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)