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

@@ -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

View 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)