Add DEMO feature to the project

This commit is contained in:
Urtzi Alfaro
2025-10-03 14:09:34 +02:00
parent 1243c2ca6d
commit dc8221bd2f
77 changed files with 6251 additions and 1074 deletions

1
scripts/demo/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Demo Data Seeding Scripts"""

View File

@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""
Clone Demo Tenant Data - Database Level
Clones all data from base template tenant to a virtual demo tenant across all databases
"""
import asyncio
import sys
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import select
import uuid
import structlog
# Add app to path for imports
sys.path.insert(0, '/app')
logger = structlog.get_logger()
# Base template tenant IDs
DEMO_TENANT_SAN_PABLO = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"
DEMO_TENANT_LA_ESPIGA = "b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7"
async def clone_inventory_data(base_tenant_id: str, virtual_tenant_id: str):
"""Clone inventory database tables using ORM"""
database_url = os.getenv("INVENTORY_DATABASE_URL")
if not database_url:
logger.warning("INVENTORY_DATABASE_URL not set, skipping inventory data")
return 0
engine = create_async_engine(database_url, echo=False)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
total_cloned = 0
try:
from app.models.inventory import Ingredient
async with session_factory() as session:
# Clone ingredients
result = await session.execute(
select(Ingredient).where(Ingredient.tenant_id == uuid.UUID(base_tenant_id))
)
base_ingredients = result.scalars().all()
logger.info(f"Found {len(base_ingredients)} ingredients to clone")
for ing in base_ingredients:
new_ing = Ingredient(
id=uuid.uuid4(),
tenant_id=uuid.UUID(virtual_tenant_id),
name=ing.name,
sku=ing.sku,
barcode=ing.barcode,
product_type=ing.product_type,
ingredient_category=ing.ingredient_category,
product_category=ing.product_category,
subcategory=ing.subcategory,
description=ing.description,
brand=ing.brand,
unit_of_measure=ing.unit_of_measure,
package_size=ing.package_size,
average_cost=ing.average_cost,
last_purchase_price=ing.last_purchase_price,
standard_cost=ing.standard_cost,
low_stock_threshold=ing.low_stock_threshold,
reorder_point=ing.reorder_point,
reorder_quantity=ing.reorder_quantity,
max_stock_level=ing.max_stock_level,
shelf_life_days=ing.shelf_life_days,
is_perishable=ing.is_perishable,
is_active=ing.is_active,
allergen_info=ing.allergen_info
)
session.add(new_ing)
total_cloned += 1
await session.commit()
logger.info(f"Cloned {total_cloned} ingredients")
except Exception as e:
logger.error(f"Failed to clone inventory data: {str(e)}", exc_info=True)
raise
finally:
await engine.dispose()
return total_cloned
async def clone_sales_data(base_tenant_id: str, virtual_tenant_id: str):
"""Clone sales database tables"""
database_url = os.getenv("SALES_DATABASE_URL")
if not database_url:
logger.warning("SALES_DATABASE_URL not set, skipping sales data")
return 0
# Sales cloning not implemented yet
logger.info("Sales data cloning not yet implemented")
return 0
async def clone_orders_data(base_tenant_id: str, virtual_tenant_id: str):
"""Clone orders database tables"""
database_url = os.getenv("ORDERS_DATABASE_URL")
if not database_url:
logger.warning("ORDERS_DATABASE_URL not set, skipping orders data")
return 0
# Orders cloning not implemented yet
logger.info("Orders data cloning not yet implemented")
return 0
async def create_virtual_tenant(virtual_tenant_id: str, demo_account_type: str):
"""Create the virtual tenant record in tenant database"""
database_url = os.getenv("TENANT_DATABASE_URL")
if not database_url:
logger.warning("TENANT_DATABASE_URL not set, skipping tenant creation")
return
engine = create_async_engine(database_url, echo=False)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
try:
# Import after adding to path
from services.tenant.app.models.tenants import Tenant
async with session_factory() as session:
# Check if tenant already exists
result = await session.execute(
select(Tenant).where(Tenant.id == uuid.UUID(virtual_tenant_id))
)
existing = result.scalars().first()
if existing:
logger.info(f"Virtual tenant {virtual_tenant_id} already exists")
return
# Create virtual tenant
tenant = Tenant(
id=uuid.UUID(virtual_tenant_id),
name=f"Demo Session Tenant",
is_demo=True,
is_demo_template=False,
business_model=demo_account_type
)
session.add(tenant)
await session.commit()
logger.info(f"Created virtual tenant {virtual_tenant_id}")
except ImportError:
# Tenant model not available, skip
logger.warning("Could not import Tenant model, skipping virtual tenant creation")
except Exception as e:
logger.error(f"Failed to create virtual tenant: {str(e)}", exc_info=True)
finally:
await engine.dispose()
async def clone_demo_tenant(virtual_tenant_id: str, demo_account_type: str = "individual_bakery"):
"""
Main function to clone all demo data for a virtual tenant
Args:
virtual_tenant_id: The UUID of the virtual tenant to create
demo_account_type: Type of demo account (individual_bakery or central_baker)
"""
base_tenant_id = DEMO_TENANT_SAN_PABLO if demo_account_type == "individual_bakery" else DEMO_TENANT_LA_ESPIGA
logger.info(
"Starting demo tenant cloning",
virtual_tenant=virtual_tenant_id,
base_tenant=base_tenant_id,
demo_type=demo_account_type
)
try:
# Create virtual tenant record
await create_virtual_tenant(virtual_tenant_id, demo_account_type)
# Clone data from each database
stats = {
"inventory": await clone_inventory_data(base_tenant_id, virtual_tenant_id),
"sales": await clone_sales_data(base_tenant_id, virtual_tenant_id),
"orders": await clone_orders_data(base_tenant_id, virtual_tenant_id),
}
total_records = sum(stats.values())
logger.info(
"Demo tenant cloning completed successfully",
virtual_tenant=virtual_tenant_id,
total_records=total_records,
stats=stats
)
# Print summary for job logs
print(f"✅ Cloning completed: {total_records} total records")
print(f" - Inventory: {stats['inventory']} records")
print(f" - Sales: {stats['sales']} records")
print(f" - Orders: {stats['orders']} records")
return True
except Exception as e:
logger.error(
"Demo tenant cloning failed",
virtual_tenant=virtual_tenant_id,
error=str(e),
exc_info=True
)
print(f"❌ Cloning failed: {str(e)}")
return False
if __name__ == "__main__":
# Get virtual tenant ID from environment or CLI argument
virtual_tenant_id = os.getenv("VIRTUAL_TENANT_ID") or (sys.argv[1] if len(sys.argv) > 1 else None)
demo_type = os.getenv("DEMO_ACCOUNT_TYPE", "individual_bakery")
if not virtual_tenant_id:
print("Usage: python clone_demo_tenant.py <virtual_tenant_id>")
print(" or: VIRTUAL_TENANT_ID=<uuid> python clone_demo_tenant.py")
sys.exit(1)
# Validate UUID
try:
uuid.UUID(virtual_tenant_id)
except ValueError:
print(f"Error: Invalid UUID format: {virtual_tenant_id}")
sys.exit(1)
result = asyncio.run(clone_demo_tenant(virtual_tenant_id, demo_type))
sys.exit(0 if result else 1)

View File

@@ -0,0 +1,278 @@
"""
Demo AI Models Seed Script
Creates fake AI models for demo tenants to populate the models list
without having actual trained model files.
"""
import asyncio
import sys
import os
from uuid import UUID
from datetime import datetime, timezone, timedelta
from decimal import Decimal
# Add project root to path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
from sqlalchemy import select
from shared.database.base import create_database_manager
import structlog
# Import models - these paths work both locally and in container
try:
# Container environment (training-service image)
from app.models.training import TrainedModel
except ImportError:
# Local environment
from services.training.app.models.training import TrainedModel
# Tenant model - define minimal version for container environment
try:
from services.tenant.app.models.tenants import Tenant
except ImportError:
# If running in training-service container, define minimal Tenant model
from sqlalchemy import Column, String, Boolean
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Tenant(Base):
__tablename__ = "tenants"
id = Column(PGUUID(as_uuid=True), primary_key=True)
name = Column(String)
is_demo = Column(Boolean)
is_demo_template = Column(Boolean)
logger = structlog.get_logger()
class DemoAIModelSeeder:
"""Seed fake AI models for demo tenants"""
def __init__(self):
self.training_db_url = os.getenv("TRAINING_DATABASE_URL")
self.tenant_db_url = os.getenv("TENANT_DATABASE_URL")
if not self.training_db_url or not self.tenant_db_url:
raise ValueError("Missing required database URLs")
self.training_db = create_database_manager(self.training_db_url, "demo-ai-seed")
self.tenant_db = create_database_manager(self.tenant_db_url, "demo-tenant-seed")
async def get_demo_tenants(self):
"""Get all demo tenants"""
async with self.tenant_db.get_session() as session:
result = await session.execute(
select(Tenant).where(Tenant.is_demo == True, Tenant.is_demo_template == True)
)
return result.scalars().all()
async def get_tenant_products(self, tenant_id: UUID):
"""
Get finished products for a tenant from inventory database.
We need to query the actual inventory to get real product UUIDs.
"""
try:
inventory_db_url = os.getenv("INVENTORY_DATABASE_URL")
if not inventory_db_url:
logger.warning("INVENTORY_DATABASE_URL not set, cannot get products")
return []
inventory_db = create_database_manager(inventory_db_url, "demo-inventory-check")
# Define minimal Ingredient model for querying
from sqlalchemy import Column, String, Enum as SQLEnum
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.ext.declarative import declarative_base
import enum
Base = declarative_base()
class IngredientType(str, enum.Enum):
INGREDIENT = "INGREDIENT"
FINISHED_PRODUCT = "FINISHED_PRODUCT"
class Ingredient(Base):
__tablename__ = "ingredients"
id = Column(PGUUID(as_uuid=True), primary_key=True)
tenant_id = Column(PGUUID(as_uuid=True))
name = Column(String)
ingredient_type = Column(SQLEnum(IngredientType, name="ingredienttype"))
async with inventory_db.get_session() as session:
result = await session.execute(
select(Ingredient).where(
Ingredient.tenant_id == tenant_id,
Ingredient.ingredient_type == IngredientType.FINISHED_PRODUCT
).limit(10) # Get up to 10 finished products
)
products = result.scalars().all()
product_list = [
{"id": product.id, "name": product.name}
for product in products
]
logger.info(f"Found {len(product_list)} finished products for tenant",
tenant_id=str(tenant_id))
return product_list
except Exception as e:
logger.error("Error fetching tenant products", error=str(e), tenant_id=str(tenant_id))
return []
async def create_fake_model(self, session, tenant_id: UUID, product_info: dict):
"""Create a fake AI model entry for a product"""
now = datetime.now(timezone.utc)
training_start = now - timedelta(days=90)
training_end = now - timedelta(days=7)
fake_model = TrainedModel(
tenant_id=tenant_id,
inventory_product_id=product_info["id"],
model_type="prophet_optimized",
model_version="1.0-demo",
job_id=f"demo-job-{tenant_id}-{product_info['id']}",
# Fake file paths (files don't actually exist)
model_path=f"/fake/models/{tenant_id}/{product_info['id']}/model.pkl",
metadata_path=f"/fake/models/{tenant_id}/{product_info['id']}/metadata.json",
# Fake but realistic metrics
mape=Decimal("12.5"), # Mean Absolute Percentage Error
mae=Decimal("2.3"), # Mean Absolute Error
rmse=Decimal("3.1"), # Root Mean Squared Error
r2_score=Decimal("0.85"), # R-squared
training_samples=60, # 60 days of training data
# Fake hyperparameters
hyperparameters={
"changepoint_prior_scale": 0.05,
"seasonality_prior_scale": 10.0,
"holidays_prior_scale": 10.0,
"seasonality_mode": "multiplicative"
},
# Features used
features_used=["weekday", "month", "is_holiday", "temperature", "precipitation"],
# Normalization params (fake)
normalization_params={
"temperature": {"mean": 15.0, "std": 5.0},
"precipitation": {"mean": 2.0, "std": 1.5}
},
# Model status
is_active=True,
is_production=False, # Demo models are not production-ready
# Training data info
training_start_date=training_start,
training_end_date=training_end,
data_quality_score=Decimal("0.75"), # Good but not excellent
# Metadata
notes="Demo model - No actual trained file exists. For demonstration purposes only.",
created_by="demo-seed-script",
created_at=now,
updated_at=now,
last_used_at=None
)
session.add(fake_model)
return fake_model
async def seed_models_for_tenant(self, tenant: Tenant):
"""Create fake AI models for a demo tenant"""
logger.info("Creating fake AI models for demo tenant",
tenant_id=str(tenant.id),
tenant_name=tenant.name)
try:
# Get products for this tenant
products = await self.get_tenant_products(tenant.id)
async with self.training_db.get_session() as session:
models_created = 0
for product in products:
# Check if model already exists
result = await session.execute(
select(TrainedModel).where(
TrainedModel.tenant_id == tenant.id,
TrainedModel.inventory_product_id == product["id"]
)
)
existing_model = result.scalars().first()
if existing_model:
logger.info("Model already exists, skipping",
tenant_id=str(tenant.id),
product_id=product["id"])
continue
# Create fake model
model = await self.create_fake_model(session, tenant.id, product)
models_created += 1
logger.info("Created fake AI model",
tenant_id=str(tenant.id),
product_id=product["id"],
model_id=str(model.id))
await session.commit()
logger.info("Successfully created fake AI models for tenant",
tenant_id=str(tenant.id),
models_created=models_created)
except Exception as e:
logger.error("Error creating fake AI models for tenant",
tenant_id=str(tenant.id),
error=str(e))
raise
async def seed_all_demo_models(self):
"""Seed fake AI models for all demo tenants"""
logger.info("Starting demo AI models seeding")
try:
# Get all demo tenants
demo_tenants = await self.get_demo_tenants()
if not demo_tenants:
logger.warning("No demo tenants found")
return
logger.info(f"Found {len(demo_tenants)} demo tenants")
# Seed models for each tenant
for tenant in demo_tenants:
await self.seed_models_for_tenant(tenant)
logger.info("✅ Demo AI models seeding completed successfully",
tenants_processed=len(demo_tenants))
except Exception as e:
logger.error("❌ Demo AI models seeding failed", error=str(e))
raise
async def main():
"""Main entry point"""
logger.info("Demo AI Models Seed Script started")
try:
seeder = DemoAIModelSeeder()
await seeder.seed_all_demo_models()
logger.info("Demo AI models seed completed successfully")
except Exception as e:
logger.error("Demo AI models seed failed", error=str(e))
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,338 @@
#!/usr/bin/env python3
"""
Seed Demo Inventory Data
Populates comprehensive Spanish inventory data for both demo tenants
"""
import asyncio
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import select, delete
import structlog
import uuid
from datetime import datetime, timedelta, timezone
logger = structlog.get_logger()
# Demo tenant IDs
DEMO_TENANT_SAN_PABLO = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"
DEMO_TENANT_LA_ESPIGA = "b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7"
async def seed_inventory_for_tenant(session, tenant_id: str, business_model: str):
"""Seed inventory data for a specific tenant"""
try:
from app.models.inventory import Ingredient, Stock, StockMovement
except ImportError:
from services.inventory.app.models.inventory import Ingredient, Stock, StockMovement
logger.info(f"Seeding inventory for {business_model}", tenant_id=tenant_id)
# Check if data already exists - if so, skip seeding to avoid duplicates
result = await session.execute(select(Ingredient).where(Ingredient.tenant_id == uuid.UUID(tenant_id)).limit(1))
existing = result.scalars().first()
if existing:
logger.info(f"Demo tenant {tenant_id} already has inventory data, skipping seed")
return
if business_model == "individual_bakery":
await seed_individual_bakery_inventory(session, tenant_id)
elif business_model == "central_baker_satellite":
await seed_central_baker_inventory(session, tenant_id)
async def seed_individual_bakery_inventory(session, tenant_id: str):
"""Seed inventory for individual bakery (produces locally)"""
try:
from app.models.inventory import Ingredient, Stock
except ImportError:
from services.inventory.app.models.inventory import Ingredient, Stock
tenant_uuid = uuid.UUID(tenant_id)
# Raw ingredients for local production
ingredients_data = [
# Harinas
("Harina de Trigo 000", "INGREDIENT", "FLOUR", None, "KILOGRAMS", 25.0, 50.0, 200.0, 2.50, "Molinos del Valle"),
("Harina Integral", "INGREDIENT", "FLOUR", None, "KILOGRAMS", 15.0, 30.0, 100.0, 3.20, "Bio Natural"),
("Harina de Centeno", "INGREDIENT", "FLOUR", None, "KILOGRAMS", 10.0, 20.0, 50.0, 3.50, "Ecológica"),
# Levaduras
("Levadura Fresca", "INGREDIENT", "YEAST", None, "KILOGRAMS", 1.0, 2.5, 10.0, 8.50, "Levapan"),
("Levadura Seca Activa", "INGREDIENT", "YEAST", None, "KILOGRAMS", 0.5, 1.0, 5.0, 12.00, "Fleischmann"),
# Grasas
("Mantequilla", "INGREDIENT", "FATS", None, "KILOGRAMS", 3.0, 8.0, 25.0, 6.80, "La Serenísima"),
("Aceite de Oliva Virgen Extra", "INGREDIENT", "FATS", None, "LITERS", 2.0, 5.0, 20.0, 15.50, "Cocinero"),
# Lácteos y Huevos
("Huevos Frescos", "INGREDIENT", "EGGS", None, "UNITS", 36, 60, 180, 0.25, "Granja San José"),
("Leche Entera", "INGREDIENT", "DAIRY", None, "LITERS", 5.0, 12.0, 50.0, 1.80, "La Serenísima"),
("Nata para Montar", "INGREDIENT", "DAIRY", None, "LITERS", 2.0, 5.0, 20.0, 3.50, "Central Lechera"),
# Azúcares
("Azúcar Blanca", "INGREDIENT", "SUGAR", None, "KILOGRAMS", 8.0, 20.0, 100.0, 1.20, "Ledesma"),
("Azúcar Morena", "INGREDIENT", "SUGAR", None, "KILOGRAMS", 3.0, 8.0, 25.0, 2.80, "Orgánica"),
("Azúcar Glass", "INGREDIENT", "SUGAR", None, "KILOGRAMS", 2.0, 5.0, 20.0, 2.20, "Ledesma"),
# Sal y Especias
("Sal Fina", "INGREDIENT", "SALT", None, "KILOGRAMS", 2.0, 5.0, 20.0, 0.80, "Celusal"),
("Canela en Polvo", "INGREDIENT", "SPICES", None, "GRAMS", 50, 150, 500, 0.08, "Alicante"),
("Vainilla en Extracto", "INGREDIENT", "SPICES", None, "MILLILITERS", 100, 250, 1000, 0.15, "McCormick"),
# Chocolates y Aditivos
("Chocolate Negro 70%", "INGREDIENT", "ADDITIVES", None, "KILOGRAMS", 1.0, 3.0, 15.0, 8.50, "Valor"),
("Cacao en Polvo", "INGREDIENT", "ADDITIVES", None, "KILOGRAMS", 0.5, 2.0, 10.0, 6.50, "Nestlé"),
("Nueces Peladas", "INGREDIENT", "ADDITIVES", None, "KILOGRAMS", 0.5, 1.5, 8.0, 12.00, "Los Nogales"),
("Pasas de Uva", "INGREDIENT", "ADDITIVES", None, "KILOGRAMS", 1.0, 2.0, 10.0, 4.50, "Mendoza Premium"),
# Productos Terminados (producción local)
("Croissant Clásico", "FINISHED_PRODUCT", None, "CROISSANTS", "PIECES", 12, 30, 80, 1.20, None),
("Pan Integral", "FINISHED_PRODUCT", None, "BREAD", "PIECES", 8, 20, 50, 2.50, None),
("Napolitana de Chocolate", "FINISHED_PRODUCT", None, "PASTRIES", "PIECES", 10, 25, 60, 1.80, None),
("Pan de Masa Madre", "FINISHED_PRODUCT", None, "BREAD", "PIECES", 6, 15, 40, 3.50, None),
("Magdalena de Vainilla", "FINISHED_PRODUCT", None, "PASTRIES", "PIECES", 8, 20, 50, 1.00, None),
]
ingredient_map = {}
for name, product_type, ing_cat, prod_cat, uom, low_stock, reorder, reorder_qty, cost, brand in ingredients_data:
ing = Ingredient(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
name=name,
product_type=product_type,
ingredient_category=ing_cat,
product_category=prod_cat,
unit_of_measure=uom,
low_stock_threshold=low_stock,
reorder_point=reorder,
reorder_quantity=reorder_qty,
average_cost=cost,
brand=brand,
is_active=True,
is_perishable=(ing_cat in ["DAIRY", "EGGS"] if ing_cat else False),
shelf_life_days=7 if ing_cat in ["DAIRY", "EGGS"] else (365 if ing_cat else 2),
created_at=datetime.now(timezone.utc)
)
session.add(ing)
ingredient_map[name] = ing
await session.commit()
# Create stock lots
now = datetime.now(timezone.utc)
# Harina de Trigo - Good stock
harina_trigo = ingredient_map["Harina de Trigo 000"]
session.add(Stock(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
ingredient_id=harina_trigo.id,
production_stage="raw_ingredient",
current_quantity=120.0,
reserved_quantity=15.0,
available_quantity=105.0,
batch_number=f"HARINA-TRI-{now.strftime('%Y%m%d')}-001",
received_date=now - timedelta(days=5),
expiration_date=now + timedelta(days=360),
unit_cost=2.50,
total_cost=300.0,
storage_location="Almacén Principal - Estante A1",
is_available=True,
is_expired=False,
quality_status="good",
created_at=now
))
# Levadura Fresca - Low stock (critical)
levadura = ingredient_map["Levadura Fresca"]
session.add(Stock(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
ingredient_id=levadura.id,
production_stage="raw_ingredient",
current_quantity=0.8,
reserved_quantity=0.3,
available_quantity=0.5,
batch_number=f"LEVAD-FRE-{now.strftime('%Y%m%d')}-001",
received_date=now - timedelta(days=2),
expiration_date=now + timedelta(days=5),
unit_cost=8.50,
total_cost=6.8,
storage_location="Cámara Fría - Nivel 2",
is_available=True,
is_expired=False,
quality_status="good",
created_at=now
))
# Croissants - Fresh batch
croissant = ingredient_map["Croissant Clásico"]
session.add(Stock(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
ingredient_id=croissant.id,
production_stage="fully_baked",
current_quantity=35,
reserved_quantity=5,
available_quantity=30,
batch_number=f"CROIS-FRESH-{now.strftime('%Y%m%d')}-001",
received_date=now - timedelta(hours=4),
expiration_date=now + timedelta(hours=20),
unit_cost=1.20,
total_cost=42.0,
storage_location="Vitrina Principal - Nivel 1",
is_available=True,
is_expired=False,
quality_status="good",
created_at=now
))
await session.commit()
logger.info("Individual bakery inventory seeded")
async def seed_central_baker_inventory(session, tenant_id: str):
"""Seed inventory for central baker satellite (receives products)"""
try:
from app.models.inventory import Ingredient, Stock
except ImportError:
from services.inventory.app.models.inventory import Ingredient, Stock
tenant_uuid = uuid.UUID(tenant_id)
# Finished and par-baked products from central baker
ingredients_data = [
# Productos Pre-Horneados (del obrador central)
("Croissant Pre-Horneado", "FINISHED_PRODUCT", None, "CROISSANTS", "PIECES", 20, 50, 150, 0.85, "Obrador Central"),
("Pan Baguette Pre-Horneado", "FINISHED_PRODUCT", None, "BREAD", "PIECES", 15, 40, 120, 1.20, "Obrador Central"),
("Napolitana Pre-Horneada", "FINISHED_PRODUCT", None, "PASTRIES", "PIECES", 15, 35, 100, 1.50, "Obrador Central"),
("Pan de Molde Pre-Horneado", "FINISHED_PRODUCT", None, "BREAD", "PIECES", 10, 25, 80, 1.80, "Obrador Central"),
# Productos Terminados (listos para venta)
("Croissant de Mantequilla", "FINISHED_PRODUCT", None, "CROISSANTS", "PIECES", 15, 40, 100, 1.20, "Obrador Central"),
("Palmera de Hojaldre", "FINISHED_PRODUCT", None, "PASTRIES", "PIECES", 10, 30, 80, 2.20, "Obrador Central"),
("Magdalena Tradicional", "FINISHED_PRODUCT", None, "PASTRIES", "PIECES", 12, 30, 80, 1.00, "Obrador Central"),
("Empanada de Atún", "FINISHED_PRODUCT", None, "OTHER_PRODUCTS", "PIECES", 8, 20, 60, 3.50, "Obrador Central"),
("Pan Integral de Molde", "FINISHED_PRODUCT", None, "BREAD", "PIECES", 10, 25, 75, 2.80, "Obrador Central"),
# Algunos ingredientes básicos
("Café en Grano", "INGREDIENT", "OTHER", None, "KILOGRAMS", 2.0, 5.0, 20.0, 18.50, "Lavazza"),
("Leche para Cafetería", "INGREDIENT", "DAIRY", None, "LITERS", 10.0, 20.0, 80.0, 1.50, "Central Lechera"),
("Azúcar para Cafetería", "INGREDIENT", "SUGAR", None, "KILOGRAMS", 3.0, 8.0, 30.0, 1.00, "Azucarera"),
]
ingredient_map = {}
for name, product_type, ing_cat, prod_cat, uom, low_stock, reorder, reorder_qty, cost, brand in ingredients_data:
ing = Ingredient(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
name=name,
product_type=product_type,
ingredient_category=ing_cat,
product_category=prod_cat,
unit_of_measure=uom,
low_stock_threshold=low_stock,
reorder_point=reorder,
reorder_quantity=reorder_qty,
average_cost=cost,
brand=brand,
is_active=True,
is_perishable=True,
shelf_life_days=3,
created_at=datetime.now(timezone.utc)
)
session.add(ing)
ingredient_map[name] = ing
await session.commit()
# Create stock lots
now = datetime.now(timezone.utc)
# Croissants pre-horneados
croissant_pre = ingredient_map["Croissant Pre-Horneado"]
session.add(Stock(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
ingredient_id=croissant_pre.id,
production_stage="par_baked",
current_quantity=75,
reserved_quantity=15,
available_quantity=60,
batch_number=f"CROIS-PAR-{now.strftime('%Y%m%d')}-001",
received_date=now - timedelta(days=1),
expiration_date=now + timedelta(days=4),
unit_cost=0.85,
total_cost=63.75,
storage_location="Congelador - Sección A",
is_available=True,
is_expired=False,
quality_status="good",
created_at=now
))
# Palmeras terminadas
palmera = ingredient_map["Palmera de Hojaldre"]
session.add(Stock(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
ingredient_id=palmera.id,
production_stage="fully_baked",
current_quantity=28,
reserved_quantity=4,
available_quantity=24,
batch_number=f"PALM-{now.strftime('%Y%m%d')}-001",
received_date=now - timedelta(hours=3),
expiration_date=now + timedelta(hours=45),
unit_cost=2.20,
total_cost=61.6,
storage_location="Vitrina Pasteles - Nivel 2",
is_available=True,
is_expired=False,
quality_status="good",
created_at=now
))
await session.commit()
logger.info("Central baker satellite inventory seeded")
async def seed_demo_inventory():
"""Main seeding function"""
database_url = os.getenv("INVENTORY_DATABASE_URL")
if not database_url:
logger.error("INVENTORY_DATABASE_URL not set")
return False
engine = create_async_engine(database_url, echo=False)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
try:
async with session_factory() as session:
# Seed both demo tenants
await seed_inventory_for_tenant(session, DEMO_TENANT_SAN_PABLO, "individual_bakery")
await seed_inventory_for_tenant(session, DEMO_TENANT_LA_ESPIGA, "central_baker_satellite")
logger.info("Demo inventory data seeded successfully")
return True
except Exception as e:
logger.error(f"Failed to seed inventory: {str(e)}")
import traceback
traceback.print_exc()
return False
finally:
await engine.dispose()
if __name__ == "__main__":
result = asyncio.run(seed_demo_inventory())
sys.exit(0 if result else 1)

View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""
Seed Demo Tenants
Creates base demo tenant templates with Spanish data
"""
import asyncio
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
import os
os.environ.setdefault("TENANT_DATABASE_URL", os.getenv("TENANT_DATABASE_URL"))
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import select
import structlog
import uuid
from datetime import datetime, timezone
logger = structlog.get_logger()
# Demo tenant configurations
DEMO_TENANTS = [
{
"id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
"name": "Panadería San Pablo - Demo",
"subdomain": "demo-sanpablo",
"business_type": "bakery",
"business_model": "individual_bakery",
"owner_id": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6", # María García
"address": "Calle Mayor, 15",
"city": "Madrid",
"postal_code": "28013",
"latitude": 40.4168,
"longitude": -3.7038,
"phone": "+34 912 345 678",
"email": "contacto@panaderiasanpablo.com",
"subscription_tier": "professional",
"is_active": True,
"is_demo": True,
"is_demo_template": True,
"ml_model_trained": True,
},
{
"id": "b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7",
"name": "Panadería La Espiga - Demo",
"subdomain": "demo-laespiga",
"business_type": "bakery",
"business_model": "central_baker_satellite",
"owner_id": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7", # Carlos Martínez
"address": "Avenida de la Constitución, 42",
"city": "Barcelona",
"postal_code": "08001",
"latitude": 41.3851,
"longitude": 2.1734,
"phone": "+34 913 456 789",
"email": "contacto@panaderialaespiga.com",
"subscription_tier": "enterprise",
"is_active": True,
"is_demo": True,
"is_demo_template": True,
"ml_model_trained": True,
}
]
async def seed_demo_tenants():
"""Seed demo tenants into tenant database"""
database_url = os.getenv("TENANT_DATABASE_URL")
if not database_url:
logger.error("TENANT_DATABASE_URL environment variable not set")
return False
logger.info("Connecting to tenant database", url=database_url.split("@")[-1])
engine = create_async_engine(database_url, echo=False)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
try:
async with session_factory() as session:
try:
from app.models.tenants import Tenant
except ImportError:
from services.tenant.app.models.tenants import Tenant
for tenant_data in DEMO_TENANTS:
# Check if tenant already exists
result = await session.execute(
select(Tenant).where(Tenant.subdomain == tenant_data["subdomain"])
)
existing_tenant = result.scalar_one_or_none()
if existing_tenant:
logger.info(f"Demo tenant already exists: {tenant_data['subdomain']}")
continue
# Create new demo tenant
tenant = Tenant(
id=uuid.UUID(tenant_data["id"]),
name=tenant_data["name"],
subdomain=tenant_data["subdomain"],
business_type=tenant_data["business_type"],
business_model=tenant_data["business_model"],
owner_id=uuid.UUID(tenant_data["owner_id"]),
address=tenant_data["address"],
city=tenant_data["city"],
postal_code=tenant_data["postal_code"],
latitude=tenant_data.get("latitude"),
longitude=tenant_data.get("longitude"),
phone=tenant_data.get("phone"),
email=tenant_data.get("email"),
subscription_tier=tenant_data["subscription_tier"],
is_active=tenant_data["is_active"],
is_demo=tenant_data["is_demo"],
is_demo_template=tenant_data["is_demo_template"],
ml_model_trained=tenant_data.get("ml_model_trained", False),
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
session.add(tenant)
logger.info(f"Created demo tenant: {tenant_data['name']}")
await session.commit()
logger.info("Demo tenants seeded successfully")
return True
except Exception as e:
logger.error(f"Failed to seed demo tenants: {str(e)}")
import traceback
traceback.print_exc()
return False
finally:
await engine.dispose()
if __name__ == "__main__":
result = asyncio.run(seed_demo_tenants())
sys.exit(0 if result else 1)

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Seed Demo Users
Creates demo user accounts for production demo environment
"""
import asyncio
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
import os
os.environ.setdefault("AUTH_DATABASE_URL", os.getenv("AUTH_DATABASE_URL"))
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import select
import structlog
import uuid
logger = structlog.get_logger()
# Demo user configurations (public credentials for prospects)
DEMO_USERS = [
{
"id": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"email": "demo.individual@panaderiasanpablo.com",
"password_hash": "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYVPWzO8hGi", # DemoSanPablo2024!
"full_name": "María García López",
"phone": "+34 912 345 678",
"language": "es",
"timezone": "Europe/Madrid",
"role": "owner",
"is_active": True,
"is_verified": True,
"is_demo": True
},
{
"id": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7",
"email": "demo.central@panaderialaespiga.com",
"password_hash": "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYVPWzO8hGi", # DemoLaEspiga2024!
"full_name": "Carlos Martínez Ruiz",
"phone": "+34 913 456 789",
"language": "es",
"timezone": "Europe/Madrid",
"role": "owner",
"is_active": True,
"is_verified": True,
"is_demo": True
}
]
async def seed_demo_users():
"""Seed demo users into auth database"""
database_url = os.getenv("AUTH_DATABASE_URL")
if not database_url:
logger.error("AUTH_DATABASE_URL environment variable not set")
return False
logger.info("Connecting to auth database", url=database_url.split("@")[-1])
engine = create_async_engine(database_url, echo=False)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
try:
async with session_factory() as session:
# Import User model
try:
from app.models.users import User
except ImportError:
from services.auth.app.models.users import User
from datetime import datetime, timezone
for user_data in DEMO_USERS:
# Check if user already exists
result = await session.execute(
select(User).where(User.email == user_data["email"])
)
existing_user = result.scalar_one_or_none()
if existing_user:
logger.info(f"Demo user already exists: {user_data['email']}")
continue
# Create new demo user
user = User(
id=uuid.UUID(user_data["id"]),
email=user_data["email"],
hashed_password=user_data["password_hash"],
full_name=user_data["full_name"],
phone=user_data.get("phone"),
language=user_data.get("language", "es"),
timezone=user_data.get("timezone", "Europe/Madrid"),
role=user_data.get("role", "owner"),
is_active=user_data.get("is_active", True),
is_verified=user_data.get("is_verified", True),
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
session.add(user)
logger.info(f"Created demo user: {user_data['email']}")
await session.commit()
logger.info("Demo users seeded successfully")
return True
except Exception as e:
logger.error(f"Failed to seed demo users: {str(e)}")
return False
finally:
await engine.dispose()
if __name__ == "__main__":
result = asyncio.run(seed_demo_users())
sys.exit(0 if result else 1)

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""
Manual demo data seeding script
Run this to populate the base demo template tenant with inventory data
"""
import asyncio
import sys
import os
# Add the project root to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
async def seed_demo_data():
"""Seed demo data by running all seed scripts in order"""
from scripts.demo.seed_demo_users import main as seed_users
from scripts.demo.seed_demo_tenants import main as seed_tenants
from scripts.demo.seed_demo_inventory import main as seed_inventory
from scripts.demo.seed_demo_ai_models import main as seed_ai_models
print("🌱 Starting demo data seeding...")
try:
print("\n📝 Step 1: Seeding demo users...")
await seed_users()
print("✅ Demo users seeded successfully")
print("\n🏢 Step 2: Seeding demo tenants...")
await seed_tenants()
print("✅ Demo tenants seeded successfully")
print("\n📦 Step 3: Seeding demo inventory...")
await seed_inventory()
print("✅ Demo inventory seeded successfully")
print("\n🤖 Step 4: Seeding demo AI models...")
await seed_ai_models()
print("✅ Demo AI models seeded successfully")
print("\n🎉 All demo data seeded successfully!")
except Exception as e:
print(f"\n❌ Error during seeding: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
asyncio.run(seed_demo_data())