Added Notifications Microservice API - Integrated notification system with MongoDB, RabbitMQ consumer, and automatic status change notifications for reports

This commit is contained in:
2026-05-03 21:51:41 -06:00
parent c72397c228
commit 325517a130
19 changed files with 1280 additions and 81 deletions

View 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

View 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)}"}

View 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

View File

@@ -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")

View 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()

View File

@@ -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"]

View File

@@ -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")
)

View File

@@ -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)

View 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

View 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"
)

View 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"
}

View 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"]
)

View 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

View File

@@ -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

View File

@@ -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:

View File

@@ -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...")