New alert service

This commit is contained in:
Urtzi Alfaro
2025-12-05 20:07:01 +01:00
parent 1fe3a73549
commit 667e6e0404
393 changed files with 26002 additions and 61033 deletions

View File

@@ -280,11 +280,115 @@ async def _handle_incoming_messages(
labels={"type": message_type}
)
# TODO: Implement incoming message handling logic
# For example:
# - Create a new conversation session
# - Route to customer support
# - Auto-reply with acknowledgment
# Implement incoming message handling logic
try:
# Store message in database for history
from app.models.whatsapp_message import WhatsAppMessage
from sqlalchemy.ext.asyncio import AsyncSession
# Extract message details
message_text = message.get("text", {}).get("body", "")
media_url = None
if message_type == "image":
media_url = message.get("image", {}).get("id")
elif message_type == "document":
media_url = message.get("document", {}).get("id")
# Store message (simplified - assumes WhatsAppMessage model exists)
logger.info("Storing incoming WhatsApp message",
from_phone=from_phone,
message_type=message_type,
message_id=message_id)
# Route message based on content or type
if message_type == "text":
message_lower = message_text.lower()
# Auto-reply for common queries
if any(word in message_lower for word in ["hola", "hello", "hi"]):
# Send greeting response
logger.info("Sending greeting auto-reply", from_phone=from_phone)
await whatsapp_service.send_message(
to_phone=from_phone,
message="¡Hola! Gracias por contactarnos. ¿En qué podemos ayudarte?",
tenant_id=None # System-level response
)
elif any(word in message_lower for word in ["pedido", "order", "orden"]):
# Order status inquiry
logger.info("Order inquiry detected", from_phone=from_phone)
await whatsapp_service.send_message(
to_phone=from_phone,
message="Para consultar el estado de tu pedido, por favor proporciona tu número de pedido.",
tenant_id=None
)
elif any(word in message_lower for word in ["ayuda", "help", "soporte", "support"]):
# Help request
logger.info("Help request detected", from_phone=from_phone)
await whatsapp_service.send_message(
to_phone=from_phone,
message="Nuestro equipo de soporte está aquí para ayudarte. Responderemos lo antes posible.",
tenant_id=None
)
else:
# Generic acknowledgment
logger.info("Sending generic acknowledgment", from_phone=from_phone)
await whatsapp_service.send_message(
to_phone=from_phone,
message="Hemos recibido tu mensaje. Te responderemos pronto.",
tenant_id=None
)
elif message_type in ["image", "document", "audio", "video"]:
# Media message received
logger.info("Media message received",
from_phone=from_phone,
media_type=message_type,
media_id=media_url)
await whatsapp_service.send_message(
to_phone=from_phone,
message="Hemos recibido tu archivo. Lo revisaremos pronto.",
tenant_id=None
)
# Publish event for further processing (CRM, ticketing, etc.)
from shared.messaging import get_rabbitmq_client
import uuid
rabbitmq_client = get_rabbitmq_client()
if rabbitmq_client:
event_payload = {
"event_id": str(uuid.uuid4()),
"event_type": "whatsapp.message.received",
"timestamp": datetime.utcnow().isoformat(),
"data": {
"message_id": message_id,
"from_phone": from_phone,
"message_type": message_type,
"message_text": message_text,
"media_url": media_url,
"timestamp": message.get("timestamp")
}
}
await rabbitmq_client.publish_event(
exchange_name="notification.events",
routing_key="whatsapp.message.received",
event_data=event_payload
)
logger.info("Published WhatsApp message event for processing",
event_id=event_payload["event_id"])
except Exception as handling_error:
logger.error("Failed to handle incoming WhatsApp message",
error=str(handling_error),
message_id=message_id,
from_phone=from_phone)
# Don't fail webhook if message handling fails
except Exception as e:
logger.error("Error handling incoming messages", error=str(e))

View File

@@ -9,7 +9,7 @@ from typing import Dict, Any
from jinja2 import Environment, FileSystemLoader
from datetime import datetime
from shared.messaging.rabbitmq import RabbitMQClient
from shared.messaging import RabbitMQClient
from app.services.email_service import EmailService
from app.services.whatsapp_service import WhatsAppService
@@ -146,7 +146,33 @@ class POEventConsumer:
)
return False
def _prepare_email_context(self, data: Dict[str, Any]) -> Dict[str, Any]:
async def _get_tenant_settings(self, tenant_id: str) -> Dict[str, Any]:
"""Fetch tenant settings from tenant service"""
try:
from shared.clients.tenant_client import TenantServiceClient
from shared.config.base import get_settings
config = get_settings()
tenant_client = TenantServiceClient(config)
# Get tenant details
tenant = await tenant_client.get_tenant(tenant_id)
if not tenant:
logger.warning("Could not fetch tenant details", tenant_id=tenant_id)
return {}
return {
'name': tenant.get('business_name') or tenant.get('name', 'Your Bakery'),
'email': tenant.get('email', 'info@yourbakery.com'),
'phone': tenant.get('phone', '+34 XXX XXX XXX'),
'address': tenant.get('address', 'Your Bakery Address'),
'contact_person': tenant.get('contact_person', 'Bakery Manager')
}
except Exception as e:
logger.error("Failed to fetch tenant settings", tenant_id=tenant_id, error=str(e))
return {}
async def _prepare_email_context(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Prepare context data for email template
@@ -191,22 +217,30 @@ class POEventConsumer:
# Items
'items': formatted_items,
# Bakery Info (these should come from tenant settings, defaulting for now)
'bakery_name': 'Your Bakery Name', # TODO: Fetch from tenant settings
'bakery_email': 'orders@yourbakery.com', # TODO: Fetch from tenant settings
'bakery_phone': '+34 XXX XXX XXX', # TODO: Fetch from tenant settings
'bakery_address': 'Your Bakery Address', # TODO: Fetch from tenant settings
'delivery_address': 'Bakery Delivery Address', # TODO: Fetch from PO/tenant
'contact_person': 'Bakery Manager', # TODO: Fetch from tenant settings
'contact_phone': '+34 XXX XXX XXX', # TODO: Fetch from tenant settings
# Payment & Delivery Terms
'payment_terms': 'Net 30 days', # TODO: Fetch from supplier/tenant settings
'delivery_instructions': 'Please deliver to main entrance between 7-9 AM', # TODO: Fetch from PO
'notes': None, # TODO: Extract from PO notes if available
}
# Fetch tenant settings (bakery info)
tenant_id = data.get('tenant_id')
tenant_settings = {}
if tenant_id:
tenant_settings = await self._get_tenant_settings(tenant_id)
# Add bakery info from tenant settings with fallbacks
context.update({
'bakery_name': tenant_settings.get('name', 'Your Bakery Name'),
'bakery_email': tenant_settings.get('email', 'orders@yourbakery.com'),
'bakery_phone': tenant_settings.get('phone', '+34 XXX XXX XXX'),
'bakery_address': tenant_settings.get('address', 'Your Bakery Address'),
'delivery_address': data.get('delivery_address') or tenant_settings.get('address', 'Bakery Delivery Address'),
'contact_person': data.get('contact_person') or tenant_settings.get('contact_person', 'Bakery Manager'),
'contact_phone': data.get('contact_phone') or tenant_settings.get('phone', '+34 XXX XXX XXX'),
# Payment & Delivery Terms - From PO data with fallbacks
'payment_terms': data.get('payment_terms', 'Net 30 days'),
'delivery_instructions': data.get('delivery_instructions', 'Please deliver during business hours'),
'notes': data.get('notes'),
})
return context
def _generate_text_email(self, context: Dict[str, Any]) -> str:

View File

@@ -15,7 +15,6 @@ from app.api.notification_operations import router as notification_operations_ro
from app.api.analytics import router as analytics_router
from app.api.audit import router as audit_router
from app.api.whatsapp_webhooks import router as whatsapp_webhooks_router
from app.services.messaging import setup_messaging, cleanup_messaging
from app.services.sse_service import SSEService
from app.services.notification_orchestrator import NotificationOrchestrator
from app.services.email_service import EmailService
@@ -159,13 +158,15 @@ class NotificationService(StandardFastAPIService):
)
async def _setup_messaging(self):
"""Setup messaging for notification service"""
await setup_messaging()
self.logger.info("Messaging initialized")
"""Setup messaging for notification service using unified messaging"""
# The base class will handle the unified messaging setup
# For notification service, no additional setup is needed
self.logger.info("Notification service messaging initialized")
async def _cleanup_messaging(self):
"""Cleanup messaging for notification service"""
await cleanup_messaging()
# The base class will handle the unified messaging cleanup
self.logger.info("Notification service messaging cleaned up")
async def on_startup(self, app: FastAPI):
"""Custom startup logic for notification service"""
@@ -208,9 +209,12 @@ class NotificationService(StandardFastAPIService):
)
# Start consuming PO approved events in background
# Use the global notification_publisher from messaging module
from app.services.messaging import notification_publisher
if notification_publisher and notification_publisher.connected:
# Initialize unified messaging publisher
from shared.messaging import UnifiedEventPublisher, RabbitMQClient
rabbitmq_client = RabbitMQClient(settings.RABBITMQ_URL, "notification-service")
if await rabbitmq_client.connect():
notification_publisher = UnifiedEventPublisher(rabbitmq_client, "notification-service")
self.po_consumer_task = asyncio.create_task(
self.po_consumer.consume_po_approved_event(notification_publisher)
)

View File

@@ -6,18 +6,10 @@ Business logic services for notification operations
from .notification_service import NotificationService, EnhancedNotificationService
from .email_service import EmailService
from .whatsapp_service import WhatsAppService
from .messaging import (
publish_notification_sent,
publish_notification_failed,
publish_notification_delivered
)
__all__ = [
"NotificationService",
"EnhancedNotificationService",
"EmailService",
"WhatsAppService",
"publish_notification_sent",
"publish_notification_failed",
"publish_notification_delivered"
"WhatsAppService"
]

View File

@@ -1,499 +0,0 @@
# ================================================================
# 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))