Files
bakery-ia/services/notification/app/services/messaging.py
2025-07-21 22:44:11 +02:00

499 lines
19 KiB
Python

# ================================================================
# services/notification/app/services/messaging.py
# ================================================================
"""
Messaging service for notification events
Handles RabbitMQ integration for the notification service
"""
import structlog
from typing import Dict, Any
import asyncio
from shared.messaging.rabbitmq import RabbitMQClient
from shared.messaging.events import BaseEvent
from app.core.config import settings
logger = structlog.get_logger()
# Global messaging instance
notification_publisher = None
async def setup_messaging():
"""Initialize messaging services for notification service"""
global notification_publisher
try:
notification_publisher = RabbitMQClient(settings.RABBITMQ_URL, service_name="notification-service")
await notification_publisher.connect()
# Set up event consumers
await _setup_event_consumers()
logger.info("Notification service messaging setup completed")
except Exception as e:
logger.error("Failed to setup notification messaging", error=str(e))
raise
async def cleanup_messaging():
"""Cleanup messaging services"""
global notification_publisher
try:
if notification_publisher:
await notification_publisher.disconnect()
logger.info("Notification service messaging cleanup completed")
except Exception as e:
logger.error("Error during notification messaging cleanup", error=str(e))
async def _setup_event_consumers():
"""Setup event consumers for other service events"""
try:
# Listen for user registration events (from auth service)
await notification_publisher.consume_events(
exchange_name="user.events",
queue_name="notification_user_registered_queue",
routing_key="user.registered",
callback=handle_user_registered
)
# Listen for forecast alert events (from forecasting service)
await notification_publisher.consume_events(
exchange_name="forecast.events",
queue_name="notification_forecast_alert_queue",
routing_key="forecast.alert_generated",
callback=handle_forecast_alert
)
# Listen for training completion events (from training service)
await notification_publisher.consume_events(
exchange_name="training.events",
queue_name="notification_training_completed_queue",
routing_key="training.completed",
callback=handle_training_completed
)
# Listen for data import events (from data service)
await notification_publisher.consume_events(
exchange_name="data.events",
queue_name="notification_data_imported_queue",
routing_key="data.imported",
callback=handle_data_imported
)
logger.info("Notification event consumers setup completed")
except Exception as e:
logger.error("Failed to setup notification event consumers", error=str(e))
# ================================================================
# EVENT HANDLERS
# ================================================================
async def handle_user_registered(message):
"""Handle user registration events"""
try:
import json
from app.services.notification_service import NotificationService
from app.schemas.notifications import NotificationCreate, NotificationType
# Parse message
data = json.loads(message.body.decode())
user_data = data.get("data", {})
logger.info("Handling user registration event", user_id=user_data.get("user_id"))
# Send welcome email
notification_service = NotificationService()
welcome_notification = NotificationCreate(
type=NotificationType.EMAIL,
recipient_email=user_data.get("email"),
template_id="welcome",
template_data={
"user_name": user_data.get("full_name", "Usuario"),
"dashboard_url": f"{settings.FRONTEND_API_URL}/dashboard"
},
tenant_id=user_data.get("tenant_id"),
sender_id="system"
)
await notification_service.send_notification(welcome_notification)
# Acknowledge message
await message.ack()
except Exception as e:
logger.error("Failed to handle user registration event", error=str(e))
await message.nack(requeue=False)
async def handle_forecast_alert(message):
"""Handle forecast alert events"""
try:
import json
from app.services.notification_service import NotificationService
from app.schemas.notifications import NotificationCreate, NotificationType
# Parse message
data = json.loads(message.body.decode())
alert_data = data.get("data", {})
logger.info("Handling forecast alert event",
tenant_id=alert_data.get("tenant_id"),
product=alert_data.get("product_name"))
# Send alert notification to tenant users
notification_service = NotificationService()
# Email alert
email_notification = NotificationCreate(
type=NotificationType.EMAIL,
template_id="forecast_alert",
template_data={
"bakery_name": alert_data.get("bakery_name", "Tu Panadería"),
"product_name": alert_data.get("product_name"),
"forecast_date": alert_data.get("forecast_date"),
"predicted_demand": alert_data.get("predicted_demand"),
"variation_percentage": alert_data.get("variation_percentage"),
"alert_message": alert_data.get("message"),
"dashboard_url": f"{settings.FRONTEND_API_URL}/forecasts"
},
tenant_id=alert_data.get("tenant_id"),
sender_id="system",
broadcast=True,
priority="high"
)
await notification_service.send_notification(email_notification)
# WhatsApp alert for urgent cases
if alert_data.get("severity") == "urgent":
whatsapp_notification = NotificationCreate(
type=NotificationType.WHATSAPP,
message=f"🚨 ALERTA: {alert_data.get('product_name')} - Variación del {alert_data.get('variation_percentage')}% para {alert_data.get('forecast_date')}. Revisar pronósticos.",
tenant_id=alert_data.get("tenant_id"),
sender_id="system",
broadcast=True,
priority="urgent"
)
await notification_service.send_notification(whatsapp_notification)
# Acknowledge message
await message.ack()
except Exception as e:
logger.error("Failed to handle forecast alert event", error=str(e))
await message.nack(requeue=False)
async def handle_training_completed(message):
"""Handle training completion events"""
try:
import json
from app.services.notification_service import NotificationService
from app.schemas.notifications import NotificationCreate, NotificationType
# Parse message
data = json.loads(message.body.decode())
training_data = data.get("data", {})
logger.info("Handling training completion event",
tenant_id=training_data.get("tenant_id"),
job_id=training_data.get("job_id"))
# Send training completion notification
notification_service = NotificationService()
success = training_data.get("success", False)
template_data = {
"bakery_name": training_data.get("bakery_name", "Tu Panadería"),
"job_id": training_data.get("job_id"),
"model_name": training_data.get("model_name"),
"accuracy": training_data.get("accuracy"),
"completion_time": training_data.get("completion_time"),
"dashboard_url": f"{settings.FRONTEND_API_URL}/models"
}
if success:
subject = "✅ Entrenamiento de Modelo Completado"
template_data["status"] = "exitoso"
template_data["message"] = f"El modelo {training_data.get('model_name')} se ha entrenado correctamente con una precisión del {training_data.get('accuracy')}%."
else:
subject = "❌ Error en Entrenamiento de Modelo"
template_data["status"] = "fallido"
template_data["message"] = f"El entrenamiento del modelo {training_data.get('model_name')} ha fallado. Error: {training_data.get('error_message', 'Error desconocido')}"
notification = NotificationCreate(
type=NotificationType.EMAIL,
subject=subject,
message=template_data["message"],
template_data=template_data,
tenant_id=training_data.get("tenant_id"),
sender_id="system",
broadcast=True
)
await notification_service.send_notification(notification)
# Acknowledge message
await message.ack()
except Exception as e:
logger.error("Failed to handle training completion event", error=str(e))
await message.nack(requeue=False)
async def handle_data_imported(message):
"""Handle data import events"""
try:
import json
from app.services.notification_service import NotificationService
from app.schemas.notifications import NotificationCreate, NotificationType
# Parse message
data = json.loads(message.body.decode())
import_data = data.get("data", {})
logger.info("Handling data import event",
tenant_id=import_data.get("tenant_id"),
data_type=import_data.get("data_type"))
# Only send notifications for significant data imports
records_count = import_data.get("records_count", 0)
if records_count < 100: # Skip notification for small imports
await message.ack()
return
# Send data import notification
notification_service = NotificationService()
template_data = {
"bakery_name": import_data.get("bakery_name", "Tu Panadería"),
"data_type": import_data.get("data_type"),
"records_count": records_count,
"import_date": import_data.get("import_date"),
"source": import_data.get("source", "Manual"),
"dashboard_url": f"{settings.FRONTEND_API_URL}/data"
}
notification = NotificationCreate(
type=NotificationType.EMAIL,
subject=f"📊 Datos Importados: {import_data.get('data_type')}",
message=f"Se han importado {records_count} registros de {import_data.get('data_type')} desde {import_data.get('source')}.",
template_data=template_data,
tenant_id=import_data.get("tenant_id"),
sender_id="system"
)
await notification_service.send_notification(notification)
# Acknowledge message
await message.ack()
except Exception as e:
logger.error("Failed to handle data import event", error=str(e))
await message.nack(requeue=False)
# ================================================================
# NOTIFICATION EVENT PUBLISHERS
# ================================================================
async def publish_notification_sent(notification_data: Dict[str, Any]) -> bool:
"""Publish notification sent event"""
try:
if notification_publisher:
return await notification_publisher.publish_event(
"notification.events",
"notification.sent",
notification_data
)
else:
logger.warning("Notification publisher not initialized")
return False
except Exception as e:
logger.error("Failed to publish notification sent event", error=str(e))
return False
async def publish_notification_failed(notification_data: Dict[str, Any]) -> bool:
"""Publish notification failed event"""
try:
if notification_publisher:
return await notification_publisher.publish_event(
"notification.events",
"notification.failed",
notification_data
)
else:
logger.warning("Notification publisher not initialized")
return False
except Exception as e:
logger.error("Failed to publish notification failed event", error=str(e))
return False
async def publish_notification_delivered(notification_data: Dict[str, Any]) -> bool:
"""Publish notification delivered event"""
try:
if notification_publisher:
return await notification_publisher.publish_event(
"notification.events",
"notification.delivered",
notification_data
)
else:
logger.warning("Notification publisher not initialized")
return False
except Exception as e:
logger.error("Failed to publish notification delivered event", error=str(e))
return False
async def publish_bulk_notification_completed(bulk_data: Dict[str, Any]) -> bool:
"""Publish bulk notification completion event"""
try:
if notification_publisher:
return await notification_publisher.publish_event(
"notification.events",
"notification.bulk_completed",
bulk_data
)
else:
logger.warning("Notification publisher not initialized")
return False
except Exception as e:
logger.error("Failed to publish bulk notification event", error=str(e))
return False
# ================================================================
# WEBHOOK HANDLERS (for external delivery status updates)
# ================================================================
async def handle_email_delivery_webhook(webhook_data: Dict[str, Any]):
"""Handle email delivery status webhooks (e.g., from SendGrid, Mailgun)"""
try:
notification_id = webhook_data.get("notification_id")
status = webhook_data.get("status")
logger.info("Received email delivery webhook",
notification_id=notification_id,
status=status)
# Update notification status in database
from app.services.notification_service import NotificationService
notification_service = NotificationService()
# This would require additional method in NotificationService
# await notification_service.update_delivery_status(notification_id, status)
# Publish delivery event
await publish_notification_delivered({
"notification_id": notification_id,
"status": status,
"delivery_time": webhook_data.get("timestamp"),
"provider": webhook_data.get("provider")
})
except Exception as e:
logger.error("Failed to handle email delivery webhook", error=str(e))
async def handle_whatsapp_delivery_webhook(webhook_data: Dict[str, Any]):
"""Handle WhatsApp delivery status webhooks (from Twilio)"""
try:
message_sid = webhook_data.get("MessageSid")
status = webhook_data.get("MessageStatus")
logger.info("Received WhatsApp delivery webhook",
message_sid=message_sid,
status=status)
# Map Twilio status to our status
status_mapping = {
"sent": "sent",
"delivered": "delivered",
"read": "read",
"failed": "failed",
"undelivered": "failed"
}
mapped_status = status_mapping.get(status, status)
# Publish delivery event
await publish_notification_delivered({
"provider_message_id": message_sid,
"status": mapped_status,
"delivery_time": webhook_data.get("timestamp"),
"provider": "twilio"
})
except Exception as e:
logger.error("Failed to handle WhatsApp delivery webhook", error=str(e))
# ================================================================
# SCHEDULED NOTIFICATION PROCESSING
# ================================================================
async def process_scheduled_notifications():
"""Process scheduled notifications (called by background task)"""
try:
from datetime import datetime
from app.core.database import get_db
from app.models.notifications import Notification, NotificationStatus
from app.services.notification_service import NotificationService
from sqlalchemy import select, and_
logger.info("Processing scheduled notifications")
async for db in get_db():
# Get notifications scheduled for now or earlier
now = datetime.utcnow()
result = await db.execute(
select(Notification).where(
and_(
Notification.status == NotificationStatus.PENDING,
Notification.scheduled_at <= now,
Notification.scheduled_at.isnot(None)
)
).limit(100) # Process in batches
)
scheduled_notifications = result.scalars().all()
if not scheduled_notifications:
return
logger.info("Found scheduled notifications to process",
count=len(scheduled_notifications))
notification_service = NotificationService()
for notification in scheduled_notifications:
try:
# Convert to schema for processing
from app.schemas.notifications import NotificationCreate, NotificationType
notification_create = NotificationCreate(
type=NotificationType(notification.type.value),
recipient_id=str(notification.recipient_id) if notification.recipient_id else None,
recipient_email=notification.recipient_email,
recipient_phone=notification.recipient_phone,
subject=notification.subject,
message=notification.message,
html_content=notification.html_content,
template_id=notification.template_id,
template_data=notification.template_data,
priority=notification.priority,
tenant_id=str(notification.tenant_id),
sender_id=str(notification.sender_id),
broadcast=notification.broadcast
)
# Process the scheduled notification
await notification_service.send_notification(notification_create)
except Exception as e:
logger.error("Failed to process scheduled notification",
notification_id=str(notification.id),
error=str(e))
await db.commit()
except Exception as e:
logger.error("Failed to process scheduled notifications", error=str(e))