New alert system and panel de control page
This commit is contained in:
@@ -277,9 +277,34 @@ class BaseAlertService:
|
||||
|
||||
# Publishing (Updated for type)
|
||||
async def publish_item(self, tenant_id: UUID, item: Dict[str, Any], item_type: str = 'alert'):
|
||||
"""Publish alert or recommendation to RabbitMQ with deduplication"""
|
||||
"""Publish alert or recommendation to RabbitMQ with deduplication and validation"""
|
||||
|
||||
try:
|
||||
# Validate alert structure before publishing
|
||||
from shared.schemas.alert_types import RawAlert
|
||||
try:
|
||||
raw_alert = RawAlert(
|
||||
tenant_id=str(tenant_id),
|
||||
alert_type=item.get('type'),
|
||||
title=item.get('title'),
|
||||
message=item.get('message'),
|
||||
service=self.config.SERVICE_NAME,
|
||||
actions=item.get('actions', []),
|
||||
alert_metadata=item.get('metadata', {}),
|
||||
item_type=item_type
|
||||
)
|
||||
# Validation passed, continue with validated data
|
||||
logger.debug("Alert schema validation passed",
|
||||
service=self.config.SERVICE_NAME,
|
||||
alert_type=item.get('type'))
|
||||
except Exception as validation_error:
|
||||
logger.error("Alert schema validation failed",
|
||||
service=self.config.SERVICE_NAME,
|
||||
alert_type=item.get('type'),
|
||||
error=str(validation_error))
|
||||
self._errors_count += 1
|
||||
return False
|
||||
|
||||
# Generate proper deduplication key based on alert type and specific identifiers
|
||||
unique_id = self._generate_unique_identifier(item)
|
||||
item_key = f"{tenant_id}:{item_type}:{item['type']}:{unique_id}"
|
||||
@@ -350,6 +375,7 @@ class BaseAlertService:
|
||||
metadata = item.get('metadata', {})
|
||||
|
||||
# Generate unique identifier based on alert type
|
||||
# Inventory alerts
|
||||
if alert_type == 'overstock_warning':
|
||||
return metadata.get('ingredient_id', '')
|
||||
elif alert_type == 'critical_stock_shortage' or alert_type == 'low_stock_warning':
|
||||
@@ -377,6 +403,47 @@ class BaseAlertService:
|
||||
return f"opt:{metadata.get('ingredient_id', '')}:{metadata.get('recommendation_type', '')}"
|
||||
elif alert_type == 'waste_reduction':
|
||||
return f"waste:{metadata.get('ingredient_id', '')}"
|
||||
|
||||
# Procurement alerts
|
||||
elif alert_type == 'procurement_pos_pending_approval':
|
||||
# Use hash of PO IDs for grouped alerts
|
||||
pos = metadata.get('pos', [])
|
||||
if pos:
|
||||
po_ids = sorted([str(po.get('po_id', '')) for po in pos])
|
||||
import hashlib
|
||||
return hashlib.md5(':'.join(po_ids).encode()).hexdigest()[:16]
|
||||
return ''
|
||||
elif alert_type == 'procurement_approval_reminder':
|
||||
return metadata.get('po_id', '')
|
||||
elif alert_type == 'procurement_critical_po':
|
||||
return metadata.get('po_id', '')
|
||||
elif alert_type == 'procurement_po_approved':
|
||||
return metadata.get('po_id', '')
|
||||
elif alert_type == 'procurement_auto_approval_summary':
|
||||
# Daily summary - use date as identifier
|
||||
summary_date = metadata.get('summary_date', '')[:10] # Date only
|
||||
return f"summary:{summary_date}"
|
||||
|
||||
# Production alerts
|
||||
elif alert_type in ['severe_capacity_overload', 'capacity_overload', 'near_capacity']:
|
||||
return f"capacity:{metadata.get('planned_date', '')}"
|
||||
elif alert_type == 'production_delay':
|
||||
return metadata.get('batch_id', '')
|
||||
elif alert_type == 'quality_control_failure':
|
||||
return metadata.get('quality_check_id', '')
|
||||
elif alert_type in ['equipment_failure', 'maintenance_required', 'low_equipment_efficiency']:
|
||||
return metadata.get('equipment_id', '')
|
||||
elif alert_type == 'production_ingredient_shortage':
|
||||
return metadata.get('ingredient_id', '')
|
||||
|
||||
# Forecasting alerts
|
||||
elif alert_type in ['demand_surge_weekend', 'holiday_preparation', 'demand_spike_detected', 'unexpected_demand_spike']:
|
||||
return f"{alert_type}:{metadata.get('product_name', '')}:{metadata.get('forecast_date', '')}"
|
||||
elif alert_type == 'weather_impact_alert':
|
||||
return f"weather:{metadata.get('forecast_date', '')}"
|
||||
elif alert_type == 'severe_weather_impact':
|
||||
return f"severe_weather:{metadata.get('weather_type', '')}:{metadata.get('duration_hours', '')}"
|
||||
|
||||
else:
|
||||
# Fallback to generic metadata.id or empty string
|
||||
return metadata.get('id', '')
|
||||
@@ -485,25 +552,20 @@ class BaseAlertService:
|
||||
|
||||
class AlertServiceMixin:
|
||||
"""Mixin providing common alert helper methods"""
|
||||
|
||||
def format_spanish_message(self, template_key: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Format Spanish alert message"""
|
||||
from shared.alerts.templates import format_item_message
|
||||
return format_item_message(template_key, 'es', **kwargs)
|
||||
|
||||
|
||||
def get_business_hours_severity(self, base_severity: str) -> str:
|
||||
"""Adjust severity based on business hours"""
|
||||
current_hour = datetime.now().hour
|
||||
|
||||
|
||||
# Reduce non-critical severity outside business hours (7-20)
|
||||
if not (7 <= current_hour <= 20):
|
||||
if base_severity == 'medium':
|
||||
return 'low'
|
||||
elif base_severity == 'high' and current_hour < 6 or current_hour > 22:
|
||||
return 'medium'
|
||||
|
||||
|
||||
return base_severity
|
||||
|
||||
|
||||
def should_send_recommendation(self, tenant_id: UUID, rec_type: str) -> bool:
|
||||
"""Check if recommendation should be sent based on tenant preferences"""
|
||||
# Implement tenant-specific recommendation frequency limits
|
||||
|
||||
Reference in New Issue
Block a user