#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Demo Alert Seeding Script for Alert Processor Service ONLY seeds prevented-issue alerts (AI interventions with financial impact). Action-needed alerts are system-generated and should not be seeded. All alerts reference real seed data: - Real ingredient IDs from inventory seed - Real supplier IDs from supplier seed - Real product names from recipes seed - Historical data over past 7 days for trend analysis """ import asyncio import uuid import sys import os import random from datetime import datetime, timezone, timedelta from pathlib import Path from decimal import Decimal from typing import List, Dict, Any # Add app to path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from sqlalchemy import select, delete import structlog from app.models.events import Alert, AlertStatus, PriorityLevel, AlertTypeClass from app.config import AlertProcessorConfig # Add shared utilities to path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) from shared.utils.demo_dates import BASE_REFERENCE_DATE, adjust_date_for_demo # Configure logging 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 ] # System user ID for AI actions SYSTEM_USER_ID = uuid.UUID("50000000-0000-0000-0000-000000000004") # ============================================================================ # REAL SEED DATA IDs (from demo seed scripts) # ============================================================================ # Real ingredient IDs from inventory seed HARINA_T55_ID = "10000000-0000-0000-0000-000000000001" MANTEQUILLA_ID = "10000000-0000-0000-0000-000000000007" HUEVOS_ID = "10000000-0000-0000-0000-000000000008" AZUCAR_BLANCO_ID = "10000000-0000-0000-0000-000000000005" NUECES_ID = "10000000-0000-0000-0000-000000000018" PASAS_ID = "10000000-0000-0000-0000-000000000019" # Real supplier IDs from supplier seed MOLINOS_SAN_JOSE_ID = "40000000-0000-0000-0000-000000000001" LACTEOS_DEL_VALLE_ID = "40000000-0000-0000-0000-000000000002" # Real product names from recipes seed PAN_DE_PUEBLO = "Pan de Pueblo" BAGUETTE_FRANCESA = "Baguette Francesa Tradicional" PAN_RUSTICO_CEREALES = "Pan Rústico de Cereales" def create_prevented_issue_alerts(tenant_id: uuid.UUID, reference_time: datetime) -> List[Alert]: """Create prevented issue alerts showing AI interventions with financial impact""" alerts = [] # Historical prevented issues over past 7 days prevented_scenarios = [ { "days_ago": 1, "title": "Problema Evitado: Exceso de Stock de Harina", "message": "Detecté que la orden automática iba a crear sobrestockexceso. Reduje la cantidad de 150kg a 100kg.", "alert_type": "prevented_overstock", "service": "inventory", "priority_score": 55, "priority_level": "standard", "financial_impact": 87.50, "ai_reasoning": "El análisis de demanda mostró una disminución del 15% en productos con harina. Ajusté la orden para evitar desperdicio.", "confidence": 0.88, "metadata": { "ingredient_id": HARINA_T55_ID, "ingredient": "Harina de Trigo T55", "original_quantity_kg": 150, "adjusted_quantity_kg": 100, "savings_eur": 87.50, "waste_prevented_kg": 50, "supplier_id": MOLINOS_SAN_JOSE_ID, "supplier": "Molinos San José S.L." } }, { "days_ago": 2, "title": "Problema Evitado: Conflicto de Equipamiento", "message": "Evité un conflicto en el horno principal reprogramando el lote de baguettes 30 minutos antes.", "alert_type": "prevented_equipment_conflict", "service": "production", "priority_score": 70, "priority_level": "important", "financial_impact": 0, "ai_reasoning": "Dos lotes estaban programados para el mismo horno. Reprogramé automáticamente para optimizar el uso.", "confidence": 0.94, "metadata": { "equipment": "Horno Principal", "product_name": BAGUETTE_FRANCESA, "batch_rescheduled": BAGUETTE_FRANCESA, "time_adjustment_minutes": 30, "downtime_prevented_minutes": 45 } }, { "days_ago": 3, "title": "Problema Evitado: Compra Duplicada", "message": "Detecté dos órdenes de compra casi idénticas para mantequilla. Cancelé la duplicada automáticamente.", "alert_type": "prevented_duplicate_po", "service": "procurement", "priority_score": 62, "priority_level": "standard", "financial_impact": 245.80, "ai_reasoning": "Dos servicios crearon órdenes similares con 10 minutos de diferencia. Cancelé la segunda para evitar sobrepedido.", "confidence": 0.96, "metadata": { "ingredient_id": MANTEQUILLA_ID, "ingredient": "Mantequilla sin Sal 82% MG", "duplicate_po_amount": 245.80, "time_difference_minutes": 10, "supplier_id": LACTEOS_DEL_VALLE_ID, "supplier": "Lácteos del Valle S.A." } }, { "days_ago": 4, "title": "Problema Evitado: Caducidad Inminente", "message": "Prioricé automáticamente el uso de huevos que caducan en 2 días en lugar de stock nuevo.", "alert_type": "prevented_expiration_waste", "service": "inventory", "priority_score": 58, "priority_level": "standard", "financial_impact": 34.50, "ai_reasoning": "Detecté stock próximo a caducar. Ajusté el plan de producción para usar primero los ingredientes más antiguos.", "confidence": 0.90, "metadata": { "ingredient_id": HUEVOS_ID, "ingredient": "Huevos Frescos Categoría A", "quantity_prioritized": 120, "days_until_expiration": 2, "waste_prevented_eur": 34.50 } }, { "days_ago": 5, "title": "Problema Evitado: Sobrepago a Proveedor", "message": "Detecté una discrepancia de precio en la orden de azúcar blanco. Precio cotizado: €2.20/kg, precio esperado: €1.85/kg.", "alert_type": "prevented_price_discrepancy", "service": "procurement", "priority_score": 68, "priority_level": "standard", "financial_impact": 17.50, "ai_reasoning": "El precio era 18.9% mayor que el histórico. Rechacé la orden automáticamente y notifiqué al proveedor.", "confidence": 0.85, "metadata": { "ingredient_id": AZUCAR_BLANCO_ID, "ingredient": "Azúcar Blanco Refinado", "quoted_price_per_kg": 2.20, "expected_price_per_kg": 1.85, "quantity_kg": 50, "savings_eur": 17.50, "supplier": "Varios Distribuidores" } }, { "days_ago": 6, "title": "Problema Evitado: Pedido Sin Ingredientes", "message": f"Un pedido de cliente incluía {PAN_RUSTICO_CEREALES}, pero no había suficiente stock. Sugerí sustitución con {PAN_DE_PUEBLO}.", "alert_type": "prevented_unfulfillable_order", "service": "orders", "priority_score": 75, "priority_level": "important", "financial_impact": 0, "ai_reasoning": "Detecté que el pedido no podía cumplirse con el stock actual. Ofrecí automáticamente una alternativa antes de confirmar.", "confidence": 0.92, "metadata": { "original_product": PAN_RUSTICO_CEREALES, "missing_ingredients": ["Semillas de girasol", "Semillas de sésamo"], "suggested_alternative": PAN_DE_PUEBLO, "customer_satisfaction_preserved": True } }, ] for scenario in prevented_scenarios: created_at = reference_time - timedelta(days=scenario["days_ago"]) resolved_at = created_at + timedelta(seconds=1) # Instantly resolved by AI alert = Alert( id=uuid.uuid4(), tenant_id=tenant_id, item_type="alert", alert_type=scenario["alert_type"], service=scenario["service"], title=scenario["title"], message=scenario["message"], status=AlertStatus.RESOLVED, # Already resolved by AI priority_score=scenario["priority_score"], priority_level=scenario["priority_level"], type_class="prevented_issue", # KEY: This classifies as prevented orchestrator_context={ "created_by": "ai_intervention_system", "auto_resolved": True, "resolution_method": "automatic" }, business_impact={ "financial_impact": scenario["financial_impact"], "currency": "EUR", "orders_affected": scenario["metadata"].get("orders_affected", 0), "impact_description": f"Ahorro estimado: €{scenario['financial_impact']:.2f}" if scenario["financial_impact"] > 0 else "Operación mejorada" }, urgency_context={ "time_until_consequence": "0 segundos", "consequence": "Problema resuelto automáticamente", "resolution_time_ms": random.randint(100, 500) }, user_agency={ "user_can_fix": False, # AI already fixed it "requires_supplier": False, "requires_external_party": False, "estimated_resolution_time": "Automático" }, trend_context=None, smart_actions=[], # No actions needed - already resolved ai_reasoning_summary=scenario["ai_reasoning"], confidence_score=scenario["confidence"], timing_decision="send_now", scheduled_send_time=None, placement=["dashboard"], # Only dashboard - not urgent since already resolved action_created_at=None, superseded_by_action_id=None, hidden_from_ui=False, alert_metadata=scenario["metadata"], created_at=created_at, updated_at=resolved_at, resolved_at=resolved_at ) alerts.append(alert) return alerts async def seed_demo_alerts(): """Main function to seed demo alerts""" logger.info("Starting demo alert seeding") # Initialize database config = AlertProcessorConfig() engine = create_async_engine(config.DATABASE_URL, echo=False) async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async with async_session() as session: try: # Delete existing alerts for demo tenants for tenant_id in DEMO_TENANT_IDS: logger.info("Deleting existing alerts", tenant_id=str(tenant_id)) await session.execute( delete(Alert).where(Alert.tenant_id == tenant_id) ) await session.commit() logger.info("Existing alerts deleted") # Create alerts for each tenant reference_time = datetime.now(timezone.utc) total_alerts_created = 0 for tenant_id in DEMO_TENANT_IDS: logger.info("Creating prevented-issue alerts for tenant", tenant_id=str(tenant_id)) # Create prevented-issue alerts (historical AI interventions) # NOTE: Action-needed alerts are NOT seeded - they are system-generated prevented_alerts = create_prevented_issue_alerts(tenant_id, reference_time) for alert in prevented_alerts: session.add(alert) logger.info(f"Created {len(prevented_alerts)} prevented-issue alerts") total_alerts_created += len(prevented_alerts) # Commit all alerts await session.commit() logger.info( "Demo alert seeding completed", total_alerts=total_alerts_created, tenants=len(DEMO_TENANT_IDS) ) print(f"\n✅ Successfully seeded {total_alerts_created} demo alerts") print(f" - Prevented-issue alerts (AI interventions): {len(prevented_alerts) * len(DEMO_TENANT_IDS)}") print(f" - Action-needed alerts: 0 (system-generated, not seeded)") print(f" - Tenants: {len(DEMO_TENANT_IDS)}") print(f"\n📝 Note: All alerts reference real seed data (ingredients, suppliers, products)") except Exception as e: logger.error("Error seeding demo alerts", error=str(e), exc_info=True) await session.rollback() raise finally: await engine.dispose() if __name__ == "__main__": asyncio.run(seed_demo_alerts())