279 lines
11 KiB
Python
279 lines
11 KiB
Python
# services/notification/app/services/notification_orchestrator.py
|
|
"""
|
|
Notification orchestrator for managing delivery across all channels
|
|
Includes SSE integration for real-time dashboard updates
|
|
"""
|
|
|
|
from typing import List, Dict, Any
|
|
from datetime import datetime
|
|
import structlog
|
|
|
|
from .email_service import EmailService
|
|
from .whatsapp_service import WhatsAppService
|
|
from .sse_service import SSEService
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
class NotificationOrchestrator:
|
|
"""
|
|
Orchestrates delivery across all notification channels
|
|
Now includes SSE for real-time dashboard updates, with support for recommendations
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
email_service: EmailService,
|
|
whatsapp_service: WhatsAppService,
|
|
sse_service: SSEService,
|
|
push_service=None # Optional push service
|
|
):
|
|
self.email_service = email_service
|
|
self.whatsapp_service = whatsapp_service
|
|
self.sse_service = sse_service
|
|
self.push_service = push_service
|
|
|
|
async def send_notification(
|
|
self,
|
|
tenant_id: str,
|
|
notification: Dict[str, Any],
|
|
channels: List[str]
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Send notification through specified channels
|
|
Channels can include: email, whatsapp, push, dashboard (SSE)
|
|
"""
|
|
results = {}
|
|
|
|
# Always send to dashboard for visibility (SSE)
|
|
if 'dashboard' in channels or notification.get('type') in ['alert', 'recommendation']:
|
|
try:
|
|
await self.sse_service.send_item_notification(
|
|
tenant_id,
|
|
notification
|
|
)
|
|
results['dashboard'] = {'status': 'sent', 'timestamp': datetime.utcnow().isoformat()}
|
|
logger.info("Item sent to dashboard via SSE",
|
|
tenant_id=tenant_id,
|
|
item_type=notification.get('type'),
|
|
item_id=notification.get('id'))
|
|
except Exception as e:
|
|
logger.error("Failed to send to dashboard",
|
|
tenant_id=tenant_id,
|
|
error=str(e))
|
|
results['dashboard'] = {'status': 'failed', 'error': str(e)}
|
|
|
|
# Send to email channel
|
|
if 'email' in channels:
|
|
try:
|
|
email_result = await self.email_service.send_notification_email(
|
|
to_email=notification.get('email'),
|
|
subject=notification.get('title'),
|
|
template_data={
|
|
'title': notification.get('title'),
|
|
'message': notification.get('message'),
|
|
'severity': notification.get('severity'),
|
|
'item_type': notification.get('type'),
|
|
'actions': notification.get('actions', []),
|
|
'metadata': notification.get('metadata', {}),
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
},
|
|
notification_type=notification.get('type', 'alert')
|
|
)
|
|
results['email'] = email_result
|
|
except Exception as e:
|
|
logger.error("Failed to send email",
|
|
tenant_id=tenant_id,
|
|
error=str(e))
|
|
results['email'] = {'status': 'failed', 'error': str(e)}
|
|
|
|
# Send to WhatsApp channel
|
|
if 'whatsapp' in channels:
|
|
try:
|
|
whatsapp_result = await self.whatsapp_service.send_notification_message(
|
|
to_phone=notification.get('phone'),
|
|
message=self._format_whatsapp_message(notification),
|
|
notification_type=notification.get('type', 'alert')
|
|
)
|
|
results['whatsapp'] = whatsapp_result
|
|
except Exception as e:
|
|
logger.error("Failed to send WhatsApp",
|
|
tenant_id=tenant_id,
|
|
error=str(e))
|
|
results['whatsapp'] = {'status': 'failed', 'error': str(e)}
|
|
|
|
# Send to push notification channel
|
|
if 'push' in channels and self.push_service:
|
|
try:
|
|
push_result = await self.push_service.send_notification(
|
|
user_id=notification.get('user_id'),
|
|
title=notification.get('title'),
|
|
body=notification.get('message'),
|
|
data={
|
|
'item_type': notification.get('type'),
|
|
'severity': notification.get('severity'),
|
|
'item_id': notification.get('id'),
|
|
'metadata': notification.get('metadata', {})
|
|
}
|
|
)
|
|
results['push'] = push_result
|
|
except Exception as e:
|
|
logger.error("Failed to send push notification",
|
|
tenant_id=tenant_id,
|
|
error=str(e))
|
|
results['push'] = {'status': 'failed', 'error': str(e)}
|
|
|
|
# Log summary
|
|
successful_channels = [ch for ch, result in results.items() if result.get('status') == 'sent']
|
|
failed_channels = [ch for ch, result in results.items() if result.get('status') == 'failed']
|
|
|
|
logger.info("Notification delivery completed",
|
|
tenant_id=tenant_id,
|
|
item_type=notification.get('type'),
|
|
item_id=notification.get('id'),
|
|
successful_channels=successful_channels,
|
|
failed_channels=failed_channels,
|
|
total_channels=len(channels))
|
|
|
|
return {
|
|
'status': 'completed',
|
|
'successful_channels': successful_channels,
|
|
'failed_channels': failed_channels,
|
|
'results': results,
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
}
|
|
|
|
def _format_whatsapp_message(self, notification: Dict[str, Any]) -> str:
|
|
"""Format message for WhatsApp with emojis and structure"""
|
|
item_type = notification.get('type', 'alert')
|
|
severity = notification.get('severity', 'medium')
|
|
|
|
# Get appropriate emoji
|
|
type_emoji = '🚨' if item_type == 'alert' else '💡'
|
|
severity_emoji = {
|
|
'urgent': '🔴',
|
|
'high': '🟡',
|
|
'medium': '🔵',
|
|
'low': '🟢'
|
|
}.get(severity, '🔵')
|
|
|
|
message = f"{type_emoji} {severity_emoji} *{notification.get('title', 'Notificación')}*\n\n"
|
|
message += f"{notification.get('message', '')}\n"
|
|
|
|
# Add actions if available
|
|
actions = notification.get('actions', [])
|
|
if actions and len(actions) > 0:
|
|
message += "\n*Acciones sugeridas:*\n"
|
|
for i, action in enumerate(actions[:3], 1): # Limit to 3 actions for WhatsApp
|
|
message += f"{i}. {action}\n"
|
|
|
|
# Add timestamp
|
|
message += f"\n_Enviado: {datetime.now().strftime('%H:%M, %d/%m/%Y')}_"
|
|
|
|
return message
|
|
|
|
def get_channels_by_severity(self, severity: str, item_type: str, hour: int = None) -> List[str]:
|
|
"""
|
|
Determine notification channels based on severity and item_type
|
|
Now includes 'dashboard' as a channel
|
|
"""
|
|
if hour is None:
|
|
hour = datetime.now().hour
|
|
|
|
# Dashboard always gets all items
|
|
channels = ['dashboard']
|
|
|
|
if item_type == 'alert':
|
|
if severity == 'urgent':
|
|
# Urgent alerts: All channels immediately
|
|
channels.extend(['email', 'whatsapp', 'push'])
|
|
|
|
elif severity == 'high':
|
|
# High alerts: Email and WhatsApp during extended hours
|
|
if 6 <= hour <= 22:
|
|
channels.extend(['email', 'whatsapp'])
|
|
else:
|
|
channels.append('email') # Email only during night
|
|
|
|
elif severity == 'medium':
|
|
# Medium alerts: Email during business hours
|
|
if 7 <= hour <= 20:
|
|
channels.append('email')
|
|
|
|
elif item_type == 'recommendation':
|
|
# Recommendations: Generally less urgent, respect business hours
|
|
if severity in ['medium', 'high']:
|
|
if 8 <= hour <= 19: # Stricter business hours for recommendations
|
|
channels.append('email')
|
|
# Low/urgent: Dashboard only (urgent rare for recommendations)
|
|
|
|
return channels
|
|
|
|
async def health_check(self) -> Dict[str, Any]:
|
|
"""Check health of all notification channels"""
|
|
health_status = {
|
|
'status': 'healthy',
|
|
'channels': {},
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
}
|
|
|
|
# Check email service
|
|
try:
|
|
email_health = await self.email_service.health_check()
|
|
health_status['channels']['email'] = email_health
|
|
except Exception as e:
|
|
health_status['channels']['email'] = {'status': 'unhealthy', 'error': str(e)}
|
|
|
|
# Check WhatsApp service
|
|
try:
|
|
whatsapp_health = await self.whatsapp_service.health_check()
|
|
health_status['channels']['whatsapp'] = whatsapp_health
|
|
except Exception as e:
|
|
health_status['channels']['whatsapp'] = {'status': 'unhealthy', 'error': str(e)}
|
|
|
|
# Check SSE service
|
|
try:
|
|
sse_metrics = self.sse_service.get_metrics()
|
|
sse_status = 'healthy' if sse_metrics['redis_connected'] else 'unhealthy'
|
|
health_status['channels']['sse'] = {
|
|
'status': sse_status,
|
|
'metrics': sse_metrics
|
|
}
|
|
except Exception as e:
|
|
health_status['channels']['sse'] = {'status': 'unhealthy', 'error': str(e)}
|
|
|
|
# Check push service if available
|
|
if self.push_service:
|
|
try:
|
|
push_health = await self.push_service.health_check()
|
|
health_status['channels']['push'] = push_health
|
|
except Exception as e:
|
|
health_status['channels']['push'] = {'status': 'unhealthy', 'error': str(e)}
|
|
|
|
# Determine overall status
|
|
unhealthy_channels = [
|
|
ch for ch, status in health_status['channels'].items()
|
|
if status.get('status') != 'healthy'
|
|
]
|
|
|
|
if unhealthy_channels:
|
|
health_status['status'] = 'degraded' if len(unhealthy_channels) < len(health_status['channels']) else 'unhealthy'
|
|
health_status['unhealthy_channels'] = unhealthy_channels
|
|
|
|
return health_status
|
|
|
|
def get_metrics(self) -> Dict[str, Any]:
|
|
"""Get aggregated metrics from all services"""
|
|
metrics = {
|
|
'timestamp': datetime.utcnow().isoformat(),
|
|
'channels': {}
|
|
}
|
|
|
|
# Get SSE metrics
|
|
try:
|
|
metrics['channels']['sse'] = self.sse_service.get_metrics()
|
|
except Exception as e:
|
|
logger.error("Failed to get SSE metrics", error=str(e))
|
|
|
|
# Additional metrics could be added here for other services
|
|
|
|
return metrics |