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

@@ -34,8 +34,7 @@ from shared.utils.demo_dates import BASE_REFERENCE_DATE
# Configure logging
logger = structlog.get_logger()
DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # Central bakery
DEMO_TENANT_PROFESSIONAL = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery
# Day of week mapping
DAYS_OF_WEEK = {
@@ -413,24 +412,15 @@ async def seed_all(db: AsyncSession):
results = []
# Seed San Pablo (Individual Bakery)
result_san_pablo = await generate_forecasts_for_tenant(
# Seed Professional Bakery (merged from San Pablo + La Espiga)
result_professional = await generate_forecasts_for_tenant(
db,
DEMO_TENANT_SAN_PABLO,
"San Pablo - Individual Bakery",
DEMO_TENANT_PROFESSIONAL,
"Professional Bakery",
"individual_bakery",
config
)
results.append(result_san_pablo)
# Seed La Espiga (Central Bakery)
result_la_espiga = await generate_forecasts_for_tenant(
db,
DEMO_TENANT_LA_ESPIGA,
"La Espiga - Central Bakery",
"central_bakery",
config
)
results.append(result_la_espiga)
results.append(result_professional)
total_forecasts = sum(r["forecasts_created"] for r in results)
total_batches = sum(r["batches_created"] for r in results)

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Demo Retail Forecasting Seeding Script for Forecasting Service
Creates store-level demand forecasts for child retail outlets
This script populates child retail tenants with AI-generated demand forecasts.
Usage:
python /app/scripts/demo/seed_demo_forecasts_retail.py
Environment Variables Required:
FORECASTING_DATABASE_URL - PostgreSQL connection string
DEMO_MODE - Set to 'production' for production seeding
"""
import asyncio
import uuid
import sys
import os
import random
from datetime import datetime, timezone, timedelta
from pathlib import Path
from decimal import Decimal
# Add app to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
# Add shared to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent))
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import select
import structlog
from shared.utils.demo_dates import BASE_REFERENCE_DATE
from app.models import Forecast, PredictionBatch
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.dev.ConsoleRenderer()
]
)
logger = structlog.get_logger()
# Fixed Demo Tenant IDs
DEMO_TENANT_CHILD_1 = uuid.UUID("d4e5f6a7-b8c9-40d1-e2f3-a4b5c6d7e8f9") # Madrid Centro
DEMO_TENANT_CHILD_2 = uuid.UUID("e5f6a7b8-c9d0-41e2-f3a4-b5c6d7e8f9a0") # Barcelona Gràcia
DEMO_TENANT_CHILD_3 = uuid.UUID("f6a7b8c9-d0e1-42f3-a4b5-c6d7e8f9a0b1") # Valencia Ruzafa
# Product IDs
PRODUCT_IDS = {
"PRO-BAG-001": "20000000-0000-0000-0000-000000000001",
"PRO-CRO-001": "20000000-0000-0000-0000-000000000002",
"PRO-PUE-001": "20000000-0000-0000-0000-000000000003",
"PRO-NAP-001": "20000000-0000-0000-0000-000000000004",
}
# Retail forecasting patterns
RETAIL_FORECASTS = [
(DEMO_TENANT_CHILD_1, "Madrid Centro", {"PRO-BAG-001": 120, "PRO-CRO-001": 80, "PRO-PUE-001": 35, "PRO-NAP-001": 60}),
(DEMO_TENANT_CHILD_2, "Barcelona Gràcia", {"PRO-BAG-001": 90, "PRO-CRO-001": 60, "PRO-PUE-001": 25, "PRO-NAP-001": 45}),
(DEMO_TENANT_CHILD_3, "Valencia Ruzafa", {"PRO-BAG-001": 70, "PRO-CRO-001": 45, "PRO-PUE-001": 20, "PRO-NAP-001": 35})
]
async def seed_forecasts_for_retail_tenant(db: AsyncSession, tenant_id: uuid.UUID, tenant_name: str, base_forecasts: dict):
"""Seed forecasts for a retail tenant"""
logger.info(f"Seeding forecasts for: {tenant_name}", tenant_id=str(tenant_id))
created = 0
# Create 7 days of forecasts
for days_ahead in range(1, 8):
forecast_date = BASE_REFERENCE_DATE + timedelta(days=days_ahead)
for sku, base_qty in base_forecasts.items():
base_product_id = uuid.UUID(PRODUCT_IDS[sku])
tenant_int = int(tenant_id.hex, 16)
product_id = uuid.UUID(int=tenant_int ^ int(base_product_id.hex, 16))
# Weekend boost
is_weekend = forecast_date.weekday() in [5, 6]
day_of_week = forecast_date.weekday()
multiplier = random.uniform(1.3, 1.5) if is_weekend else random.uniform(0.9, 1.1)
forecasted_quantity = int(base_qty * multiplier)
forecast = Forecast(
id=uuid.uuid4(),
tenant_id=tenant_id,
inventory_product_id=product_id,
product_name=sku,
location=tenant_name,
forecast_date=forecast_date,
created_at=BASE_REFERENCE_DATE,
predicted_demand=float(forecasted_quantity),
confidence_lower=float(int(forecasted_quantity * 0.85)),
confidence_upper=float(int(forecasted_quantity * 1.15)),
confidence_level=0.90,
model_id="retail_forecast_model",
model_version="retail_v1.0",
algorithm="prophet_retail",
business_type="retail_outlet",
day_of_week=day_of_week,
is_holiday=False,
is_weekend=is_weekend,
weather_temperature=random.uniform(10.0, 25.0),
weather_precipitation=random.uniform(0.0, 5.0) if random.random() < 0.3 else 0.0,
weather_description="Clear" if random.random() > 0.3 else "Rainy",
traffic_volume=random.randint(50, 200) if is_weekend else random.randint(30, 120),
processing_time_ms=random.randint(50, 200),
features_used={"historical_sales": True, "weather": True, "day_of_week": True}
)
db.add(forecast)
created += 1
await db.commit()
logger.info(f"Created {created} forecasts for {tenant_name}")
return {"tenant_id": str(tenant_id), "forecasts_created": created}
async def seed_all(db: AsyncSession):
"""Seed all retail forecasts"""
logger.info("=" * 80)
logger.info("📈 Starting Demo Retail Forecasting Seeding")
logger.info("=" * 80)
results = []
for tenant_id, tenant_name, base_forecasts in RETAIL_FORECASTS:
result = await seed_forecasts_for_retail_tenant(db, tenant_id, f"{tenant_name} (Retail)", base_forecasts)
results.append(result)
total = sum(r["forecasts_created"] for r in results)
logger.info(f"✅ Total forecasts created: {total}")
return {"total_forecasts": total, "results": results}
async def main():
database_url = os.getenv("FORECASTING_DATABASE_URL") or os.getenv("DATABASE_URL")
if not database_url:
logger.error("❌ DATABASE_URL not set")
return 1
if database_url.startswith("postgresql://"):
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
engine = create_async_engine(database_url, echo=False, pool_pre_ping=True)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
try:
async with async_session() as session:
await seed_all(session)
logger.info("🎉 Retail forecasting seed completed!")
return 0
except Exception as e:
logger.error(f"❌ Seed failed: {e}", exc_info=True)
return 1
finally:
await engine.dispose()
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)