demo seed change

This commit is contained in:
Urtzi Alfaro
2025-12-13 23:57:54 +01:00
parent f3688dfb04
commit ff830a3415
299 changed files with 20328 additions and 19485 deletions

View File

@@ -1,308 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Demo Subscription Seeding Script for Tenant Service
Creates subscriptions for demo template tenants
This script creates subscription records for the demo template tenants
so they have proper subscription limits and features.
Usage:
python /app/scripts/demo/seed_demo_subscriptions.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, timedelta
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 Subscription
# 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_PROFESSIONAL = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6")
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")
SUBSCRIPTIONS_DATA = [
{
"tenant_id": DEMO_TENANT_PROFESSIONAL,
"plan": "professional",
"status": "active",
"monthly_price": 0.0, # Free for demo
"max_users": -1, # Unlimited users for demo
"max_locations": 3, # Professional tier limit (will be upgraded for demo sessions)
"max_products": -1, # Unlimited products for demo
"features": {
"inventory_management": "advanced",
"demand_prediction": "advanced",
"production_reports": "advanced",
"analytics": "advanced",
"support": "priority",
"ai_model_configuration": "advanced",
"multi_location": True,
"custom_integrations": True,
"api_access": True,
"dedicated_support": False
},
"trial_ends_at": None,
"next_billing_date": datetime.now(timezone.utc) + timedelta(days=90), # 90 days for demo
},
{
"tenant_id": DEMO_TENANT_ENTERPRISE_CHAIN,
"plan": "enterprise",
"status": "active",
"monthly_price": 0.0, # Free for demo
"max_users": -1, # Unlimited users
"max_locations": -1, # Unlimited locations
"max_products": -1, # Unlimited products
"features": {
"inventory_management": "advanced",
"demand_prediction": "advanced",
"production_reports": "advanced",
"analytics": "predictive",
"support": "priority",
"ai_model_configuration": "advanced",
"multi_location": True,
"custom_integrations": True,
"api_access": True,
"dedicated_support": True
},
"trial_ends_at": None,
"next_billing_date": datetime.now(timezone.utc) + timedelta(days=90),
},
{
"tenant_id": DEMO_TENANT_CHILD_1,
"plan": "enterprise", # Child inherits parent's enterprise plan
"status": "active",
"monthly_price": 0.0, # Free for demo
"max_users": -1, # Unlimited users
"max_locations": 1, # Single location
"max_products": -1, # Unlimited products
"features": {
"inventory_management": "advanced",
"demand_prediction": "advanced",
"production_reports": "advanced",
"analytics": "predictive",
"support": "priority",
"ai_model_configuration": "advanced",
"multi_location": True,
"custom_integrations": True,
"api_access": True,
"dedicated_support": True
},
"trial_ends_at": None,
"next_billing_date": datetime.now(timezone.utc) + timedelta(days=90),
},
{
"tenant_id": DEMO_TENANT_CHILD_2,
"plan": "enterprise", # Child inherits parent's enterprise plan
"status": "active",
"monthly_price": 0.0, # Free for demo
"max_users": -1, # Unlimited users
"max_locations": 1, # Single location
"max_products": -1, # Unlimited products
"features": {
"inventory_management": "advanced",
"demand_prediction": "advanced",
"production_reports": "advanced",
"analytics": "predictive",
"support": "priority",
"ai_model_configuration": "advanced",
"multi_location": True,
"custom_integrations": True,
"api_access": True,
"dedicated_support": True
},
"trial_ends_at": None,
"next_billing_date": datetime.now(timezone.utc) + timedelta(days=90),
},
{
"tenant_id": DEMO_TENANT_CHILD_3,
"plan": "enterprise", # Child inherits parent's enterprise plan
"status": "active",
"monthly_price": 0.0, # Free for demo
"max_users": -1, # Unlimited users
"max_locations": 1, # Single location
"max_products": -1, # Unlimited products
"features": {
"inventory_management": "advanced",
"demand_prediction": "advanced",
"production_reports": "advanced",
"analytics": "predictive",
"support": "priority",
"ai_model_configuration": "advanced",
"multi_location": True,
"custom_integrations": True,
"api_access": True,
"dedicated_support": True
},
"trial_ends_at": None,
"next_billing_date": datetime.now(timezone.utc) + timedelta(days=90),
}
]
async def seed_subscriptions(db: AsyncSession) -> dict:
"""
Seed subscriptions for demo template tenants
Returns:
Dict with seeding statistics
"""
logger.info("=" * 80)
logger.info("💳 Starting Demo Subscription Seeding")
logger.info("=" * 80)
created_count = 0
updated_count = 0
for subscription_data in SUBSCRIPTIONS_DATA:
tenant_id = subscription_data["tenant_id"]
# Check if subscription already exists for this tenant
result = await db.execute(
select(Subscription).where(
Subscription.tenant_id == tenant_id,
Subscription.status == "active"
)
)
existing_subscription = result.scalars().first()
if existing_subscription:
logger.info(
"Subscription already exists - updating",
tenant_id=str(tenant_id),
subscription_id=str(existing_subscription.id)
)
# Update existing subscription
for key, value in subscription_data.items():
if key != "tenant_id": # Don't update the tenant_id
setattr(existing_subscription, key, value)
existing_subscription.updated_at = datetime.now(timezone.utc)
updated_count += 1
else:
logger.info(
"Creating new subscription",
tenant_id=str(tenant_id),
plan=subscription_data["plan"]
)
# Create new subscription
subscription = Subscription(**subscription_data)
db.add(subscription)
created_count += 1
# Commit all changes
await db.commit()
logger.info("=" * 80)
logger.info(
"✅ Demo Subscription Seeding Completed",
created=created_count,
updated=updated_count,
total=len(SUBSCRIPTIONS_DATA)
)
logger.info("=" * 80)
return {
"service": "subscriptions",
"created": created_count,
"updated": updated_count,
"total": len(SUBSCRIPTIONS_DATA)
}
async def main():
"""Main execution function"""
logger.info("Demo Subscription 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_subscriptions(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! Demo subscriptions are ready.")
logger.info("")
return 0
except Exception as e:
logger.error("=" * 80)
logger.error("❌ Demo Subscription 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)

View File

@@ -1,399 +0,0 @@
#!/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)

View File

@@ -1,580 +0,0 @@
#!/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)