New alert service
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user