Added Notifications Microservice API - Integrated notification system with MongoDB, RabbitMQ consumer, and automatic status change notifications for reports
This commit is contained in:
53
src/application/ports/notification_repository.py
Normal file
53
src/application/ports/notification_repository.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Port (interface) for Notification Repository"""
|
||||
from abc import ABC, abstractmethod
|
||||
from domain.notifications import Notification
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class NotificationRepository(ABC):
|
||||
"""Puerto (interfaz) para el repositorio de Notificaciones"""
|
||||
|
||||
@abstractmethod
|
||||
def save(self, notification: Notification) -> Notification:
|
||||
"""Guarda una notificación en la base de datos"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_id(self, notification_id: str) -> Optional[Notification]:
|
||||
"""Obtiene una notificación por ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_user_id(self, user_id: int) -> List[Notification]:
|
||||
"""Obtiene todas las notificaciones de un usuario"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_unread_by_user_id(self, user_id: int) -> List[Notification]:
|
||||
"""Obtiene todas las notificaciones no leídas de un usuario"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_all(self) -> List[Notification]:
|
||||
"""Obtiene todas las notificaciones"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def mark_as_read(self, notification_id: str) -> bool:
|
||||
"""Marca una notificación como leída"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def mark_all_as_read(self, user_id: int) -> bool:
|
||||
"""Marca todas las notificaciones de un usuario como leídas"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, notification_id: str) -> bool:
|
||||
"""Elimina una notificación"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_all_by_user(self, user_id: int) -> bool:
|
||||
"""Elimina todas las notificaciones de un usuario"""
|
||||
pass
|
||||
173
src/application/services/notification_services.py
Normal file
173
src/application/services/notification_services.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""Notification services implementing the business logic"""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict
|
||||
from domain.notifications import Notification
|
||||
from application.ports.notification_repository import NotificationRepository
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreateNotification:
|
||||
"""Use case: Create a new notification"""
|
||||
|
||||
def __init__(self, notification_repo: NotificationRepository):
|
||||
self.notification_repo = notification_repo
|
||||
|
||||
def execute(self, id_usuario: int, tipo_notificacion: str, titulo: str,
|
||||
mensaje: str, id_reporte: Optional[str] = None,
|
||||
estado_reporte: Optional[str] = None) -> Dict:
|
||||
"""
|
||||
Creates a new notification
|
||||
|
||||
Args:
|
||||
id_usuario: User ID who receives the notification
|
||||
tipo_notificacion: Type of notification
|
||||
titulo: Notification title
|
||||
mensaje: Notification message
|
||||
id_reporte: Optional report ID related to notification
|
||||
estado_reporte: Optional report status
|
||||
|
||||
Returns:
|
||||
Dictionary with status and message
|
||||
"""
|
||||
try:
|
||||
# Validate inputs
|
||||
if not id_usuario or id_usuario <= 0:
|
||||
return {"status": "error", "message": "Usuario inválido"}
|
||||
if not tipo_notificacion or not titulo or not mensaje:
|
||||
return {"status": "error", "message": "Campos obligatorios faltantes"}
|
||||
|
||||
# Create notification domain object
|
||||
notification = Notification(
|
||||
id_notificacion=str(uuid.uuid4()),
|
||||
id_usuario=id_usuario,
|
||||
tipo_notificacion=tipo_notificacion,
|
||||
titulo=titulo,
|
||||
mensaje=mensaje,
|
||||
id_reporte=id_reporte,
|
||||
estado_reporte=estado_reporte,
|
||||
leida=False,
|
||||
fecha_creacion=datetime.utcnow()
|
||||
)
|
||||
|
||||
# Save to repository
|
||||
saved_notification = self.notification_repo.save(notification)
|
||||
logger.info(f"Notification created: {saved_notification.id_notificacion}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Notificación creada exitosamente",
|
||||
"id_notificacion": saved_notification.id_notificacion
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating notification: {e}", exc_info=True)
|
||||
return {"status": "error", "message": f"Error al crear notificación: {str(e)}"}
|
||||
|
||||
|
||||
class GetNotificationById:
|
||||
"""Use case: Get a notification by ID"""
|
||||
|
||||
def __init__(self, notification_repo: NotificationRepository):
|
||||
self.notification_repo = notification_repo
|
||||
|
||||
def execute(self, notification_id: str) -> Optional[Notification]:
|
||||
"""Retrieves a notification by ID"""
|
||||
return self.notification_repo.find_by_id(notification_id)
|
||||
|
||||
|
||||
class GetUserNotifications:
|
||||
"""Use case: Get all notifications for a user"""
|
||||
|
||||
def __init__(self, notification_repo: NotificationRepository):
|
||||
self.notification_repo = notification_repo
|
||||
|
||||
def execute(self, user_id: int) -> List[Notification]:
|
||||
"""Retrieves all notifications for a user"""
|
||||
return self.notification_repo.find_by_user_id(user_id)
|
||||
|
||||
|
||||
class GetUnreadUserNotifications:
|
||||
"""Use case: Get unread notifications for a user"""
|
||||
|
||||
def __init__(self, notification_repo: NotificationRepository):
|
||||
self.notification_repo = notification_repo
|
||||
|
||||
def execute(self, user_id: int) -> List[Notification]:
|
||||
"""Retrieves unread notifications for a user"""
|
||||
return self.notification_repo.find_unread_by_user_id(user_id)
|
||||
|
||||
|
||||
class MarkNotificationAsRead:
|
||||
"""Use case: Mark a notification as read"""
|
||||
|
||||
def __init__(self, notification_repo: NotificationRepository):
|
||||
self.notification_repo = notification_repo
|
||||
|
||||
def execute(self, notification_id: str) -> Dict:
|
||||
"""Marks a notification as read"""
|
||||
try:
|
||||
success = self.notification_repo.mark_as_read(notification_id)
|
||||
if success:
|
||||
logger.info(f"Notification marked as read: {notification_id}")
|
||||
return {"status": "success", "message": "Notificación marcada como leída"}
|
||||
else:
|
||||
return {"status": "error", "message": "Notificación no encontrada"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking notification as read: {e}", exc_info=True)
|
||||
return {"status": "error", "message": f"Error: {str(e)}"}
|
||||
|
||||
|
||||
class MarkAllUserNotificationsAsRead:
|
||||
"""Use case: Mark all notifications for a user as read"""
|
||||
|
||||
def __init__(self, notification_repo: NotificationRepository):
|
||||
self.notification_repo = notification_repo
|
||||
|
||||
def execute(self, user_id: int) -> Dict:
|
||||
"""Marks all notifications for a user as read"""
|
||||
try:
|
||||
success = self.notification_repo.mark_all_as_read(user_id)
|
||||
logger.info(f"All notifications marked as read for user: {user_id}")
|
||||
return {"status": "success", "message": "Todas las notificaciones marcadas como leídas"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking notifications as read: {e}", exc_info=True)
|
||||
return {"status": "error", "message": f"Error: {str(e)}"}
|
||||
|
||||
|
||||
class DeleteNotification:
|
||||
"""Use case: Delete a notification"""
|
||||
|
||||
def __init__(self, notification_repo: NotificationRepository):
|
||||
self.notification_repo = notification_repo
|
||||
|
||||
def execute(self, notification_id: str) -> Dict:
|
||||
"""Deletes a notification"""
|
||||
try:
|
||||
success = self.notification_repo.delete(notification_id)
|
||||
if success:
|
||||
logger.info(f"Notification deleted: {notification_id}")
|
||||
return {"status": "success", "message": "Notificación eliminada"}
|
||||
else:
|
||||
return {"status": "error", "message": "Notificación no encontrada"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting notification: {e}", exc_info=True)
|
||||
return {"status": "error", "message": f"Error: {str(e)}"}
|
||||
|
||||
|
||||
class DeleteAllUserNotifications:
|
||||
"""Use case: Delete all notifications for a user"""
|
||||
|
||||
def __init__(self, notification_repo: NotificationRepository):
|
||||
self.notification_repo = notification_repo
|
||||
|
||||
def execute(self, user_id: int) -> Dict:
|
||||
"""Deletes all notifications for a user"""
|
||||
try:
|
||||
success = self.notification_repo.delete_all_by_user(user_id)
|
||||
logger.info(f"All notifications deleted for user: {user_id}")
|
||||
return {"status": "success", "message": "Todas las notificaciones eliminadas"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting notifications: {e}", exc_info=True)
|
||||
return {"status": "error", "message": f"Error: {str(e)}"}
|
||||
135
src/consumers/notification_consumer.py
Normal file
135
src/consumers/notification_consumer.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""Notification RabbitMQ Consumer - Processes notification events"""
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
# Add src to path to import modules
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from infrastructure.adapters.rabbitmq.consumer import RabbitMQConsumer
|
||||
from infrastructure.adapters.rabbitmq.messages import NotificationMessage, NotificationEventType
|
||||
from infrastructure.adapters.persistence.notification_repository_mongo import NotificationRepositoryMongo
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NotificationConsumer:
|
||||
"""Consumer for notification events from RabbitMQ"""
|
||||
|
||||
def __init__(self):
|
||||
self.repo = NotificationRepositoryMongo()
|
||||
self.consumer = RabbitMQConsumer(queue_name='notifications_queue')
|
||||
self.consumer.set_callback(self.process_message)
|
||||
|
||||
def process_message(self, message_dict: dict):
|
||||
"""
|
||||
Processes a notification event message from RabbitMQ
|
||||
|
||||
Args:
|
||||
message_dict: Dictionary containing the message data
|
||||
"""
|
||||
try:
|
||||
# Reconstruct the NotificationMessage object
|
||||
message = NotificationMessage.from_dict(message_dict)
|
||||
|
||||
if message.event_type == NotificationEventType.CREATE:
|
||||
self._handle_create_notification(message)
|
||||
elif message.event_type == NotificationEventType.REPORT_STATUS_CHANGE:
|
||||
self._handle_report_status_change(message)
|
||||
else:
|
||||
logger.warning(f"Unknown event type: {message.event_type}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing notification message: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _handle_create_notification(self, message: NotificationMessage):
|
||||
"""Handle notification create event"""
|
||||
try:
|
||||
logger.info(f"Creating notification for user: {message.id_usuario}")
|
||||
|
||||
# Parse datetime string if provided
|
||||
fecha_creacion = None
|
||||
if message.fecha_creacion:
|
||||
try:
|
||||
fecha_creacion = datetime.fromisoformat(message.fecha_creacion)
|
||||
except (ValueError, TypeError):
|
||||
fecha_creacion = datetime.utcnow()
|
||||
else:
|
||||
fecha_creacion = datetime.utcnow()
|
||||
|
||||
from domain.notifications import Notification
|
||||
|
||||
# Create Notification domain object
|
||||
notification = Notification(
|
||||
id_notificacion=message.id_notificacion,
|
||||
id_usuario=message.id_usuario,
|
||||
tipo_notificacion=message.tipo_notificacion,
|
||||
titulo=message.titulo,
|
||||
mensaje=message.mensaje,
|
||||
id_reporte=message.id_reporte,
|
||||
estado_reporte=message.estado_reporte,
|
||||
leida=False,
|
||||
fecha_creacion=fecha_creacion
|
||||
)
|
||||
|
||||
# Save to MongoDB repository
|
||||
saved_notification = self.repo.save(notification)
|
||||
logger.info(f"Notification created successfully: {message.id_notificacion}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating notification: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _handle_report_status_change(self, message: NotificationMessage):
|
||||
"""Handle report status change notification"""
|
||||
try:
|
||||
logger.info(f"Processing report status change notification for user: {message.id_usuario}")
|
||||
|
||||
# Parse datetime string if provided
|
||||
fecha_creacion = None
|
||||
if message.fecha_creacion:
|
||||
try:
|
||||
fecha_creacion = datetime.fromisoformat(message.fecha_creacion)
|
||||
except (ValueError, TypeError):
|
||||
fecha_creacion = datetime.utcnow()
|
||||
else:
|
||||
fecha_creacion = datetime.utcnow()
|
||||
|
||||
from domain.notifications import Notification
|
||||
|
||||
# Create Notification domain object for status change
|
||||
notification = Notification(
|
||||
id_notificacion=message.id_notificacion,
|
||||
id_usuario=message.id_usuario,
|
||||
tipo_notificacion=message.tipo_notificacion,
|
||||
titulo=message.titulo,
|
||||
mensaje=message.mensaje,
|
||||
id_reporte=message.id_reporte,
|
||||
estado_reporte=message.estado_reporte,
|
||||
leida=False,
|
||||
fecha_creacion=fecha_creacion
|
||||
)
|
||||
|
||||
# Save to MongoDB repository
|
||||
saved_notification = self.repo.save(notification)
|
||||
logger.info(f"Report status change notification created: {message.id_notificacion}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing report status change: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def start(self):
|
||||
"""Start consuming messages from RabbitMQ"""
|
||||
try:
|
||||
logger.info("Starting Notification Consumer...")
|
||||
self.consumer.start()
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting notification consumer: {e}", exc_info=True)
|
||||
raise
|
||||
@@ -20,6 +20,16 @@ class Settings(BaseSettings):
|
||||
default="voxpopuli_reports",
|
||||
description="Base de datos MongoDB"
|
||||
)
|
||||
|
||||
# Base de datos MongoDB para Notificaciones
|
||||
mongodb_notifications_url: str = Field(
|
||||
default=os.getenv("MONGODB_NOTIFICATIONS_URL", "mongodb://localhost:27018"),
|
||||
description="URL de conexión a MongoDB para API de Notificaciones"
|
||||
)
|
||||
mongodb_notifications_db: str = Field(
|
||||
default="voxpopuli_notifications",
|
||||
description="Base de datos MongoDB para notificaciones"
|
||||
)
|
||||
|
||||
rabbitmq: str = Field (
|
||||
default=os.getenv("RABBITMQ_URI", "localhost")
|
||||
|
||||
34
src/domain/notifications.py
Normal file
34
src/domain/notifications.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Notification domain model"""
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Notification:
|
||||
"""Domain model for notifications"""
|
||||
id_notificacion: str
|
||||
id_usuario: int # Usuario que recibe la notificación
|
||||
tipo_notificacion: str # "report_status_change", etc.
|
||||
titulo: str
|
||||
mensaje: str
|
||||
id_reporte: Optional[str] = None
|
||||
estado_reporte: Optional[str] = None # "en proceso", "resuelto", "no resuelto"
|
||||
leida: bool = False
|
||||
fecha_creacion: Optional[datetime] = None
|
||||
fecha_lectura: Optional[datetime] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validations after initialization"""
|
||||
if not self.id_notificacion:
|
||||
raise ValueError("id_notificacion is required")
|
||||
if not self.id_usuario or self.id_usuario <= 0:
|
||||
raise ValueError("id_usuario must be a positive integer")
|
||||
if not self.tipo_notificacion:
|
||||
raise ValueError("tipo_notificacion is required")
|
||||
if not self.titulo:
|
||||
raise ValueError("titulo is required")
|
||||
if not self.mensaje:
|
||||
raise ValueError("mensaje is required")
|
||||
if self.fecha_creacion is None:
|
||||
self.fecha_creacion = datetime.now()
|
||||
@@ -6,6 +6,14 @@ from core.config import ConfSettings
|
||||
mongo_client = MongoClient(ConfSettings.mongodb_url)
|
||||
mongodb = mongo_client[ConfSettings.mongodb_db]
|
||||
|
||||
# Conexión a MongoDB para Notificaciones
|
||||
mongo_client_notifications = MongoClient(ConfSettings.mongodb_notifications_url)
|
||||
mongodb_notifications = mongo_client_notifications[ConfSettings.mongodb_notifications_db]
|
||||
|
||||
def get_reports_collection() -> Collection:
|
||||
"""Obtiene la colección de reportes desde MongoDB"""
|
||||
return mongodb["reportes"]
|
||||
|
||||
def get_notifications_collection() -> Collection:
|
||||
"""Obtiene la colección de notificaciones desde MongoDB"""
|
||||
return mongodb_notifications["notificaciones"]
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Notification Repository Implementation using MongoDB"""
|
||||
from application.ports.notification_repository import NotificationRepository
|
||||
from domain.notifications import Notification
|
||||
from infrastructure.adapters.persistence.mongodb import get_notifications_collection
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
|
||||
class NotificationRepositoryMongo(NotificationRepository):
|
||||
"""Implementación del repositorio de Notificaciones usando MongoDB"""
|
||||
|
||||
def __init__(self):
|
||||
self.collection = get_notifications_collection()
|
||||
|
||||
def save(self, notification: Notification) -> Notification:
|
||||
"""Guarda una nueva notificación"""
|
||||
notification_dict = {
|
||||
"id_notificacion": notification.id_notificacion,
|
||||
"id_usuario": notification.id_usuario,
|
||||
"tipo_notificacion": notification.tipo_notificacion,
|
||||
"titulo": notification.titulo,
|
||||
"mensaje": notification.mensaje,
|
||||
"id_reporte": notification.id_reporte,
|
||||
"estado_reporte": notification.estado_reporte,
|
||||
"leida": notification.leida,
|
||||
"fecha_creacion": notification.fecha_creacion or datetime.utcnow(),
|
||||
"fecha_lectura": notification.fecha_lectura
|
||||
}
|
||||
result = self.collection.insert_one(notification_dict)
|
||||
return notification
|
||||
|
||||
def find_by_id(self, notification_id: str) -> Optional[Notification]:
|
||||
"""Obtiene una notificación por ID"""
|
||||
doc = self.collection.find_one({"id_notificacion": notification_id})
|
||||
if doc:
|
||||
return self._to_domain(doc)
|
||||
return None
|
||||
|
||||
def find_by_user_id(self, user_id: int) -> List[Notification]:
|
||||
"""Obtiene todas las notificaciones de un usuario, ordenadas por fecha descendente"""
|
||||
docs = self.collection.find({"id_usuario": user_id}).sort("fecha_creacion", -1)
|
||||
return [self._to_domain(doc) for doc in docs]
|
||||
|
||||
def find_unread_by_user_id(self, user_id: int) -> List[Notification]:
|
||||
"""Obtiene todas las notificaciones no leídas de un usuario"""
|
||||
docs = self.collection.find({
|
||||
"id_usuario": user_id,
|
||||
"leida": False
|
||||
}).sort("fecha_creacion", -1)
|
||||
return [self._to_domain(doc) for doc in docs]
|
||||
|
||||
def find_all(self) -> List[Notification]:
|
||||
"""Obtiene todas las notificaciones"""
|
||||
docs = self.collection.find().sort("fecha_creacion", -1)
|
||||
return [self._to_domain(doc) for doc in docs]
|
||||
|
||||
def mark_as_read(self, notification_id: str) -> bool:
|
||||
"""Marca una notificación como leída"""
|
||||
result = self.collection.update_one(
|
||||
{"id_notificacion": notification_id},
|
||||
{"$set": {
|
||||
"leida": True,
|
||||
"fecha_lectura": datetime.utcnow()
|
||||
}}
|
||||
)
|
||||
return result.modified_count > 0
|
||||
|
||||
def mark_all_as_read(self, user_id: int) -> bool:
|
||||
"""Marca todas las notificaciones de un usuario como leídas"""
|
||||
result = self.collection.update_many(
|
||||
{"id_usuario": user_id, "leida": False},
|
||||
{"$set": {
|
||||
"leida": True,
|
||||
"fecha_lectura": datetime.utcnow()
|
||||
}}
|
||||
)
|
||||
return result.modified_count > 0
|
||||
|
||||
def delete(self, notification_id: str) -> bool:
|
||||
"""Elimina una notificación"""
|
||||
result = self.collection.delete_one({"id_notificacion": notification_id})
|
||||
return result.deleted_count > 0
|
||||
|
||||
def delete_all_by_user(self, user_id: int) -> bool:
|
||||
"""Elimina todas las notificaciones de un usuario"""
|
||||
result = self.collection.delete_many({"id_usuario": user_id})
|
||||
return result.deleted_count > 0
|
||||
|
||||
def _to_domain(self, doc: dict) -> Notification:
|
||||
"""Convierte un documento de MongoDB a un objeto de dominio"""
|
||||
return Notification(
|
||||
id_notificacion=doc.get("id_notificacion"),
|
||||
id_usuario=doc.get("id_usuario"),
|
||||
tipo_notificacion=doc.get("tipo_notificacion"),
|
||||
titulo=doc.get("titulo"),
|
||||
mensaje=doc.get("mensaje"),
|
||||
id_reporte=doc.get("id_reporte"),
|
||||
estado_reporte=doc.get("estado_reporte"),
|
||||
leida=doc.get("leida", False),
|
||||
fecha_creacion=doc.get("fecha_creacion"),
|
||||
fecha_lectura=doc.get("fecha_lectura")
|
||||
)
|
||||
@@ -18,6 +18,13 @@ class ReportEventType(str, Enum):
|
||||
CREATE = "report.create"
|
||||
UPDATE_VISIBILITY = "report.update_visibility"
|
||||
DELETE = "report.delete"
|
||||
UPDATE_STATUS = "report.update_status"
|
||||
|
||||
|
||||
class NotificationEventType(str, Enum):
|
||||
"""Types of notification events"""
|
||||
CREATE = "notification.create"
|
||||
REPORT_STATUS_CHANGE = "notification.report_status_change"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -85,3 +92,33 @@ class ReportMessage:
|
||||
"""Create from dictionary"""
|
||||
data['event_type'] = ReportEventType(data['event_type'])
|
||||
return ReportMessage(**data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotificationMessage:
|
||||
"""Message for notification events"""
|
||||
event_type: NotificationEventType
|
||||
id_notificacion: Optional[str] = None
|
||||
id_usuario: Optional[int] = None
|
||||
tipo_notificacion: Optional[str] = None
|
||||
titulo: Optional[str] = None
|
||||
mensaje: Optional[str] = None
|
||||
id_reporte: Optional[str] = None
|
||||
estado_reporte: Optional[str] = None # Estado del reporte que cambió
|
||||
fecha_creacion: Optional[str] = None # ISO format datetime string
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary"""
|
||||
data = asdict(self)
|
||||
data['event_type'] = self.event_type.value
|
||||
return data
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert to JSON string"""
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> 'NotificationMessage':
|
||||
"""Create from dictionary"""
|
||||
data['event_type'] = NotificationEventType(data['event_type'])
|
||||
return NotificationMessage(**data)
|
||||
|
||||
0
src/infrastructure/api/notifications/__init__.py
Normal file
0
src/infrastructure/api/notifications/__init__.py
Normal file
16
src/infrastructure/api/notifications/app.py
Normal file
16
src/infrastructure/api/notifications/app.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Notifications API Application Factory"""
|
||||
from fastapi import FastAPI
|
||||
from core.config import ConfSettings
|
||||
from infrastructure.api.notifications.router import router
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""Factory para crear la aplicación de Notificaciones"""
|
||||
app = FastAPI(
|
||||
title="Notificaciones Microservice",
|
||||
version="1.0.0",
|
||||
description="Microservicio de gestión de notificaciones"
|
||||
)
|
||||
app.include_router(router)
|
||||
|
||||
return app
|
||||
176
src/infrastructure/api/notifications/notifications.py
Normal file
176
src/infrastructure/api/notifications/notifications.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""Notifications API endpoints"""
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from infrastructure.api.notifications.schemas import NotificationResponse, NotificationListResponse, MarkNotificationAsReadRequest
|
||||
from application.services.notification_services import (
|
||||
GetUserNotifications, GetUnreadUserNotifications, MarkNotificationAsRead,
|
||||
MarkAllUserNotificationsAsRead, DeleteNotification, DeleteAllUserNotifications
|
||||
)
|
||||
from infrastructure.adapters.persistence.notification_repository_mongo import NotificationRepositoryMongo
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
notification_repo = NotificationRepositoryMongo()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _notification_to_response(notification) -> dict:
|
||||
"""Convierte un objeto Notification a dict de respuesta"""
|
||||
return {
|
||||
"id_notificacion": notification.id_notificacion,
|
||||
"id_usuario": notification.id_usuario,
|
||||
"tipo_notificacion": notification.tipo_notificacion,
|
||||
"titulo": notification.titulo,
|
||||
"mensaje": notification.mensaje,
|
||||
"id_reporte": notification.id_reporte,
|
||||
"estado_reporte": notification.estado_reporte,
|
||||
"leida": notification.leida,
|
||||
"fecha_creacion": notification.fecha_creacion,
|
||||
"fecha_lectura": notification.fecha_lectura
|
||||
}
|
||||
|
||||
|
||||
@router.get("/user/{user_id}")
|
||||
async def get_user_notifications(user_id: int):
|
||||
"""Obtiene todas las notificaciones de un usuario"""
|
||||
try:
|
||||
get_use_case = GetUserNotifications(notification_repo)
|
||||
notifications = get_use_case.execute(user_id)
|
||||
unread_use_case = GetUnreadUserNotifications(notification_repo)
|
||||
unread = unread_use_case.execute(user_id)
|
||||
|
||||
return NotificationListResponse(
|
||||
total=len(notifications),
|
||||
unread=len(unread),
|
||||
notifications=[_notification_to_response(n) for n in notifications]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error al obtener notificaciones del usuario {user_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error interno del servidor"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/user/{user_id}/unread")
|
||||
async def get_unread_user_notifications(user_id: int):
|
||||
"""Obtiene todas las notificaciones no leídas de un usuario"""
|
||||
try:
|
||||
get_use_case = GetUnreadUserNotifications(notification_repo)
|
||||
notifications = get_use_case.execute(user_id)
|
||||
|
||||
return NotificationListResponse(
|
||||
total=len(notifications),
|
||||
unread=len(notifications),
|
||||
notifications=[_notification_to_response(n) for n in notifications]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error al obtener notificaciones no leídas del usuario {user_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error interno del servidor"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{notification_id}")
|
||||
async def get_notification(notification_id: str):
|
||||
"""Obtiene una notificación por ID"""
|
||||
try:
|
||||
notification = notification_repo.find_by_id(notification_id)
|
||||
if not notification:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Notificación con ID {notification_id} no encontrada"
|
||||
)
|
||||
return _notification_to_response(notification)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al obtener notificación {notification_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error interno del servidor"
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{notification_id}/read", status_code=status.HTTP_200_OK)
|
||||
async def mark_notification_as_read(notification_id: str):
|
||||
"""Marca una notificación como leída"""
|
||||
try:
|
||||
mark_use_case = MarkNotificationAsRead(notification_repo)
|
||||
result = mark_use_case.execute(notification_id)
|
||||
|
||||
if result["status"] == "error":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=result["message"]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al marcar notificación como leída {notification_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error interno del servidor"
|
||||
)
|
||||
|
||||
|
||||
@router.put("/user/{user_id}/read-all", status_code=status.HTTP_200_OK)
|
||||
async def mark_all_user_notifications_as_read(user_id: int):
|
||||
"""Marca todas las notificaciones de un usuario como leídas"""
|
||||
try:
|
||||
mark_use_case = MarkAllUserNotificationsAsRead(notification_repo)
|
||||
result = mark_use_case.execute(user_id)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error al marcar todas las notificaciones como leídas para el usuario {user_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error interno del servidor"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{notification_id}", status_code=status.HTTP_200_OK)
|
||||
async def delete_notification(notification_id: str):
|
||||
"""Elimina una notificación"""
|
||||
try:
|
||||
delete_use_case = DeleteNotification(notification_repo)
|
||||
result = delete_use_case.execute(notification_id)
|
||||
|
||||
if result["status"] == "error":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=result["message"]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al eliminar notificación {notification_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error interno del servidor"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/user/{user_id}/delete-all", status_code=status.HTTP_200_OK)
|
||||
async def delete_all_user_notifications(user_id: int):
|
||||
"""Elimina todas las notificaciones de un usuario"""
|
||||
try:
|
||||
delete_use_case = DeleteAllUserNotifications(notification_repo)
|
||||
result = delete_use_case.execute(user_id)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error al eliminar todas las notificaciones del usuario {user_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error interno del servidor"
|
||||
)
|
||||
14
src/infrastructure/api/notifications/root.py
Normal file
14
src/infrastructure/api/notifications/root.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Notifications API Root endpoint"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def root():
|
||||
"""Root endpoint for Notifications API"""
|
||||
return {
|
||||
"message": "Notificaciones API",
|
||||
"version": "1.0.0",
|
||||
"status": "running"
|
||||
}
|
||||
18
src/infrastructure/api/notifications/router.py
Normal file
18
src/infrastructure/api/notifications/router.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Notifications API Router"""
|
||||
from fastapi import APIRouter
|
||||
from infrastructure.api.notifications.notifications import router as notifications_router
|
||||
from infrastructure.api.notifications.root import router as root_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(
|
||||
notifications_router,
|
||||
prefix="/notifications",
|
||||
tags=["notifications"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
root_router,
|
||||
prefix='',
|
||||
tags=["root"]
|
||||
)
|
||||
33
src/infrastructure/api/notifications/schemas.py
Normal file
33
src/infrastructure/api/notifications/schemas.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Notification API Schemas"""
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class NotificationResponse(BaseModel):
|
||||
"""Schema for notification response"""
|
||||
id_notificacion: str
|
||||
id_usuario: int
|
||||
tipo_notificacion: str
|
||||
titulo: str
|
||||
mensaje: str
|
||||
id_reporte: Optional[str] = None
|
||||
estado_reporte: Optional[str] = None
|
||||
leida: bool
|
||||
fecha_creacion: datetime
|
||||
fecha_lectura: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NotificationListResponse(BaseModel):
|
||||
"""Schema for notification list response"""
|
||||
total: int
|
||||
unread: int
|
||||
notifications: list[NotificationResponse]
|
||||
|
||||
|
||||
class MarkNotificationAsReadRequest(BaseModel):
|
||||
"""Schema for marking notification as read"""
|
||||
pass
|
||||
@@ -7,8 +7,12 @@ from application.services.report_services import (
|
||||
from infrastructure.adapters.persistence.report_repository_mongo import ReportRepositoryMongo
|
||||
from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL
|
||||
from infrastructure.adapters.file_storage import image_storage
|
||||
from infrastructure.adapters.rabbitmq.sender import send_to_queue
|
||||
from infrastructure.adapters.rabbitmq.messages import NotificationMessage, NotificationEventType
|
||||
import logging
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
router = APIRouter()
|
||||
report_repo = ReportRepositoryMongo()
|
||||
@@ -231,8 +235,17 @@ async def delete_report(report_id: str):
|
||||
|
||||
@router.put("/{report_id}/status", status_code=status.HTTP_200_OK)
|
||||
async def update_report_status(report_id: str, status_data: ReportUpdateStatusRequest):
|
||||
"""Actualiza el estado de un reporte"""
|
||||
"""Actualiza el estado de un reporte y envía notificación al usuario"""
|
||||
try:
|
||||
# Obtener el reporte actual para saber el usuario creador
|
||||
report = report_repo.find_by_id(report_id)
|
||||
if not report:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Reporte con ID {report_id} no encontrado"
|
||||
)
|
||||
|
||||
# Actualizar el estado
|
||||
update_use_case = UpdateReportStatus(report_repo)
|
||||
result = update_use_case.execute(
|
||||
report_id=report_id,
|
||||
@@ -254,6 +267,30 @@ async def update_report_status(report_id: str, status_data: ReportUpdateStatusRe
|
||||
detail=message
|
||||
)
|
||||
|
||||
# Enviar notificación al usuario creador del reporte
|
||||
try:
|
||||
notification_message = NotificationMessage(
|
||||
event_type=NotificationEventType.REPORT_STATUS_CHANGE,
|
||||
id_notificacion=str(uuid.uuid4()),
|
||||
id_usuario=report.id_usuario,
|
||||
tipo_notificacion="report_status_change",
|
||||
titulo="Tu reporte ha cambiado de estado",
|
||||
mensaje=f"El estado de tu reporte ha sido actualizado a: {status_data.estado}",
|
||||
id_reporte=report_id,
|
||||
estado_reporte=status_data.estado,
|
||||
fecha_creacion=datetime.utcnow().isoformat()
|
||||
)
|
||||
|
||||
# Enviar a la cola de notificaciones
|
||||
send_to_queue(
|
||||
queue_name='notifications_queue',
|
||||
message=notification_message.to_dict()
|
||||
)
|
||||
logger.info(f"Notification sent to user {report.id_usuario} for report {report_id}")
|
||||
except Exception as notification_error:
|
||||
logger.warning(f"Error sending notification for report {report_id}: {notification_error}")
|
||||
# No fallar la actualización si hay error en notificación
|
||||
|
||||
# 200 OK: estado actualizado correctamente
|
||||
return result
|
||||
|
||||
|
||||
@@ -4,13 +4,9 @@ from passlib.context import CryptContext
|
||||
from typing import Optional, Dict
|
||||
from core.config import ConfSettings
|
||||
|
||||
# Configurar contexto para hashing de contraseñas
|
||||
# Soporta argon2 (nuevo) y bcrypt (antiguo) para compatibilidad hacia atrás
|
||||
pwd_context = CryptContext(
|
||||
schemes=["argon2"],
|
||||
deprecated=["bcrypt"],
|
||||
argon2__rounds=3
|
||||
)
|
||||
# Configurar contexto para hashing de contraseñas con argon2
|
||||
# argon2 es más moderno y más seguro que bcrypt
|
||||
pwd_context = CryptContext(schemes=["argon2"])
|
||||
|
||||
|
||||
class AuthService:
|
||||
|
||||
28
src/main.py
28
src/main.py
@@ -1,11 +1,13 @@
|
||||
"""
|
||||
Punto de entrada principal para VoxPopuli Microservices
|
||||
Ejecuta dos APIs en paralelo: Usuarios (puerto 8000) y Reportes (puerto 8001)
|
||||
Ejecuta dos APIs en paralelo: Usuarios (puerto 8000) y Reportes (puerto 8001), Notificaciones (puerto 8002)
|
||||
"""
|
||||
from infrastructure.api.users.app import create_app as create_users_app
|
||||
from infrastructure.api.reports.app import create_app as create_reports_app
|
||||
from infrastructure.api.notifications.app import create_app as create_notifications_app
|
||||
from consumers.report_consumer import ReportConsumer
|
||||
from consumers.user_consumer import UserConsumer
|
||||
from consumers.notification_consumer import NotificationConsumer
|
||||
from core.config import ConfSettings
|
||||
import threading
|
||||
import uvicorn
|
||||
@@ -32,6 +34,17 @@ def run_reports_api():
|
||||
log_level=ConfSettings.log_level,
|
||||
)
|
||||
|
||||
def run_notifications_api():
|
||||
"""Ejecuta la API de Notificaciones en puerto 8002"""
|
||||
app_notifications = create_notifications_app()
|
||||
uvicorn.run(
|
||||
app_notifications,
|
||||
host=ConfSettings.host,
|
||||
port=8002,
|
||||
reload=False,
|
||||
log_level=ConfSettings.log_level,
|
||||
)
|
||||
|
||||
def run_user_consumer():
|
||||
consumer = UserConsumer()
|
||||
consumer.start()
|
||||
@@ -40,34 +53,45 @@ def run_reports_consumer():
|
||||
consumer = ReportConsumer()
|
||||
consumer.start()
|
||||
|
||||
def run_notifications_consumer():
|
||||
consumer = NotificationConsumer()
|
||||
consumer.start()
|
||||
|
||||
|
||||
def run():
|
||||
"""Inicia ambas APIs en threads separados"""
|
||||
"""Inicia todas las APIs en threads separados"""
|
||||
print("=" * 60)
|
||||
print("Iniciando VoxPopuli Microservices...")
|
||||
print("=" * 60)
|
||||
|
||||
users_thread = threading.Thread(target=run_users_api, daemon=True, name="Users-API")
|
||||
reports_thread = threading.Thread(target=run_reports_api, daemon=True, name="Reports-API")
|
||||
notifications_thread = threading.Thread(target=run_notifications_api, daemon=True, name="Notifications-API")
|
||||
user_consumer_thread = threading.Thread(target=run_user_consumer, daemon=True, name="Users-Consumer")
|
||||
report_consumer_thread = threading.Thread(target=run_reports_consumer, daemon=True, name="Reports-Consumer")
|
||||
notifications_consumer_thread = threading.Thread(target=run_notifications_consumer, daemon=True, name="Notifications-Consumer")
|
||||
|
||||
|
||||
users_thread.start()
|
||||
reports_thread.start()
|
||||
notifications_thread.start()
|
||||
user_consumer_thread.start()
|
||||
report_consumer_thread.start()
|
||||
notifications_consumer_thread.start()
|
||||
|
||||
print("\n✓ API de Usuarios ejecutándose en http://0.0.0.0:8000")
|
||||
print("✓ API de Reportes ejecutándose en http://0.0.0.0:8001")
|
||||
print("✓ API de Notificaciones ejecutándose en http://0.0.0.0:8002")
|
||||
print("\nDocumentación disponible en:")
|
||||
print(" - Usuarios: http://localhost:8000/docs")
|
||||
print(" - Reportes: http://localhost:8001/docs")
|
||||
print(" - Notificaciones: http://localhost:8002/docs")
|
||||
print("\n" + "=" * 60 + "\n")
|
||||
|
||||
try:
|
||||
users_thread.join()
|
||||
reports_thread.join()
|
||||
notifications_thread.join()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nRecibiendo señal de salida...")
|
||||
print("Cerrando APIs...")
|
||||
|
||||
Reference in New Issue
Block a user