949 lines
37 KiB
Python
949 lines
37 KiB
Python
"""
|
|
Context-Aware Alert Message Templates with i18n Support
|
|
|
|
This module generates parametrized alert messages that can be translated by the frontend.
|
|
Instead of hardcoded Spanish messages, we generate structured message keys and parameters.
|
|
|
|
Messages are generated AFTER enrichment, leveraging:
|
|
- Orchestrator context (AI actions already taken)
|
|
- Business impact (financial, customers affected)
|
|
- Urgency context (hours until consequence, actual dates)
|
|
- User agency (supplier contacts, external dependencies)
|
|
|
|
Frontend uses i18n to translate message keys with parameters.
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, Any, Optional, List
|
|
from shared.schemas.alert_types import (
|
|
EnrichedAlert, OrchestratorContext, BusinessImpact,
|
|
UrgencyContext, UserAgency, TrendContext, SmartAction
|
|
)
|
|
|
|
import structlog
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
def format_date_spanish(dt: datetime) -> str:
|
|
"""Format datetime in Spanish (for backwards compatibility)"""
|
|
days = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"]
|
|
months = ["enero", "febrero", "marzo", "abril", "mayo", "junio",
|
|
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]
|
|
|
|
day_name = days[dt.weekday()]
|
|
month_name = months[dt.month - 1]
|
|
|
|
return f"{day_name} {dt.day} de {month_name}"
|
|
|
|
|
|
def format_iso_date(dt: datetime) -> str:
|
|
"""Format datetime as ISO date for frontend i18n"""
|
|
return dt.strftime('%Y-%m-%d')
|
|
|
|
|
|
def get_production_date(metadata: Dict[str, Any], default_days: int = 1) -> datetime:
|
|
"""Get actual production date from metadata or estimate"""
|
|
if metadata.get('production_date'):
|
|
if isinstance(metadata['production_date'], str):
|
|
return datetime.fromisoformat(metadata['production_date'])
|
|
return metadata['production_date']
|
|
else:
|
|
return datetime.now() + timedelta(days=default_days)
|
|
|
|
|
|
class ContextualMessageGenerator:
|
|
"""Generates context-aware parametrized messages for i18n"""
|
|
|
|
@staticmethod
|
|
def generate_message_data(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""
|
|
Generate contextual message data with i18n support
|
|
|
|
Returns dict with:
|
|
- title_key: i18n key for title
|
|
- title_params: parameters for title translation
|
|
- message_key: i18n key for message
|
|
- message_params: parameters for message translation
|
|
- fallback_title: fallback if i18n not available
|
|
- fallback_message: fallback if i18n not available
|
|
"""
|
|
alert_type = enriched.alert_type
|
|
|
|
# Dispatch to specific generator based on alert type
|
|
generators = {
|
|
# Inventory alerts
|
|
'critical_stock_shortage': ContextualMessageGenerator._stock_shortage,
|
|
'low_stock_warning': ContextualMessageGenerator._low_stock,
|
|
'stock_depleted_by_order': ContextualMessageGenerator._stock_depleted,
|
|
'production_ingredient_shortage': ContextualMessageGenerator._ingredient_shortage,
|
|
'expired_products': ContextualMessageGenerator._expired_products,
|
|
|
|
# Production alerts
|
|
'production_delay': ContextualMessageGenerator._production_delay,
|
|
'equipment_failure': ContextualMessageGenerator._equipment_failure,
|
|
'maintenance_required': ContextualMessageGenerator._maintenance_required,
|
|
'low_equipment_efficiency': ContextualMessageGenerator._low_efficiency,
|
|
'order_overload': ContextualMessageGenerator._order_overload,
|
|
|
|
# Supplier alerts
|
|
'supplier_delay': ContextualMessageGenerator._supplier_delay,
|
|
|
|
# Procurement alerts
|
|
'po_approval_needed': ContextualMessageGenerator._po_approval_needed,
|
|
'production_batch_start': ContextualMessageGenerator._production_batch_start,
|
|
|
|
# Environmental alerts
|
|
'temperature_breach': ContextualMessageGenerator._temperature_breach,
|
|
|
|
# Forecasting alerts
|
|
'demand_surge_weekend': ContextualMessageGenerator._demand_surge,
|
|
'weather_impact_alert': ContextualMessageGenerator._weather_impact,
|
|
'holiday_preparation': ContextualMessageGenerator._holiday_prep,
|
|
'severe_weather_impact': ContextualMessageGenerator._severe_weather,
|
|
'unexpected_demand_spike': ContextualMessageGenerator._demand_spike,
|
|
'demand_pattern_optimization': ContextualMessageGenerator._demand_pattern,
|
|
|
|
# Recommendations
|
|
'inventory_optimization': ContextualMessageGenerator._inventory_optimization,
|
|
'production_efficiency': ContextualMessageGenerator._production_efficiency,
|
|
'sales_opportunity': ContextualMessageGenerator._sales_opportunity,
|
|
'seasonal_adjustment': ContextualMessageGenerator._seasonal_adjustment,
|
|
'cost_reduction': ContextualMessageGenerator._cost_reduction,
|
|
'waste_reduction': ContextualMessageGenerator._waste_reduction,
|
|
'quality_improvement': ContextualMessageGenerator._quality_improvement,
|
|
'customer_satisfaction': ContextualMessageGenerator._customer_satisfaction,
|
|
'energy_optimization': ContextualMessageGenerator._energy_optimization,
|
|
'staff_optimization': ContextualMessageGenerator._staff_optimization,
|
|
}
|
|
|
|
generator_func = generators.get(alert_type)
|
|
if generator_func:
|
|
return generator_func(enriched)
|
|
else:
|
|
# Fallback for unknown alert types
|
|
return {
|
|
'title_key': f'alerts.{alert_type}.title',
|
|
'title_params': {},
|
|
'message_key': f'alerts.{alert_type}.message',
|
|
'message_params': {'alert_type': alert_type},
|
|
'fallback_title': f"Alerta: {alert_type}",
|
|
'fallback_message': f"Se detectó una situación que requiere atención: {alert_type}"
|
|
}
|
|
|
|
# ===================================================================
|
|
# INVENTORY ALERTS
|
|
# ===================================================================
|
|
|
|
@staticmethod
|
|
def _stock_shortage(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Critical stock shortage with AI context"""
|
|
metadata = enriched.alert_metadata
|
|
orch = enriched.orchestrator_context
|
|
urgency = enriched.urgency_context
|
|
agency = enriched.user_agency
|
|
|
|
ingredient_name = metadata.get('ingredient_name', 'Ingrediente')
|
|
current_stock = round(metadata.get('current_stock', 0), 1)
|
|
required_stock = round(metadata.get('required_stock', metadata.get('tomorrow_needed', 0)), 1)
|
|
|
|
# Base parameters
|
|
params = {
|
|
'ingredient_name': ingredient_name,
|
|
'current_stock': current_stock,
|
|
'required_stock': required_stock
|
|
}
|
|
|
|
# Determine message variant based on context
|
|
if orch and orch.already_addressed and orch.action_type == "purchase_order":
|
|
# AI already created PO
|
|
params['po_id'] = orch.action_id
|
|
params['po_amount'] = metadata.get('po_amount', 0)
|
|
|
|
if orch.delivery_date:
|
|
params['delivery_date'] = format_iso_date(orch.delivery_date)
|
|
params['delivery_day_name'] = format_date_spanish(orch.delivery_date)
|
|
|
|
if orch.action_status == "pending_approval":
|
|
message_key = 'alerts.critical_stock_shortage.message_with_po_pending'
|
|
else:
|
|
message_key = 'alerts.critical_stock_shortage.message_with_po_created'
|
|
|
|
elif urgency and urgency.time_until_consequence_hours:
|
|
# Time-specific message
|
|
hours = urgency.time_until_consequence_hours
|
|
params['hours_until'] = round(hours, 1)
|
|
message_key = 'alerts.critical_stock_shortage.message_with_hours'
|
|
|
|
elif metadata.get('production_date'):
|
|
# Date-specific message
|
|
prod_date = get_production_date(metadata)
|
|
params['production_date'] = format_iso_date(prod_date)
|
|
params['production_day_name'] = format_date_spanish(prod_date)
|
|
message_key = 'alerts.critical_stock_shortage.message_with_date'
|
|
|
|
else:
|
|
# Generic message
|
|
message_key = 'alerts.critical_stock_shortage.message_generic'
|
|
|
|
# Add supplier contact if available
|
|
if agency and agency.requires_external_party and agency.external_party_contact:
|
|
params['supplier_name'] = agency.external_party_name
|
|
params['supplier_contact'] = agency.external_party_contact
|
|
|
|
return {
|
|
'title_key': 'alerts.critical_stock_shortage.title',
|
|
'title_params': {'ingredient_name': ingredient_name},
|
|
'message_key': message_key,
|
|
'message_params': params,
|
|
'fallback_title': f"🚨 Stock Crítico: {ingredient_name}",
|
|
'fallback_message': f"Solo {current_stock}kg de {ingredient_name} disponibles (necesitas {required_stock}kg)."
|
|
}
|
|
|
|
@staticmethod
|
|
def _low_stock(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Low stock warning"""
|
|
metadata = enriched.alert_metadata
|
|
orch = enriched.orchestrator_context
|
|
|
|
ingredient_name = metadata.get('ingredient_name', 'Ingrediente')
|
|
current_stock = round(metadata.get('current_stock', 0), 1)
|
|
minimum_stock = round(metadata.get('minimum_stock', 0), 1)
|
|
|
|
params = {
|
|
'ingredient_name': ingredient_name,
|
|
'current_stock': current_stock,
|
|
'minimum_stock': minimum_stock
|
|
}
|
|
|
|
if orch and orch.already_addressed and orch.action_type == "purchase_order":
|
|
params['po_id'] = orch.action_id
|
|
message_key = 'alerts.low_stock.message_with_po'
|
|
else:
|
|
message_key = 'alerts.low_stock.message_generic'
|
|
|
|
return {
|
|
'title_key': 'alerts.low_stock.title',
|
|
'title_params': {'ingredient_name': ingredient_name},
|
|
'message_key': message_key,
|
|
'message_params': params,
|
|
'fallback_title': f"⚠️ Stock Bajo: {ingredient_name}",
|
|
'fallback_message': f"Stock de {ingredient_name}: {current_stock}kg (mínimo: {minimum_stock}kg)."
|
|
}
|
|
|
|
@staticmethod
|
|
def _stock_depleted(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Stock depleted by order"""
|
|
metadata = enriched.alert_metadata
|
|
agency = enriched.user_agency
|
|
|
|
ingredient_name = metadata.get('ingredient_name', 'Ingrediente')
|
|
order_id = metadata.get('order_id', '???')
|
|
current_stock = round(metadata.get('current_stock', 0), 1)
|
|
minimum_stock = round(metadata.get('minimum_stock', 0), 1)
|
|
|
|
params = {
|
|
'ingredient_name': ingredient_name,
|
|
'order_id': order_id,
|
|
'current_stock': current_stock,
|
|
'minimum_stock': minimum_stock
|
|
}
|
|
|
|
if agency and agency.requires_external_party and agency.external_party_contact:
|
|
params['supplier_name'] = agency.external_party_name
|
|
params['supplier_contact'] = agency.external_party_contact
|
|
message_key = 'alerts.stock_depleted.message_with_supplier'
|
|
else:
|
|
message_key = 'alerts.stock_depleted.message_generic'
|
|
|
|
return {
|
|
'title_key': 'alerts.stock_depleted.title',
|
|
'title_params': {'ingredient_name': ingredient_name},
|
|
'message_key': message_key,
|
|
'message_params': params,
|
|
'fallback_title': f"⚠️ Stock Agotado por Pedido: {ingredient_name}",
|
|
'fallback_message': f"El pedido #{order_id} agotaría el stock de {ingredient_name}."
|
|
}
|
|
|
|
@staticmethod
|
|
def _ingredient_shortage(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Production ingredient shortage"""
|
|
metadata = enriched.alert_metadata
|
|
impact = enriched.business_impact
|
|
|
|
ingredient_name = metadata.get('ingredient_name', 'Ingrediente')
|
|
shortage_amount = round(metadata.get('shortage_amount', 0), 1)
|
|
affected_batches = metadata.get('affected_batches_count', 0)
|
|
|
|
params = {
|
|
'ingredient_name': ingredient_name,
|
|
'shortage_amount': shortage_amount,
|
|
'affected_batches': affected_batches
|
|
}
|
|
|
|
if impact and impact.affected_customers:
|
|
params['customer_count'] = len(impact.affected_customers)
|
|
params['customer_names'] = ', '.join(impact.affected_customers[:2])
|
|
if len(impact.affected_customers) > 2:
|
|
params['additional_customers'] = len(impact.affected_customers) - 2
|
|
message_key = 'alerts.ingredient_shortage.message_with_customers'
|
|
else:
|
|
message_key = 'alerts.ingredient_shortage.message_generic'
|
|
|
|
return {
|
|
'title_key': 'alerts.ingredient_shortage.title',
|
|
'title_params': {'ingredient_name': ingredient_name},
|
|
'message_key': message_key,
|
|
'message_params': params,
|
|
'fallback_title': f"🚨 Escasez en Producción: {ingredient_name}",
|
|
'fallback_message': f"Faltan {shortage_amount}kg de {ingredient_name}."
|
|
}
|
|
|
|
@staticmethod
|
|
def _expired_products(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Expired products alert"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
product_count = metadata.get('product_count', 0)
|
|
expired_items = metadata.get('expired_items', [])
|
|
|
|
params = {
|
|
'product_count': product_count
|
|
}
|
|
|
|
if len(expired_items) > 0:
|
|
params['product_names'] = ', '.join([item.get('name', 'Producto') for item in expired_items[:2]])
|
|
if len(expired_items) > 2:
|
|
params['additional_count'] = len(expired_items) - 2
|
|
message_key = 'alerts.expired_products.message_with_names'
|
|
else:
|
|
message_key = 'alerts.expired_products.message_generic'
|
|
|
|
return {
|
|
'title_key': 'alerts.expired_products.title',
|
|
'title_params': {},
|
|
'message_key': message_key,
|
|
'message_params': params,
|
|
'fallback_title': "📅 Productos Caducados",
|
|
'fallback_message': f"{product_count} producto(s) caducado(s). Retirar inmediatamente."
|
|
}
|
|
|
|
# ===================================================================
|
|
# PRODUCTION ALERTS
|
|
# ===================================================================
|
|
|
|
@staticmethod
|
|
def _production_delay(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Production delay with affected orders"""
|
|
metadata = enriched.alert_metadata
|
|
impact = enriched.business_impact
|
|
|
|
batch_name = metadata.get('batch_name', 'Lote')
|
|
delay_minutes = metadata.get('delay_minutes', 0)
|
|
|
|
params = {
|
|
'batch_name': batch_name,
|
|
'delay_minutes': delay_minutes
|
|
}
|
|
|
|
if impact and impact.affected_customers:
|
|
params['customer_names'] = ', '.join(impact.affected_customers[:2])
|
|
if len(impact.affected_customers) > 2:
|
|
params['additional_count'] = len(impact.affected_customers) - 2
|
|
message_key = 'alerts.production_delay.message_with_customers'
|
|
elif impact and impact.affected_orders:
|
|
params['affected_orders'] = impact.affected_orders
|
|
message_key = 'alerts.production_delay.message_with_orders'
|
|
else:
|
|
message_key = 'alerts.production_delay.message_generic'
|
|
|
|
return {
|
|
'title_key': 'alerts.production_delay.title',
|
|
'title_params': {},
|
|
'message_key': message_key,
|
|
'message_params': params,
|
|
'fallback_title': "⏰ Retraso en Producción",
|
|
'fallback_message': f"Lote {batch_name} con {delay_minutes} minutos de retraso."
|
|
}
|
|
|
|
@staticmethod
|
|
def _equipment_failure(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Equipment failure"""
|
|
metadata = enriched.alert_metadata
|
|
impact = enriched.business_impact
|
|
|
|
equipment_name = metadata.get('equipment_name', 'Equipo')
|
|
|
|
params = {
|
|
'equipment_name': equipment_name
|
|
}
|
|
|
|
if impact and impact.production_batches_at_risk:
|
|
params['batch_count'] = len(impact.production_batches_at_risk)
|
|
message_key = 'alerts.equipment_failure.message_with_batches'
|
|
else:
|
|
message_key = 'alerts.equipment_failure.message_generic'
|
|
|
|
return {
|
|
'title_key': 'alerts.equipment_failure.title',
|
|
'title_params': {'equipment_name': equipment_name},
|
|
'message_key': message_key,
|
|
'message_params': params,
|
|
'fallback_title': f"⚙️ Fallo de Equipo: {equipment_name}",
|
|
'fallback_message': f"{equipment_name} no está funcionando correctamente."
|
|
}
|
|
|
|
@staticmethod
|
|
def _maintenance_required(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Maintenance required"""
|
|
metadata = enriched.alert_metadata
|
|
urgency = enriched.urgency_context
|
|
|
|
equipment_name = metadata.get('equipment_name', 'Equipo')
|
|
|
|
params = {
|
|
'equipment_name': equipment_name
|
|
}
|
|
|
|
if urgency and urgency.time_until_consequence_hours:
|
|
params['hours_until'] = round(urgency.time_until_consequence_hours, 1)
|
|
message_key = 'alerts.maintenance_required.message_with_hours'
|
|
else:
|
|
params['days_until'] = metadata.get('days_until_maintenance', 0)
|
|
message_key = 'alerts.maintenance_required.message_with_days'
|
|
|
|
return {
|
|
'title_key': 'alerts.maintenance_required.title',
|
|
'title_params': {'equipment_name': equipment_name},
|
|
'message_key': message_key,
|
|
'message_params': params,
|
|
'fallback_title': f"🔧 Mantenimiento Requerido: {equipment_name}",
|
|
'fallback_message': f"Equipo {equipment_name} requiere mantenimiento."
|
|
}
|
|
|
|
@staticmethod
|
|
def _low_efficiency(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Low equipment efficiency"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
equipment_name = metadata.get('equipment_name', 'Equipo')
|
|
efficiency = round(metadata.get('efficiency_percent', 0), 1)
|
|
|
|
return {
|
|
'title_key': 'alerts.low_efficiency.title',
|
|
'title_params': {'equipment_name': equipment_name},
|
|
'message_key': 'alerts.low_efficiency.message',
|
|
'message_params': {
|
|
'equipment_name': equipment_name,
|
|
'efficiency_percent': efficiency
|
|
},
|
|
'fallback_title': f"📉 Baja Eficiencia: {equipment_name}",
|
|
'fallback_message': f"Eficiencia del {equipment_name} bajó a {efficiency}%."
|
|
}
|
|
|
|
@staticmethod
|
|
def _order_overload(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Order capacity overload"""
|
|
metadata = enriched.alert_metadata
|
|
impact = enriched.business_impact
|
|
|
|
percentage = round(metadata.get('percentage', 0), 1)
|
|
|
|
params = {
|
|
'percentage': percentage
|
|
}
|
|
|
|
if impact and impact.affected_orders:
|
|
params['affected_orders'] = impact.affected_orders
|
|
message_key = 'alerts.order_overload.message_with_orders'
|
|
else:
|
|
message_key = 'alerts.order_overload.message_generic'
|
|
|
|
return {
|
|
'title_key': 'alerts.order_overload.title',
|
|
'title_params': {},
|
|
'message_key': message_key,
|
|
'message_params': params,
|
|
'fallback_title': "📋 Sobrecarga de Pedidos",
|
|
'fallback_message': f"Capacidad excedida en {percentage}%."
|
|
}
|
|
|
|
# ===================================================================
|
|
# SUPPLIER ALERTS
|
|
# ===================================================================
|
|
|
|
@staticmethod
|
|
def _supplier_delay(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Supplier delivery delay"""
|
|
metadata = enriched.alert_metadata
|
|
impact = enriched.business_impact
|
|
agency = enriched.user_agency
|
|
|
|
supplier_name = metadata.get('supplier_name', 'Proveedor')
|
|
hours = round(metadata.get('hours', metadata.get('delay_hours', 0)), 0)
|
|
products = metadata.get('products', metadata.get('affected_products', ''))
|
|
|
|
params = {
|
|
'supplier_name': supplier_name,
|
|
'hours': hours,
|
|
'products': products
|
|
}
|
|
|
|
if impact and impact.production_batches_at_risk:
|
|
params['batch_count'] = len(impact.production_batches_at_risk)
|
|
|
|
if agency and agency.external_party_contact:
|
|
params['supplier_contact'] = agency.external_party_contact
|
|
|
|
message_key = 'alerts.supplier_delay.message'
|
|
|
|
return {
|
|
'title_key': 'alerts.supplier_delay.title',
|
|
'title_params': {'supplier_name': supplier_name},
|
|
'message_key': message_key,
|
|
'message_params': params,
|
|
'fallback_title': f"🚚 Retraso de Proveedor: {supplier_name}",
|
|
'fallback_message': f"Entrega de {supplier_name} retrasada {hours} hora(s)."
|
|
}
|
|
|
|
# ===================================================================
|
|
# PROCUREMENT ALERTS
|
|
# ===================================================================
|
|
|
|
@staticmethod
|
|
def _po_approval_needed(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Purchase order approval needed"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
po_number = metadata.get('po_number', 'PO-XXXX')
|
|
supplier_name = metadata.get('supplier_name', 'Proveedor')
|
|
total_amount = metadata.get('total_amount', 0)
|
|
currency = metadata.get('currency', '€')
|
|
required_delivery_date = metadata.get('required_delivery_date')
|
|
|
|
# Format required delivery date for i18n
|
|
required_delivery_date_iso = None
|
|
if required_delivery_date:
|
|
if isinstance(required_delivery_date, str):
|
|
try:
|
|
dt = datetime.fromisoformat(required_delivery_date.replace('Z', '+00:00'))
|
|
required_delivery_date_iso = format_iso_date(dt)
|
|
except:
|
|
required_delivery_date_iso = required_delivery_date
|
|
elif isinstance(required_delivery_date, datetime):
|
|
required_delivery_date_iso = format_iso_date(required_delivery_date)
|
|
|
|
params = {
|
|
'po_number': po_number,
|
|
'supplier_name': supplier_name,
|
|
'total_amount': round(total_amount, 2),
|
|
'currency': currency,
|
|
'required_delivery_date': required_delivery_date_iso or 'fecha no especificada'
|
|
}
|
|
|
|
return {
|
|
'title_key': 'alerts.po_approval_needed.title',
|
|
'title_params': {'po_number': po_number},
|
|
'message_key': 'alerts.po_approval_needed.message',
|
|
'message_params': params
|
|
}
|
|
|
|
@staticmethod
|
|
def _production_batch_start(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Production batch ready to start"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
batch_number = metadata.get('batch_number', 'BATCH-XXXX')
|
|
product_name = metadata.get('product_name', 'Producto')
|
|
quantity_planned = metadata.get('quantity_planned', 0)
|
|
unit = metadata.get('unit', 'kg')
|
|
priority = metadata.get('priority', 'normal')
|
|
|
|
params = {
|
|
'batch_number': batch_number,
|
|
'product_name': product_name,
|
|
'quantity_planned': round(quantity_planned, 1),
|
|
'unit': unit,
|
|
'priority': priority
|
|
}
|
|
|
|
return {
|
|
'title_key': 'alerts.production_batch_start.title',
|
|
'title_params': {'product_name': product_name},
|
|
'message_key': 'alerts.production_batch_start.message',
|
|
'message_params': params
|
|
}
|
|
|
|
# ===================================================================
|
|
# ENVIRONMENTAL ALERTS
|
|
# ===================================================================
|
|
|
|
@staticmethod
|
|
def _temperature_breach(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Temperature breach alert"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
location = metadata.get('location', 'Ubicación')
|
|
temperature = round(metadata.get('temperature', 0), 1)
|
|
duration = metadata.get('duration', 0)
|
|
|
|
return {
|
|
'title_key': 'alerts.temperature_breach.title',
|
|
'title_params': {'location': location},
|
|
'message_key': 'alerts.temperature_breach.message',
|
|
'message_params': {
|
|
'location': location,
|
|
'temperature': temperature,
|
|
'duration': duration
|
|
}
|
|
}
|
|
|
|
# ===================================================================
|
|
# FORECASTING ALERTS
|
|
# ===================================================================
|
|
|
|
@staticmethod
|
|
def _demand_surge(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Weekend demand surge"""
|
|
metadata = enriched.alert_metadata
|
|
urgency = enriched.urgency_context
|
|
|
|
product_name = metadata.get('product_name', 'Producto')
|
|
percentage = round(metadata.get('percentage', metadata.get('growth_percentage', 0)), 0)
|
|
predicted_demand = metadata.get('predicted_demand', 0)
|
|
current_stock = metadata.get('current_stock', 0)
|
|
|
|
params = {
|
|
'product_name': product_name,
|
|
'percentage': percentage
|
|
}
|
|
|
|
if predicted_demand and current_stock:
|
|
params['predicted_demand'] = round(predicted_demand, 0)
|
|
params['current_stock'] = round(current_stock, 0)
|
|
|
|
if urgency and urgency.time_until_consequence_hours:
|
|
params['hours_until'] = round(urgency.time_until_consequence_hours, 1)
|
|
|
|
return {
|
|
'title_key': 'alerts.demand_surge.title',
|
|
'title_params': {'product_name': product_name},
|
|
'message_key': 'alerts.demand_surge.message',
|
|
'message_params': params
|
|
}
|
|
|
|
@staticmethod
|
|
def _weather_impact(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Weather impact on demand"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
weather_type = metadata.get('weather_type', 'Lluvia')
|
|
impact_percentage = round(metadata.get('impact_percentage', -20), 0)
|
|
|
|
return {
|
|
'title_key': 'alerts.weather_impact.title',
|
|
'title_params': {},
|
|
'message_key': 'alerts.weather_impact.message',
|
|
'message_params': {
|
|
'weather_type': weather_type,
|
|
'impact_percentage': abs(impact_percentage),
|
|
'is_negative': impact_percentage < 0
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _holiday_prep(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Holiday preparation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
holiday_name = metadata.get('holiday_name', 'Festividad')
|
|
days = metadata.get('days', 0)
|
|
percentage = round(metadata.get('percentage', metadata.get('increase_percentage', 0)), 0)
|
|
|
|
return {
|
|
'title_key': 'alerts.holiday_prep.title',
|
|
'title_params': {'holiday_name': holiday_name},
|
|
'message_key': 'alerts.holiday_prep.message',
|
|
'message_params': {
|
|
'holiday_name': holiday_name,
|
|
'days': days,
|
|
'percentage': percentage
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _severe_weather(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Severe weather impact"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
weather_type = metadata.get('weather_type', 'Tormenta')
|
|
duration_hours = metadata.get('duration_hours', 0)
|
|
|
|
return {
|
|
'title_key': 'alerts.severe_weather.title',
|
|
'title_params': {'weather_type': weather_type},
|
|
'message_key': 'alerts.severe_weather.message',
|
|
'message_params': {
|
|
'weather_type': weather_type,
|
|
'duration_hours': duration_hours
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _demand_spike(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Unexpected demand spike"""
|
|
metadata = enriched.alert_metadata
|
|
trend = enriched.trend_context
|
|
|
|
product_name = metadata.get('product_name', 'Producto')
|
|
spike_percentage = round(metadata.get('spike_percentage', metadata.get('growth_percentage', 0)), 0)
|
|
|
|
params = {
|
|
'product_name': product_name,
|
|
'spike_percentage': spike_percentage
|
|
}
|
|
|
|
if trend:
|
|
params['current_value'] = round(trend.current_value, 0)
|
|
params['baseline_value'] = round(trend.baseline_value, 0)
|
|
|
|
return {
|
|
'title_key': 'alerts.demand_spike.title',
|
|
'title_params': {'product_name': product_name},
|
|
'message_key': 'alerts.demand_spike.message',
|
|
'message_params': params
|
|
}
|
|
|
|
@staticmethod
|
|
def _demand_pattern(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Demand pattern optimization"""
|
|
metadata = enriched.alert_metadata
|
|
trend = enriched.trend_context
|
|
|
|
product_name = metadata.get('product_name', 'Producto')
|
|
variation = round(metadata.get('variation_percent', 0), 0)
|
|
|
|
params = {
|
|
'product_name': product_name,
|
|
'variation_percent': variation
|
|
}
|
|
|
|
if trend and trend.possible_causes:
|
|
params['possible_causes'] = ', '.join(trend.possible_causes[:2])
|
|
|
|
return {
|
|
'title_key': 'alerts.demand_pattern.title',
|
|
'title_params': {'product_name': product_name},
|
|
'message_key': 'alerts.demand_pattern.message',
|
|
'message_params': params
|
|
}
|
|
|
|
# ===================================================================
|
|
# RECOMMENDATIONS
|
|
# ===================================================================
|
|
|
|
@staticmethod
|
|
def _inventory_optimization(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Inventory optimization recommendation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
ingredient_name = metadata.get('ingredient_name', 'Ingrediente')
|
|
period = metadata.get('period', 7)
|
|
suggested_increase = round(metadata.get('suggested_increase', 0), 1)
|
|
|
|
return {
|
|
'title_key': 'recommendations.inventory_optimization.title',
|
|
'title_params': {'ingredient_name': ingredient_name},
|
|
'message_key': 'recommendations.inventory_optimization.message',
|
|
'message_params': {
|
|
'ingredient_name': ingredient_name,
|
|
'period': period,
|
|
'suggested_increase': suggested_increase
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _production_efficiency(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Production efficiency recommendation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
suggested_time = metadata.get('suggested_time', '')
|
|
savings_percent = round(metadata.get('savings_percent', 0), 1)
|
|
|
|
return {
|
|
'title_key': 'recommendations.production_efficiency.title',
|
|
'title_params': {},
|
|
'message_key': 'recommendations.production_efficiency.message',
|
|
'message_params': {
|
|
'suggested_time': suggested_time,
|
|
'savings_percent': savings_percent
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _sales_opportunity(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Sales opportunity recommendation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
product_name = metadata.get('product_name', 'Producto')
|
|
days = metadata.get('days', '')
|
|
increase_percent = round(metadata.get('increase_percent', 0), 0)
|
|
|
|
return {
|
|
'title_key': 'recommendations.sales_opportunity.title',
|
|
'title_params': {'product_name': product_name},
|
|
'message_key': 'recommendations.sales_opportunity.message',
|
|
'message_params': {
|
|
'product_name': product_name,
|
|
'days': days,
|
|
'increase_percent': increase_percent
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _seasonal_adjustment(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Seasonal adjustment recommendation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
season = metadata.get('season', 'temporada')
|
|
products = metadata.get('products', 'productos estacionales')
|
|
|
|
return {
|
|
'title_key': 'recommendations.seasonal_adjustment.title',
|
|
'title_params': {},
|
|
'message_key': 'recommendations.seasonal_adjustment.message',
|
|
'message_params': {
|
|
'season': season,
|
|
'products': products
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _cost_reduction(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Cost reduction recommendation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
supplier_name = metadata.get('supplier_name', 'Proveedor')
|
|
ingredient = metadata.get('ingredient', 'ingrediente')
|
|
savings_euros = round(metadata.get('savings_euros', 0), 0)
|
|
|
|
return {
|
|
'title_key': 'recommendations.cost_reduction.title',
|
|
'title_params': {},
|
|
'message_key': 'recommendations.cost_reduction.message',
|
|
'message_params': {
|
|
'supplier_name': supplier_name,
|
|
'ingredient': ingredient,
|
|
'savings_euros': savings_euros
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _waste_reduction(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Waste reduction recommendation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
product = metadata.get('product', 'producto')
|
|
waste_reduction_percent = round(metadata.get('waste_reduction_percent', 0), 0)
|
|
|
|
return {
|
|
'title_key': 'recommendations.waste_reduction.title',
|
|
'title_params': {},
|
|
'message_key': 'recommendations.waste_reduction.message',
|
|
'message_params': {
|
|
'product': product,
|
|
'waste_reduction_percent': waste_reduction_percent
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _quality_improvement(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Quality improvement recommendation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
product = metadata.get('product', 'producto')
|
|
|
|
return {
|
|
'title_key': 'recommendations.quality_improvement.title',
|
|
'title_params': {},
|
|
'message_key': 'recommendations.quality_improvement.message',
|
|
'message_params': {
|
|
'product': product
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _customer_satisfaction(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Customer satisfaction recommendation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
product = metadata.get('product', 'producto')
|
|
days = metadata.get('days', '')
|
|
|
|
return {
|
|
'title_key': 'recommendations.customer_satisfaction.title',
|
|
'title_params': {},
|
|
'message_key': 'recommendations.customer_satisfaction.message',
|
|
'message_params': {
|
|
'product': product,
|
|
'days': days
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _energy_optimization(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Energy optimization recommendation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
start_time = metadata.get('start_time', '')
|
|
end_time = metadata.get('end_time', '')
|
|
savings_euros = round(metadata.get('savings_euros', 0), 0)
|
|
|
|
return {
|
|
'title_key': 'recommendations.energy_optimization.title',
|
|
'title_params': {},
|
|
'message_key': 'recommendations.energy_optimization.message',
|
|
'message_params': {
|
|
'start_time': start_time,
|
|
'end_time': end_time,
|
|
'savings_euros': savings_euros
|
|
}
|
|
}
|
|
|
|
@staticmethod
|
|
def _staff_optimization(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""Staff optimization recommendation"""
|
|
metadata = enriched.alert_metadata
|
|
|
|
days = metadata.get('days', '')
|
|
hours = metadata.get('hours', '')
|
|
|
|
return {
|
|
'title_key': 'recommendations.staff_optimization.title',
|
|
'title_params': {},
|
|
'message_key': 'recommendations.staff_optimization.message',
|
|
'message_params': {
|
|
'days': days,
|
|
'hours': hours
|
|
}
|
|
}
|
|
|
|
|
|
def generate_contextual_message(enriched: EnrichedAlert) -> Dict[str, Any]:
|
|
"""
|
|
Main entry point for contextual message generation with i18n support
|
|
|
|
Args:
|
|
enriched: Fully enriched alert with all context
|
|
|
|
Returns:
|
|
Dict with:
|
|
- title_key: i18n translation key for title
|
|
- title_params: parameters for title translation
|
|
- message_key: i18n translation key for message
|
|
- message_params: parameters for message translation
|
|
- fallback_title: fallback if i18n not available
|
|
- fallback_message: fallback if i18n not available
|
|
"""
|
|
return ContextualMessageGenerator.generate_message_data(enriched)
|