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

@@ -44,13 +44,39 @@ structlog.configure(
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")
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_SAN_PABLO,
"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
@@ -70,15 +96,61 @@ SUBSCRIPTIONS_DATA = [
"dedicated_support": True
},
"trial_ends_at": None,
"next_billing_date": datetime.now(timezone.utc) + timedelta(days=90), # 90 days for demo
"next_billing_date": datetime.now(timezone.utc) + timedelta(days=90),
},
{
"tenant_id": DEMO_TENANT_LA_ESPIGA,
"plan": "enterprise",
"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, # Unlimited locations
"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",

View File

@@ -46,8 +46,7 @@ structlog.configure(
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")
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
@@ -80,100 +79,100 @@ def get_permissions_for_role(role: str) -> str:
TENANT_MEMBERS_DATA = [
# San Pablo Members (Panadería Individual)
{
"tenant_id": DEMO_TENANT_SAN_PABLO,
"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_SAN_PABLO,
"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_SAN_PABLO,
"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_SAN_PABLO,
"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_SAN_PABLO,
"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_SAN_PABLO,
"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_SAN_PABLO,
"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 (Obrador Central)
# La Espiga Members (Professional Bakery - merged from San Pablo + La Espiga)
{
"tenant_id": DEMO_TENANT_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_LA_ESPIGA,
"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_LA_ESPIGA,
"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_LA_ESPIGA,
"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_LA_ESPIGA,
"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_LA_ESPIGA,
"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_LA_ESPIGA,
"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,
@@ -198,7 +197,8 @@ async def seed_tenant_members(db: AsyncSession) -> dict:
skipped_count = 0
# First, verify that template tenants exist
for tenant_id in [DEMO_TENANT_SAN_PABLO, DEMO_TENANT_LA_ESPIGA]:
for member_data in TENANT_MEMBERS_DATA:
tenant_id = member_data["tenant_id"]
result = await db.execute(
select(Tenant).where(Tenant.id == tenant_id)
)
@@ -206,8 +206,8 @@ async def seed_tenant_members(db: AsyncSession) -> dict:
if not tenant:
logger.error(
f"Template tenant not found: {tenant_id}",
tenant_id=str(tenant_id)
"Template tenant not found: %s",
str(tenant_id)
)
logger.error("Please run seed_demo_tenants.py first!")
return {
@@ -219,10 +219,12 @@ async def seed_tenant_members(db: AsyncSession) -> dict:
}
logger.info(
f"✓ Template tenant found: {tenant.name}",
"✓ 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:

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",