Files
bakery-ia/services/tenant/scripts/demo/seed_demo_tenant_members.py
2025-11-30 09:12:40 +01:00

400 lines
13 KiB
Python

#!/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_PROFESSIONAL = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6")
# 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_PROFESSIONAL,
"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_PROFESSIONAL,
"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_PROFESSIONAL,
"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_PROFESSIONAL,
"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_PROFESSIONAL,
"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_PROFESSIONAL,
"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_PROFESSIONAL,
"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 (Professional Bakery - merged from San Pablo + La Espiga)
{
"tenant_id": DEMO_TENANT_PROFESSIONAL,
"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_PROFESSIONAL,
"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_PROFESSIONAL,
"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_PROFESSIONAL,
"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_PROFESSIONAL,
"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_PROFESSIONAL,
"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_PROFESSIONAL,
"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 member_data in TENANT_MEMBERS_DATA:
tenant_id = member_data["tenant_id"]
result = await db.execute(
select(Tenant).where(Tenant.id == tenant_id)
)
tenant = result.scalars().first()
if not tenant:
logger.error(
"Template tenant not found: %s",
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(
"✓ Template tenant found: %s",
tenant.name,
tenant_id=str(tenant_id),
tenant_name=tenant.name
)
break # Only need to verify one tenant exists, then proceed with member creation
# 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)