New enterprise feature

This commit is contained in:
Urtzi Alfaro
2025-11-30 09:12:40 +01:00
parent f9d0eec6ec
commit 972db02f6d
176 changed files with 19741 additions and 1361 deletions

View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
"""
Demo Tenant Seeding Script for Tenant Service
Creates the two demo template tenants: San Pablo and La Espiga
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.
@@ -46,75 +46,193 @@ structlog.configure(
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")
# 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_SAN_PABLO,
"name": "Panadería San Pablo",
"business_model": "san_pablo",
"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 Mayor 45",
"address": "Calle de Fuencarral, 85",
"city": "Madrid",
"postal_code": "28013",
"owner_id": uuid.UUID("c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6"), # María García López (San Pablo owner)
"postal_code": "28004",
"owner_id": uuid.UUID("c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6"), # Professional bakery owner
"metadata_": {
"type": "traditional_bakery",
"description": "Panadería tradicional familiar con venta al público",
"type": "professional_bakery",
"description": "Modern professional bakery combining artisan quality with operational efficiency",
"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"
"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": "small",
"employees": 8,
"size": "medium",
"employees": 12,
"opening_hours": "07:00-21:00",
"production_shifts": 1,
"target_market": "local_consumers"
"target_market": "b2c_and_local_b2b",
"production_capacity_kg_day": 300,
"sales_channels": ["retail", "online", "catering"]
}
},
{
"id": DEMO_TENANT_LA_ESPIGA,
"name": "Panadería La Espiga - Obrador Central",
"business_model": "la_espiga",
"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": "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)
"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": "central_workshop",
"description": "Obrador central con distribución mayorista B2B",
"type": "retail_outlet",
"description": "Retail outlet in Madrid city center",
"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"
"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": "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
"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"
}
}
]
@@ -174,7 +292,7 @@ async def seed_tenants(db: AsyncSession) -> dict:
# Flush to get tenant IDs before creating subscriptions
await db.flush()
# Create demo subscriptions for all tenants (enterprise tier for full demo access)
# 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
@@ -188,7 +306,7 @@ async def seed_tenants(db: AsyncSession) -> dict:
)
existing_subscription = result.scalars().first()
except Exception as e:
# If there's a column error (like missing cancellation_effective_date),
# 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.")
@@ -197,28 +315,183 @@ async def seed_tenants(db: AsyncSession) -> dict:
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="enterprise"
plan=plan
)
subscription = Subscription(
tenant_id=tenant_id,
plan="enterprise", # Demo templates get full access
plan=plan,
status="active",
monthly_price=0.0, # Free for demo
billing_cycle="monthly",
max_users=-1, # Unlimited
max_locations=-1,
max_products=-1,
max_users=-1, # Unlimited for demo
max_locations=max_locations,
max_products=-1, # Unlimited for demo
features={}
)
db.add(subscription)
# Commit all changes
# 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",