2025-10-17 07:31:14 +02:00
|
|
|
"""
|
|
|
|
|
Alert Generation Utilities for Demo Sessions
|
|
|
|
|
Provides functions to create realistic alerts during data cloning
|
|
|
|
|
|
|
|
|
|
All alert messages are in Spanish for demo purposes.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from typing import List, Optional, Dict, Any
|
|
|
|
|
import uuid
|
|
|
|
|
from decimal import Decimal
|
2025-10-19 19:22:37 +02:00
|
|
|
import structlog
|
|
|
|
|
|
|
|
|
|
logger = structlog.get_logger()
|
2025-10-17 07:31:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class AlertSeverity:
|
|
|
|
|
"""Alert severity levels"""
|
|
|
|
|
LOW = "low"
|
|
|
|
|
MEDIUM = "medium"
|
|
|
|
|
HIGH = "high"
|
|
|
|
|
URGENT = "urgent"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AlertStatus:
|
|
|
|
|
"""Alert status values"""
|
|
|
|
|
ACTIVE = "active"
|
|
|
|
|
RESOLVED = "resolved"
|
|
|
|
|
ACKNOWLEDGED = "acknowledged"
|
|
|
|
|
IGNORED = "ignored"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def create_demo_alert(
|
|
|
|
|
db,
|
|
|
|
|
tenant_id: uuid.UUID,
|
|
|
|
|
alert_type: str,
|
|
|
|
|
severity: str,
|
|
|
|
|
title: str,
|
|
|
|
|
message: str,
|
|
|
|
|
service: str,
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata: Dict[str, Any] = None,
|
|
|
|
|
created_at: Optional[datetime] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
2025-10-19 19:22:37 +02:00
|
|
|
Create and persist a demo alert, then publish to RabbitMQ
|
2025-10-17 07:31:14 +02:00
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
db: Database session
|
|
|
|
|
tenant_id: Tenant UUID
|
|
|
|
|
alert_type: Type of alert (e.g., 'expiration_imminent')
|
|
|
|
|
severity: Alert severity level (low, medium, high, urgent)
|
|
|
|
|
title: Alert title (in Spanish)
|
|
|
|
|
message: Alert message (in Spanish)
|
|
|
|
|
service: Service name that generated the alert
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client: RabbitMQ client for publishing alerts
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata: Additional alert-specific data
|
|
|
|
|
created_at: When the alert was created (defaults to now)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Created Alert instance (dict for cross-service compatibility)
|
|
|
|
|
"""
|
2025-10-19 19:22:37 +02:00
|
|
|
from shared.config.rabbitmq_config import get_routing_key
|
|
|
|
|
|
|
|
|
|
alert_id = uuid.uuid4()
|
|
|
|
|
alert_created_at = created_at or datetime.now(timezone.utc)
|
|
|
|
|
|
2025-10-17 07:31:14 +02:00
|
|
|
# Import here to avoid circular dependencies
|
|
|
|
|
try:
|
|
|
|
|
from app.models.alerts import Alert
|
|
|
|
|
|
|
|
|
|
alert = Alert(
|
2025-10-19 19:22:37 +02:00
|
|
|
id=alert_id,
|
2025-10-17 07:31:14 +02:00
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
item_type="alert",
|
|
|
|
|
alert_type=alert_type,
|
|
|
|
|
severity=severity,
|
|
|
|
|
status=AlertStatus.ACTIVE,
|
|
|
|
|
service=service,
|
|
|
|
|
title=title,
|
|
|
|
|
message=message,
|
|
|
|
|
alert_metadata=metadata or {},
|
2025-10-19 19:22:37 +02:00
|
|
|
created_at=alert_created_at
|
2025-10-17 07:31:14 +02:00
|
|
|
)
|
|
|
|
|
db.add(alert)
|
2025-10-19 19:22:37 +02:00
|
|
|
await db.flush()
|
2025-10-17 07:31:14 +02:00
|
|
|
except ImportError:
|
2025-10-19 19:22:37 +02:00
|
|
|
# If Alert model not available, skip DB insert
|
|
|
|
|
logger.warning("Alert model not available, skipping DB insert", service=service)
|
|
|
|
|
|
|
|
|
|
# Publish alert to RabbitMQ for processing by Alert Processor
|
|
|
|
|
if rabbitmq_client:
|
|
|
|
|
try:
|
|
|
|
|
alert_message = {
|
|
|
|
|
'id': str(alert_id),
|
|
|
|
|
'tenant_id': str(tenant_id),
|
|
|
|
|
'item_type': 'alert',
|
|
|
|
|
'type': alert_type,
|
|
|
|
|
'severity': severity,
|
|
|
|
|
'service': service,
|
|
|
|
|
'title': title,
|
|
|
|
|
'message': message,
|
|
|
|
|
'metadata': metadata or {},
|
|
|
|
|
'timestamp': alert_created_at.isoformat()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
routing_key = get_routing_key('alert', severity, service)
|
|
|
|
|
|
|
|
|
|
published = await rabbitmq_client.publish_event(
|
|
|
|
|
exchange_name='alerts.exchange',
|
|
|
|
|
routing_key=routing_key,
|
|
|
|
|
event_data=alert_message
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if published:
|
|
|
|
|
logger.info(
|
|
|
|
|
"Demo alert published to RabbitMQ",
|
|
|
|
|
alert_id=str(alert_id),
|
|
|
|
|
alert_type=alert_type,
|
|
|
|
|
severity=severity,
|
|
|
|
|
service=service,
|
|
|
|
|
routing_key=routing_key
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
logger.warning(
|
|
|
|
|
"Failed to publish demo alert to RabbitMQ",
|
|
|
|
|
alert_id=str(alert_id),
|
|
|
|
|
alert_type=alert_type
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(
|
|
|
|
|
"Error publishing demo alert to RabbitMQ",
|
|
|
|
|
alert_id=str(alert_id),
|
|
|
|
|
error=str(e),
|
|
|
|
|
exc_info=True
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
logger.warning("No RabbitMQ client provided, alert will not be streamed", alert_id=str(alert_id))
|
|
|
|
|
|
|
|
|
|
# Return alert dict for compatibility
|
|
|
|
|
return {
|
|
|
|
|
"id": str(alert_id),
|
|
|
|
|
"tenant_id": str(tenant_id),
|
|
|
|
|
"item_type": "alert",
|
|
|
|
|
"alert_type": alert_type,
|
|
|
|
|
"severity": severity,
|
|
|
|
|
"status": AlertStatus.ACTIVE,
|
|
|
|
|
"service": service,
|
|
|
|
|
"title": title,
|
|
|
|
|
"message": message,
|
|
|
|
|
"alert_metadata": metadata or {},
|
|
|
|
|
"created_at": alert_created_at
|
|
|
|
|
}
|
2025-10-17 07:31:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def generate_inventory_alerts(
|
|
|
|
|
db,
|
|
|
|
|
tenant_id: uuid.UUID,
|
2025-10-19 19:22:37 +02:00
|
|
|
session_created_at: datetime,
|
|
|
|
|
rabbitmq_client=None
|
2025-10-17 07:31:14 +02:00
|
|
|
) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Generate inventory-related alerts for demo session
|
|
|
|
|
|
|
|
|
|
Generates alerts for:
|
|
|
|
|
- Expired stock
|
|
|
|
|
- Expiring soon stock (<= 3 days)
|
|
|
|
|
- Low stock levels
|
|
|
|
|
- Overstock situations
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
db: Database session
|
|
|
|
|
tenant_id: Virtual tenant UUID
|
|
|
|
|
session_created_at: When the demo session was created
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client: RabbitMQ client for publishing alerts
|
2025-10-17 07:31:14 +02:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Number of alerts created
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from app.models.inventory import Stock, Ingredient
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
from shared.utils.demo_dates import get_days_until_expiration
|
|
|
|
|
except ImportError:
|
|
|
|
|
# Models not available in this context
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
alerts_created = 0
|
|
|
|
|
|
|
|
|
|
# Query stocks with joins to ingredients
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Stock, Ingredient).join(
|
|
|
|
|
Ingredient, Stock.ingredient_id == Ingredient.id
|
|
|
|
|
).where(
|
|
|
|
|
Stock.tenant_id == tenant_id
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
stock_ingredient_pairs = result.all()
|
|
|
|
|
|
|
|
|
|
for stock, ingredient in stock_ingredient_pairs:
|
|
|
|
|
# Expiration alerts
|
|
|
|
|
if stock.expiration_date:
|
|
|
|
|
days_until_expiry = get_days_until_expiration(
|
|
|
|
|
stock.expiration_date,
|
|
|
|
|
session_created_at
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if days_until_expiry < 0:
|
|
|
|
|
# Expired stock
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="expired_stock",
|
|
|
|
|
severity=AlertSeverity.URGENT,
|
|
|
|
|
title=f"Stock Caducado: {ingredient.name}",
|
|
|
|
|
message=f"El lote {stock.batch_number} caducó hace {abs(days_until_expiry)} días. "
|
|
|
|
|
f"Cantidad: {stock.current_quantity:.2f} {ingredient.unit_of_measure.value}. "
|
|
|
|
|
f"Acción requerida: Retirar inmediatamente del inventario y registrar como pérdida.",
|
|
|
|
|
service="inventory",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"stock_id": str(stock.id),
|
|
|
|
|
"ingredient_id": str(ingredient.id),
|
|
|
|
|
"batch_number": stock.batch_number,
|
|
|
|
|
"expiration_date": stock.expiration_date.isoformat(),
|
|
|
|
|
"days_expired": abs(days_until_expiry),
|
|
|
|
|
"quantity": float(stock.current_quantity),
|
|
|
|
|
"unit": ingredient.unit_of_measure.value,
|
|
|
|
|
"estimated_loss": float(stock.total_cost) if stock.total_cost else 0.0
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
elif days_until_expiry <= 3:
|
|
|
|
|
# Expiring soon
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="expiration_imminent",
|
|
|
|
|
severity=AlertSeverity.HIGH,
|
|
|
|
|
title=f"Próximo a Caducar: {ingredient.name}",
|
|
|
|
|
message=f"El lote {stock.batch_number} caduca en {days_until_expiry} día{'s' if days_until_expiry > 1 else ''}. "
|
|
|
|
|
f"Cantidad: {stock.current_quantity:.2f} {ingredient.unit_of_measure.value}. "
|
|
|
|
|
f"Recomendación: Planificar uso prioritario en producción inmediata.",
|
|
|
|
|
service="inventory",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"stock_id": str(stock.id),
|
|
|
|
|
"ingredient_id": str(ingredient.id),
|
|
|
|
|
"batch_number": stock.batch_number,
|
|
|
|
|
"expiration_date": stock.expiration_date.isoformat(),
|
|
|
|
|
"days_until_expiry": days_until_expiry,
|
|
|
|
|
"quantity": float(stock.current_quantity),
|
|
|
|
|
"unit": ingredient.unit_of_measure.value
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
# Low stock alert
|
|
|
|
|
if stock.current_quantity < ingredient.low_stock_threshold:
|
|
|
|
|
shortage = ingredient.low_stock_threshold - stock.current_quantity
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="low_stock",
|
|
|
|
|
severity=AlertSeverity.MEDIUM,
|
|
|
|
|
title=f"Stock Bajo: {ingredient.name}",
|
|
|
|
|
message=f"Stock actual: {stock.current_quantity:.2f} {ingredient.unit_of_measure.value}. "
|
|
|
|
|
f"Umbral mínimo: {ingredient.low_stock_threshold:.2f}. "
|
|
|
|
|
f"Faltante: {shortage:.2f} {ingredient.unit_of_measure.value}. "
|
|
|
|
|
f"Se recomienda realizar pedido de {ingredient.reorder_quantity:.2f} {ingredient.unit_of_measure.value}.",
|
|
|
|
|
service="inventory",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"stock_id": str(stock.id),
|
|
|
|
|
"ingredient_id": str(ingredient.id),
|
|
|
|
|
"current_quantity": float(stock.current_quantity),
|
|
|
|
|
"threshold": float(ingredient.low_stock_threshold),
|
|
|
|
|
"reorder_point": float(ingredient.reorder_point),
|
|
|
|
|
"reorder_quantity": float(ingredient.reorder_quantity),
|
|
|
|
|
"shortage": float(shortage)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
# Overstock alert (if max_stock_level is defined)
|
|
|
|
|
if ingredient.max_stock_level and stock.current_quantity > ingredient.max_stock_level:
|
|
|
|
|
excess = stock.current_quantity - ingredient.max_stock_level
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="overstock",
|
|
|
|
|
severity=AlertSeverity.LOW,
|
|
|
|
|
title=f"Exceso de Stock: {ingredient.name}",
|
|
|
|
|
message=f"Stock actual: {stock.current_quantity:.2f} {ingredient.unit_of_measure.value}. "
|
|
|
|
|
f"Nivel máximo recomendado: {ingredient.max_stock_level:.2f}. "
|
|
|
|
|
f"Exceso: {excess:.2f} {ingredient.unit_of_measure.value}. "
|
|
|
|
|
f"Considerar reducir cantidad en próximos pedidos o buscar uso alternativo.",
|
|
|
|
|
service="inventory",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"stock_id": str(stock.id),
|
|
|
|
|
"ingredient_id": str(ingredient.id),
|
|
|
|
|
"current_quantity": float(stock.current_quantity),
|
|
|
|
|
"max_level": float(ingredient.max_stock_level),
|
|
|
|
|
"excess": float(excess)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
return alerts_created
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def generate_equipment_alerts(
|
|
|
|
|
db,
|
|
|
|
|
tenant_id: uuid.UUID,
|
2025-10-19 19:22:37 +02:00
|
|
|
session_created_at: datetime,
|
|
|
|
|
rabbitmq_client=None
|
2025-10-17 07:31:14 +02:00
|
|
|
) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Generate equipment-related alerts for demo session
|
|
|
|
|
|
|
|
|
|
Generates alerts for:
|
|
|
|
|
- Equipment needing maintenance
|
|
|
|
|
- Equipment in maintenance/down status
|
|
|
|
|
- Equipment with low efficiency
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
db: Database session
|
|
|
|
|
tenant_id: Virtual tenant UUID
|
|
|
|
|
session_created_at: When the demo session was created
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client: RabbitMQ client for publishing alerts
|
2025-10-17 07:31:14 +02:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Number of alerts created
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from app.models.production import Equipment, EquipmentStatus
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
except ImportError:
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
alerts_created = 0
|
|
|
|
|
|
|
|
|
|
# Query equipment
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Equipment).where(Equipment.tenant_id == tenant_id)
|
|
|
|
|
)
|
|
|
|
|
equipment_list = result.scalars().all()
|
|
|
|
|
|
|
|
|
|
for equipment in equipment_list:
|
|
|
|
|
# Maintenance required alert
|
|
|
|
|
if equipment.next_maintenance_date and equipment.next_maintenance_date <= session_created_at:
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="equipment_maintenance_due",
|
|
|
|
|
severity=AlertSeverity.MEDIUM,
|
|
|
|
|
title=f"Mantenimiento Vencido: {equipment.name}",
|
|
|
|
|
message=f"El equipo {equipment.name} ({equipment.type.value}) tiene mantenimiento vencido. "
|
|
|
|
|
f"Último mantenimiento: {equipment.last_maintenance_date.strftime('%d/%m/%Y') if equipment.last_maintenance_date else 'No registrado'}. "
|
|
|
|
|
f"Programar mantenimiento preventivo lo antes posible.",
|
|
|
|
|
service="production",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"equipment_id": str(equipment.id),
|
|
|
|
|
"equipment_name": equipment.name,
|
|
|
|
|
"equipment_type": equipment.type.value,
|
|
|
|
|
"last_maintenance": equipment.last_maintenance_date.isoformat() if equipment.last_maintenance_date else None,
|
|
|
|
|
"next_maintenance": equipment.next_maintenance_date.isoformat()
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
# Equipment status alerts
|
|
|
|
|
if equipment.status == EquipmentStatus.MAINTENANCE:
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="equipment_in_maintenance",
|
|
|
|
|
severity=AlertSeverity.MEDIUM,
|
|
|
|
|
title=f"Equipo en Mantenimiento: {equipment.name}",
|
|
|
|
|
message=f"El equipo {equipment.name} está actualmente en mantenimiento y no disponible para producción. "
|
|
|
|
|
f"Ajustar planificación de producción según capacidad reducida.",
|
|
|
|
|
service="production",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"equipment_id": str(equipment.id),
|
|
|
|
|
"equipment_name": equipment.name,
|
|
|
|
|
"equipment_type": equipment.type.value
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
elif equipment.status == EquipmentStatus.DOWN:
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="equipment_down",
|
|
|
|
|
severity=AlertSeverity.URGENT,
|
|
|
|
|
title=f"Equipo Fuera de Servicio: {equipment.name}",
|
|
|
|
|
message=f"URGENTE: El equipo {equipment.name} está fuera de servicio. "
|
|
|
|
|
f"Contactar con servicio técnico inmediatamente. "
|
|
|
|
|
f"Revisar planificación de producción y reasignar lotes a otros equipos.",
|
|
|
|
|
service="production",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"equipment_id": str(equipment.id),
|
|
|
|
|
"equipment_name": equipment.name,
|
|
|
|
|
"equipment_type": equipment.type.value
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
elif equipment.status == EquipmentStatus.WARNING:
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="equipment_warning",
|
|
|
|
|
severity=AlertSeverity.MEDIUM,
|
|
|
|
|
title=f"Advertencia de Equipo: {equipment.name}",
|
|
|
|
|
message=f"El equipo {equipment.name} presenta signos de advertencia. "
|
|
|
|
|
f"Eficiencia actual: {equipment.efficiency_percentage:.1f}%. "
|
|
|
|
|
f"Monitorear de cerca y considerar inspección preventiva.",
|
|
|
|
|
service="production",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"equipment_id": str(equipment.id),
|
|
|
|
|
"equipment_name": equipment.name,
|
|
|
|
|
"equipment_type": equipment.type.value,
|
|
|
|
|
"efficiency": float(equipment.efficiency_percentage) if equipment.efficiency_percentage else None
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
# Low efficiency alert
|
|
|
|
|
if equipment.efficiency_percentage and equipment.efficiency_percentage < 80.0:
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="equipment_low_efficiency",
|
|
|
|
|
severity=AlertSeverity.LOW,
|
|
|
|
|
title=f"Eficiencia Baja: {equipment.name}",
|
|
|
|
|
message=f"El equipo {equipment.name} está operando con eficiencia reducida ({equipment.efficiency_percentage:.1f}%). "
|
|
|
|
|
f"Eficiencia objetivo: e 85%. "
|
|
|
|
|
f"Revisar causas: limpieza, calibración, desgaste de componentes.",
|
|
|
|
|
service="production",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"equipment_id": str(equipment.id),
|
|
|
|
|
"equipment_name": equipment.name,
|
|
|
|
|
"efficiency": float(equipment.efficiency_percentage)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
return alerts_created
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def generate_order_alerts(
|
|
|
|
|
db,
|
|
|
|
|
tenant_id: uuid.UUID,
|
2025-10-19 19:22:37 +02:00
|
|
|
session_created_at: datetime,
|
|
|
|
|
rabbitmq_client=None
|
2025-10-17 07:31:14 +02:00
|
|
|
) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Generate order-related alerts for demo session
|
|
|
|
|
|
|
|
|
|
Generates alerts for:
|
|
|
|
|
- Orders with approaching delivery dates
|
|
|
|
|
- Delayed orders
|
|
|
|
|
- High-priority pending orders
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
db: Database session
|
|
|
|
|
tenant_id: Virtual tenant UUID
|
|
|
|
|
session_created_at: When the demo session was created
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client: RabbitMQ client for publishing alerts
|
2025-10-17 07:31:14 +02:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Number of alerts created
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from app.models.order import CustomerOrder
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
from shared.utils.demo_dates import get_days_until_expiration
|
|
|
|
|
except ImportError:
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
alerts_created = 0
|
|
|
|
|
|
|
|
|
|
# Query orders
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(CustomerOrder).where(
|
|
|
|
|
CustomerOrder.tenant_id == tenant_id,
|
|
|
|
|
CustomerOrder.status.in_(['pending', 'confirmed', 'in_production'])
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
orders = result.scalars().all()
|
|
|
|
|
|
|
|
|
|
for order in orders:
|
|
|
|
|
if order.requested_delivery_date:
|
|
|
|
|
days_until_delivery = (order.requested_delivery_date - session_created_at).days
|
|
|
|
|
|
|
|
|
|
# Approaching delivery date
|
|
|
|
|
if 0 <= days_until_delivery <= 2 and order.status in ['pending', 'confirmed']:
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="order_delivery_soon",
|
|
|
|
|
severity=AlertSeverity.HIGH,
|
|
|
|
|
title=f"Entrega Próxima: Pedido {order.order_number}",
|
|
|
|
|
message=f"El pedido {order.order_number} debe entregarse en {days_until_delivery} día{'s' if days_until_delivery > 1 else ''}. "
|
|
|
|
|
f"Cliente: {order.customer.name if hasattr(order, 'customer') else 'N/A'}. "
|
|
|
|
|
f"Estado actual: {order.status}. "
|
|
|
|
|
f"Verificar que esté en producción.",
|
|
|
|
|
service="orders",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"order_id": str(order.id),
|
|
|
|
|
"order_number": order.order_number,
|
|
|
|
|
"status": order.status,
|
|
|
|
|
"delivery_date": order.requested_delivery_date.isoformat(),
|
|
|
|
|
"days_until_delivery": days_until_delivery
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
# Delayed order
|
|
|
|
|
if days_until_delivery < 0:
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="order_delayed",
|
|
|
|
|
severity=AlertSeverity.URGENT,
|
|
|
|
|
title=f"Pedido Retrasado: {order.order_number}",
|
|
|
|
|
message=f"URGENTE: El pedido {order.order_number} está retrasado {abs(days_until_delivery)} días. "
|
|
|
|
|
f"Fecha de entrega prevista: {order.requested_delivery_date.strftime('%d/%m/%Y')}. "
|
|
|
|
|
f"Contactar al cliente y renegociar fecha de entrega.",
|
|
|
|
|
service="orders",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"order_id": str(order.id),
|
|
|
|
|
"order_number": order.order_number,
|
|
|
|
|
"status": order.status,
|
|
|
|
|
"delivery_date": order.requested_delivery_date.isoformat(),
|
|
|
|
|
"days_delayed": abs(days_until_delivery)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
# High priority pending orders
|
|
|
|
|
if order.priority == 'high' and order.status == 'pending':
|
|
|
|
|
await create_demo_alert(
|
|
|
|
|
db=db,
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
alert_type="high_priority_order_pending",
|
|
|
|
|
severity=AlertSeverity.MEDIUM,
|
|
|
|
|
title=f"Pedido Prioritario Pendiente: {order.order_number}",
|
|
|
|
|
message=f"El pedido de alta prioridad {order.order_number} está pendiente de confirmación. "
|
|
|
|
|
f"Monto: ¬{float(order.total_amount):.2f}. "
|
|
|
|
|
f"Revisar disponibilidad de ingredientes y confirmar producción.",
|
|
|
|
|
service="orders",
|
2025-10-19 19:22:37 +02:00
|
|
|
rabbitmq_client=rabbitmq_client,
|
2025-10-17 07:31:14 +02:00
|
|
|
metadata={
|
|
|
|
|
"order_id": str(order.id),
|
|
|
|
|
"order_number": order.order_number,
|
|
|
|
|
"priority": order.priority,
|
|
|
|
|
"total_amount": float(order.total_amount)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
alerts_created += 1
|
|
|
|
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
return alerts_created
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Utility function for cross-service alert creation
|
|
|
|
|
async def create_alert_via_api(
|
|
|
|
|
alert_processor_url: str,
|
|
|
|
|
tenant_id: uuid.UUID,
|
|
|
|
|
alert_data: Dict[str, Any],
|
|
|
|
|
internal_api_key: str
|
|
|
|
|
) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Create an alert via the alert processor service API
|
|
|
|
|
|
|
|
|
|
This function is useful when creating alerts from services that don't
|
|
|
|
|
have direct database access to the alert processor database.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
alert_processor_url: Base URL of alert processor service
|
|
|
|
|
tenant_id: Tenant UUID
|
|
|
|
|
alert_data: Alert data dictionary
|
|
|
|
|
internal_api_key: Internal API key for service-to-service auth
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if alert created successfully, False otherwise
|
|
|
|
|
"""
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
|
|
|
response = await client.post(
|
|
|
|
|
f"{alert_processor_url}/internal/alerts",
|
|
|
|
|
json={
|
|
|
|
|
"tenant_id": str(tenant_id),
|
|
|
|
|
**alert_data
|
|
|
|
|
},
|
|
|
|
|
headers={
|
|
|
|
|
"X-Internal-API-Key": internal_api_key
|
|
|
|
|
},
|
|
|
|
|
timeout=5.0
|
|
|
|
|
)
|
|
|
|
|
return response.status_code == 201
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|