New enterprise feature
This commit is contained in:
@@ -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)
|
||||
|
||||
167
services/forecasting/scripts/demo/seed_demo_forecasts_retail.py
Normal file
167
services/forecasting/scripts/demo/seed_demo_forecasts_retail.py
Normal 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)
|
||||
Reference in New Issue
Block a user