Improve the demo feature of the project

This commit is contained in:
Urtzi Alfaro
2025-10-12 18:47:33 +02:00
parent dbc7f2fa0d
commit 7556a00db7
168 changed files with 10102 additions and 18869 deletions

View File

@@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""
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_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6")
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7")
SUBSCRIPTIONS_DATA = [
{
"tenant_id": DEMO_TENANT_SAN_PABLO,
"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), # 90 days for demo
},
{
"tenant_id": DEMO_TENANT_LA_ESPIGA,
"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),
}
]
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

@@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""
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)