New enterprise feature
This commit is contained in:
@@ -43,8 +43,8 @@ logger = structlog.get_logger()
|
||||
|
||||
# Demo tenant IDs (match those from other services)
|
||||
DEMO_TENANT_IDS = [
|
||||
uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"), # San Pablo
|
||||
uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # La Espiga
|
||||
uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"), # Professional
|
||||
uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7")
|
||||
]
|
||||
|
||||
# System user ID for AI actions
|
||||
|
||||
228
services/alert_processor/scripts/demo/seed_demo_alerts_retail.py
Normal file
228
services/alert_processor/scripts/demo/seed_demo_alerts_retail.py
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Demo Retail Alerts Seeding Script for Alert Processor Service
|
||||
Creates stockout and low-stock alerts for child retail outlets
|
||||
|
||||
Usage:
|
||||
python /app/scripts/demo/seed_demo_alerts_retail.py
|
||||
|
||||
Environment Variables Required:
|
||||
ALERTS_DATABASE_URL - PostgreSQL connection string
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import sys
|
||||
import os
|
||||
import random
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# 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
|
||||
import structlog
|
||||
|
||||
from shared.utils.demo_dates import BASE_REFERENCE_DATE
|
||||
from app.models import Alert, AlertStatus, PriorityLevel, AlertTypeClass
|
||||
|
||||
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")
|
||||
DEMO_TENANT_CHILD_2 = uuid.UUID("e5f6a7b8-c9d0-41e2-f3a4-b5c6d7e8f9a0")
|
||||
DEMO_TENANT_CHILD_3 = uuid.UUID("f6a7b8c9-d0e1-42f3-a4b5-c6d7e8f9a0b1")
|
||||
|
||||
# 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_TENANTS = [
|
||||
(DEMO_TENANT_CHILD_1, "Madrid Centro"),
|
||||
(DEMO_TENANT_CHILD_2, "Barcelona Gràcia"),
|
||||
(DEMO_TENANT_CHILD_3, "Valencia Ruzafa")
|
||||
]
|
||||
|
||||
ALERT_SCENARIOS = [
|
||||
{
|
||||
"alert_type": "low_stock",
|
||||
"title": "Stock bajo detectado",
|
||||
"message_template": "Stock bajo de {product} en {location}. Unidades restantes: {units}",
|
||||
"priority_score": 75,
|
||||
"priority_level": PriorityLevel.IMPORTANT,
|
||||
"type_class": AlertTypeClass.ACTION_NEEDED,
|
||||
"financial_impact": 150.0
|
||||
},
|
||||
{
|
||||
"alert_type": "stockout_risk",
|
||||
"title": "Riesgo de quiebre de stock",
|
||||
"message_template": "Riesgo de quiebre de stock para {product} en {location}. Reposición urgente necesaria",
|
||||
"priority_score": 85,
|
||||
"priority_level": PriorityLevel.IMPORTANT,
|
||||
"type_class": AlertTypeClass.ESCALATION,
|
||||
"financial_impact": 300.0
|
||||
},
|
||||
{
|
||||
"alert_type": "expiring_soon",
|
||||
"title": "Productos próximos a vencer",
|
||||
"message_template": "Productos {product} próximos a vencer en {location}. Validar calidad antes de venta",
|
||||
"priority_score": 65,
|
||||
"priority_level": PriorityLevel.STANDARD,
|
||||
"type_class": AlertTypeClass.TREND_WARNING,
|
||||
"financial_impact": 80.0
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def seed_alerts_for_retail_tenant(db: AsyncSession, tenant_id: uuid.UUID, tenant_name: str):
|
||||
"""Seed alerts for a retail tenant"""
|
||||
logger.info(f"Seeding alerts for: {tenant_name}", tenant_id=str(tenant_id))
|
||||
|
||||
created = 0
|
||||
# Create 2-3 alerts per retail outlet
|
||||
for i in range(random.randint(2, 3)):
|
||||
scenario = random.choice(ALERT_SCENARIOS)
|
||||
|
||||
# Pick a random product
|
||||
sku = random.choice(list(PRODUCT_IDS.keys()))
|
||||
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))
|
||||
|
||||
# Random status - most are active, some acknowledged
|
||||
status = AlertStatus.ACKNOWLEDGED if random.random() < 0.3 else AlertStatus.ACTIVE
|
||||
|
||||
# Generate message from template
|
||||
message = scenario["message_template"].format(
|
||||
product=sku,
|
||||
location=tenant_name,
|
||||
units=random.randint(5, 15)
|
||||
)
|
||||
|
||||
alert = Alert(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant_id,
|
||||
item_type="alert",
|
||||
event_domain="inventory",
|
||||
alert_type=scenario["alert_type"],
|
||||
service="inventory",
|
||||
title=scenario["title"],
|
||||
message=message,
|
||||
type_class=scenario["type_class"],
|
||||
status=status,
|
||||
priority_score=scenario["priority_score"],
|
||||
priority_level=scenario["priority_level"],
|
||||
orchestrator_context={
|
||||
"product_id": str(product_id),
|
||||
"product_sku": sku,
|
||||
"location": tenant_name,
|
||||
"created_by": "inventory_monitoring_system"
|
||||
},
|
||||
business_impact={
|
||||
"financial_impact": scenario["financial_impact"],
|
||||
"currency": "EUR",
|
||||
"units_affected": random.randint(10, 50),
|
||||
"impact_description": f"Impacto estimado: €{scenario['financial_impact']:.2f}"
|
||||
},
|
||||
urgency_context={
|
||||
"time_until_consequence": f"{random.randint(2, 12)} horas",
|
||||
"consequence": "Pérdida de ventas o desperdicio de producto",
|
||||
"detection_time": (BASE_REFERENCE_DATE - timedelta(hours=random.randint(1, 24))).isoformat()
|
||||
},
|
||||
user_agency={
|
||||
"user_can_fix": True,
|
||||
"requires_supplier": scenario["alert_type"] == "stockout_risk",
|
||||
"suggested_actions": [
|
||||
"Revisar stock físico",
|
||||
"Contactar con Obrador para reposición urgente" if scenario["alert_type"] == "stockout_risk" else "Ajustar pedido próximo"
|
||||
]
|
||||
},
|
||||
trend_context=None,
|
||||
smart_actions=[
|
||||
{
|
||||
"action_type": "restock",
|
||||
"description": "Contactar con Obrador para reposición" if scenario["alert_type"] == "stockout_risk" else "Incluir en próximo pedido",
|
||||
"priority": "high" if scenario["alert_type"] == "stockout_risk" else "medium"
|
||||
}
|
||||
],
|
||||
ai_reasoning_summary=f"Sistema detectó {scenario['alert_type']} para {sku} basado en niveles actuales de inventario",
|
||||
confidence_score=0.85,
|
||||
timing_decision="send_now",
|
||||
placement=["dashboard", "notification_panel"] if scenario["type_class"] == AlertTypeClass.ESCALATION else ["dashboard"],
|
||||
alert_metadata={
|
||||
"product_sku": sku,
|
||||
"detection_method": "automated_monitoring",
|
||||
"threshold_triggered": "min_stock_level"
|
||||
},
|
||||
created_at=BASE_REFERENCE_DATE - timedelta(hours=random.randint(1, 24)),
|
||||
updated_at=BASE_REFERENCE_DATE
|
||||
)
|
||||
|
||||
db.add(alert)
|
||||
created += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Created {created} alerts for {tenant_name}")
|
||||
return {"tenant_id": str(tenant_id), "alerts_created": created}
|
||||
|
||||
|
||||
async def seed_all(db: AsyncSession):
|
||||
"""Seed all retail alerts"""
|
||||
logger.info("=" * 80)
|
||||
logger.info("🚨 Starting Demo Retail Alerts Seeding")
|
||||
logger.info("=" * 80)
|
||||
|
||||
results = []
|
||||
for tenant_id, tenant_name in RETAIL_TENANTS:
|
||||
result = await seed_alerts_for_retail_tenant(db, tenant_id, f"{tenant_name} (Retail)")
|
||||
results.append(result)
|
||||
|
||||
total = sum(r["alerts_created"] for r in results)
|
||||
logger.info(f"✅ Total alerts created: {total}")
|
||||
return {"total_alerts": total, "results": results}
|
||||
|
||||
|
||||
async def main():
|
||||
database_url = os.getenv("ALERTS_DATABASE_URL") or os.getenv("ALERT_PROCESSOR_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 alerts 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