Add DEMO feature to the project
This commit is contained in:
1
scripts/demo/__init__.py
Normal file
1
scripts/demo/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Demo Data Seeding Scripts"""
|
||||
234
scripts/demo/clone_demo_tenant.py
Normal file
234
scripts/demo/clone_demo_tenant.py
Normal 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)
|
||||
278
scripts/demo/seed_demo_ai_models.py
Normal file
278
scripts/demo/seed_demo_ai_models.py
Normal 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())
|
||||
338
scripts/demo/seed_demo_inventory.py
Normal file
338
scripts/demo/seed_demo_inventory.py
Normal 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)
|
||||
144
scripts/demo/seed_demo_tenants.py
Normal file
144
scripts/demo/seed_demo_tenants.py
Normal 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)
|
||||
121
scripts/demo/seed_demo_users.py
Normal file
121
scripts/demo/seed_demo_users.py
Normal 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)
|
||||
49
scripts/manual_seed_demo.py
Normal file
49
scripts/manual_seed_demo.py
Normal 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())
|
||||
Reference in New Issue
Block a user