100
src/application/ports/notification_repository.py
Normal file
100
src/application/ports/notification_repository.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional
|
||||
from domain.notifications import Notification
|
||||
|
||||
|
||||
class NotificationRepository(ABC):
|
||||
"""Puerto/Interfaz para el repositorio de notificaciones"""
|
||||
|
||||
@abstractmethod
|
||||
def create(self, notification: Notification) -> str:
|
||||
"""
|
||||
Crea una nueva notificación
|
||||
|
||||
Args:
|
||||
notification: Objeto Notification a crear
|
||||
|
||||
Returns:
|
||||
ID de la notificación creada
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_by_id(self, notification_id: str) -> Optional[Notification]:
|
||||
"""
|
||||
Obtiene una notificación por ID
|
||||
|
||||
Args:
|
||||
notification_id: ID de la notificación
|
||||
|
||||
Returns:
|
||||
Objeto Notification o None si no existe
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_by_user(self, user_id: int, limit: int = 50, offset: int = 0) -> List[Notification]:
|
||||
"""
|
||||
Obtiene notificaciones de un usuario
|
||||
|
||||
Args:
|
||||
user_id: ID del usuario
|
||||
limit: Número máximo de notificaciones
|
||||
offset: Desplazamiento de registros
|
||||
|
||||
Returns:
|
||||
Lista de notificaciones del usuario
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def mark_as_read(self, notification_id: str) -> bool:
|
||||
"""
|
||||
Marca una notificación como leída
|
||||
|
||||
Args:
|
||||
notification_id: ID de la notificación
|
||||
|
||||
Returns:
|
||||
True si se actualizó exitosamente
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def mark_all_as_read(self, user_id: int) -> int:
|
||||
"""
|
||||
Marca todas las notificaciones de un usuario como leídas
|
||||
|
||||
Args:
|
||||
user_id: ID del usuario
|
||||
|
||||
Returns:
|
||||
Número de notificaciones actualizadas
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, notification_id: str) -> bool:
|
||||
"""
|
||||
Elimina una notificación
|
||||
|
||||
Args:
|
||||
notification_id: ID de la notificación
|
||||
|
||||
Returns:
|
||||
True si se eliminó exitosamente
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_unread_count(self, user_id: int) -> int:
|
||||
"""
|
||||
Obtiene el número de notificaciones no leídas para un usuario
|
||||
|
||||
Args:
|
||||
user_id: ID del usuario
|
||||
|
||||
Returns:
|
||||
Número de notificaciones no leídas
|
||||
"""
|
||||
pass
|
||||
99
src/application/services/notification_services.py
Normal file
99
src/application/services/notification_services.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from typing import List, Optional
|
||||
from domain.notifications import Notification
|
||||
from infrastructure.adapters.persistence.notification_repository_mongo import NotificationRepositoryMongo
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class NotificationService:
|
||||
"""Servicio de negocio para notificaciones"""
|
||||
|
||||
def __init__(self):
|
||||
self.repository = NotificationRepositoryMongo()
|
||||
|
||||
def create_notification(
|
||||
self,
|
||||
id_usuario: int,
|
||||
id_reporte: str,
|
||||
message: str
|
||||
) -> str:
|
||||
"""
|
||||
Crea una nueva notificación
|
||||
|
||||
Args:
|
||||
id_usuario: ID del usuario
|
||||
id_reporte: ID del reporte relacionado
|
||||
message: Mensaje de la notificación
|
||||
|
||||
Returns:
|
||||
ID de la notificación creada
|
||||
"""
|
||||
notification = Notification(
|
||||
id_usuario=id_usuario,
|
||||
id_reporte=id_reporte,
|
||||
message=message,
|
||||
fecha=datetime.utcnow(),
|
||||
read=False
|
||||
)
|
||||
return self.repository.create(notification)
|
||||
|
||||
def get_notification(self, notification_id: str) -> Optional[Notification]:
|
||||
"""Obtiene una notificación por ID"""
|
||||
return self.repository.get_by_id(notification_id)
|
||||
|
||||
def get_user_notifications(
|
||||
self,
|
||||
user_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> List[Notification]:
|
||||
"""Obtiene notificaciones de un usuario"""
|
||||
return self.repository.get_by_user(user_id, limit, offset)
|
||||
|
||||
def mark_as_read(self, notification_id: str) -> bool:
|
||||
"""Marca una notificación como leída"""
|
||||
return self.repository.mark_as_read(notification_id)
|
||||
|
||||
def mark_all_as_read(self, user_id: int) -> int:
|
||||
"""Marca todas las notificaciones de un usuario como leídas"""
|
||||
return self.repository.mark_all_as_read(user_id)
|
||||
|
||||
def delete_notification(self, notification_id: str) -> bool:
|
||||
"""Elimina una notificación"""
|
||||
return self.repository.delete(notification_id)
|
||||
|
||||
def get_unread_count(self, user_id: int) -> int:
|
||||
"""Obtiene el número de notificaciones no leídas"""
|
||||
return self.repository.get_unread_count(user_id)
|
||||
|
||||
def send_report_status_notification(
|
||||
self,
|
||||
id_usuario: int,
|
||||
id_reporte: str,
|
||||
old_status: str,
|
||||
new_status: str
|
||||
) -> str:
|
||||
"""
|
||||
Envía una notificación cuando cambia el estado de un reporte
|
||||
|
||||
Args:
|
||||
id_usuario: ID del usuario propietario del reporte
|
||||
id_reporte: ID del reporte
|
||||
old_status: Estado anterior
|
||||
new_status: Nuevo estado
|
||||
|
||||
Returns:
|
||||
ID de la notificación creada
|
||||
"""
|
||||
status_messages = {
|
||||
("en proceso", "resuelto"): f"¡Tu reporte #{id_reporte} ha sido resuelto!",
|
||||
("en proceso", "no resuelto"): f"Tu reporte #{id_reporte} fue marcado como no resuelto.",
|
||||
("no resuelto", "resuelto"): f"¡Tu reporte #{id_reporte} ha sido resuelto!",
|
||||
("resuelto", "en proceso"): f"Tu reporte #{id_reporte} ha sido reabierto.",
|
||||
}
|
||||
|
||||
message = status_messages.get(
|
||||
(old_status, new_status),
|
||||
f"El estado de tu reporte #{id_reporte} ha cambiado a {new_status}"
|
||||
)
|
||||
|
||||
return self.create_notification(id_usuario, id_reporte, message)
|
||||
@@ -256,7 +256,7 @@ class DeleteReport:
|
||||
}
|
||||
|
||||
class UpdateReportStatus:
|
||||
"""Use case para actualizar el estado de un reporte"""
|
||||
"""Use case para actualizar el estado de un reporte - envía evento a RabbitMQ"""
|
||||
def __init__(self, repo: ReportRepository):
|
||||
if not isinstance(repo, ReportRepository):
|
||||
raise TypeError("repo must implement ReportRepository")
|
||||
@@ -264,7 +264,7 @@ class UpdateReportStatus:
|
||||
|
||||
def execute(self, report_id: str, new_estado: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Actualiza el estado de un reporte.
|
||||
Actualiza el estado de un reporte y envía notificación vía RabbitMQ.
|
||||
Valida previamente:
|
||||
- Reporte existe
|
||||
- Estado es válido
|
||||
@@ -294,9 +294,32 @@ class UpdateReportStatus:
|
||||
"message": f"Error al buscar reporte: {str(e)}"
|
||||
}
|
||||
|
||||
# Guardar estado anterior para notificación
|
||||
old_estado = report.estado
|
||||
|
||||
# Actualizar estado
|
||||
try:
|
||||
self.repo.update_estado(report_id, new_estado)
|
||||
|
||||
# Enviar evento a RabbitMQ solo si el estado cambió
|
||||
if old_estado != new_estado:
|
||||
message = ReportMessage(
|
||||
event_type=ReportEventType.UPDATE_STATUS,
|
||||
id_reporte=report_id,
|
||||
id_usuario=report.id_usuario,
|
||||
old_estado=old_estado,
|
||||
new_estado=new_estado,
|
||||
estado=new_estado,
|
||||
tipo_reporte=report.tipo_reporte,
|
||||
descripcion=report.descripcion,
|
||||
ubicacion=report.ubicacion,
|
||||
lat=report.lat,
|
||||
lng=report.lng,
|
||||
visibilidad=report.visibilidad,
|
||||
fecha_creacion=report.fecha_creacion.isoformat() if report.fecha_creacion else None
|
||||
)
|
||||
send_to_queue("notifications_queue", message.to_dict())
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Estado del reporte actualizado a '{new_estado}'",
|
||||
|
||||
98
src/consumers/notification_consumer.py
Normal file
98
src/consumers/notification_consumer.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Notifications RabbitMQ Consumer - Processes report status change 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 ReportMessage, ReportEventType
|
||||
from application.services.notification_services import NotificationService
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('logs/notifications_consumer.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NotificationConsumer:
|
||||
"""Consumer para eventos de cambio de estado de reportes desde RabbitMQ"""
|
||||
|
||||
def __init__(self):
|
||||
self.notification_service = NotificationService()
|
||||
self.consumer = RabbitMQConsumer(queue_name='notifications_queue')
|
||||
self.consumer.set_callback(self.process_message)
|
||||
|
||||
def start(self):
|
||||
"""Inicia el consumidor"""
|
||||
logger.info("Notifications Consumer started")
|
||||
self.consumer.start_consuming()
|
||||
|
||||
def process_message(self, message_dict: dict):
|
||||
"""
|
||||
Procesa un evento de cambio de estado de reporte desde RabbitMQ
|
||||
|
||||
Args:
|
||||
message_dict: Diccionario con los datos del mensaje
|
||||
"""
|
||||
try:
|
||||
# Reconstruir el objeto ReportMessage
|
||||
message = ReportMessage.from_dict(message_dict)
|
||||
|
||||
# Solo procesar eventos de actualización de estado
|
||||
if message.event_type == ReportEventType.UPDATE_STATUS:
|
||||
self._handle_status_update(message)
|
||||
elif message.event_type == ReportEventType.UPDATE_VISIBILITY:
|
||||
self._handle_visibility_update(message)
|
||||
else:
|
||||
logger.debug(f"Ignoring event type: {message.event_type}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing notification message: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _handle_status_update(self, message: ReportMessage):
|
||||
"""Maneja la actualización de estado de un reporte"""
|
||||
try:
|
||||
logger.info(
|
||||
f"Creating notification for report {message.id_reporte} "
|
||||
f"status change from {message.old_estado} to {message.new_estado}"
|
||||
)
|
||||
|
||||
# Crear notificación para el usuario propietario del reporte
|
||||
self.notification_service.send_report_status_notification(
|
||||
id_usuario=message.id_usuario,
|
||||
id_reporte=message.id_reporte,
|
||||
old_status=message.old_estado,
|
||||
new_status=message.new_estado
|
||||
)
|
||||
|
||||
logger.info(f"Notification created for user {message.id_usuario}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating status notification: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _handle_visibility_update(self, message: ReportMessage):
|
||||
"""Maneja la actualización de visibilidad de un reporte"""
|
||||
try:
|
||||
# Notificar si la visibilidad cambió significativamente
|
||||
if hasattr(message, 'old_visibility') and hasattr(message, 'new_visibility'):
|
||||
logger.info(
|
||||
f"Report {message.id_reporte} visibility changed "
|
||||
f"from {message.old_visibility} to {message.new_visibility}"
|
||||
)
|
||||
|
||||
# Puedes agregar lógica para notificar sobre cambios de visibilidad
|
||||
# Por ahora solo registramos el evento
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling visibility update: {e}", exc_info=True)
|
||||
@@ -11,19 +11,29 @@ class Settings(BaseSettings):
|
||||
description="URL de conexión a MySQL para API de Usuarios"
|
||||
)
|
||||
|
||||
# Base de datos MongoDB
|
||||
mongodb_url: str = Field(
|
||||
default=os.getenv("MONGODB_URL", "mongodb://localhost:27017"),
|
||||
description="URL de conexión a MongoDB para API de Reportes"
|
||||
# Base de datos MongoDB - Reportes (Instancia Separada)
|
||||
mongodb_reports_url: str = Field(
|
||||
default=os.getenv("MONGODB_REPORTS_URL", "mongodb://admin:admin_password@localhost:27017"),
|
||||
description="URL de conexión a MongoDB para API de Reportes (Instancia 1)"
|
||||
)
|
||||
mongodb_db: str = Field(
|
||||
mongodb_reports_db: str = Field(
|
||||
default="voxpopuli_reports",
|
||||
description="Base de datos MongoDB"
|
||||
description="Base de datos MongoDB para Reportes"
|
||||
)
|
||||
|
||||
rabbitmq: str = Field (
|
||||
default=os.getenv("RABBITMQ_URI", "localhost")
|
||||
# Base de datos MongoDB - Notificaciones (Instancia Separada)
|
||||
mongodb_notifications_url: str = Field(
|
||||
default=os.getenv("MONGODB_NOTIFICATIONS_URL", "mongodb://admin:admin_password@localhost:27018"),
|
||||
description="URL de conexión a MongoDB para Notificaciones (Instancia 2)"
|
||||
)
|
||||
mongodb_notifications_db: str = Field(
|
||||
default="voxpopuli_notifications",
|
||||
description="Base de datos MongoDB para Notificaciones"
|
||||
)
|
||||
|
||||
rabbitmq: str = Field(
|
||||
default=os.getenv("RABBITMQ_URI", "localhost"),
|
||||
description="URL de conexión a RabbitMQ"
|
||||
)
|
||||
|
||||
# JWT Configuration
|
||||
|
||||
14
src/domain/notifications.py
Normal file
14
src/domain/notifications.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Notification:
|
||||
"""Modelo de dominio para Notificación"""
|
||||
id_notificacion: Optional[str] = None
|
||||
id_usuario: int = None
|
||||
id_reporte: str = None
|
||||
message: str = None
|
||||
fecha: Optional[datetime] = None
|
||||
read: bool = False
|
||||
@@ -2,10 +2,18 @@ from pymongo import MongoClient
|
||||
from pymongo.collection import Collection
|
||||
from core.config import ConfSettings
|
||||
|
||||
# Conexión a MongoDB para Reportes
|
||||
mongo_client = MongoClient(ConfSettings.mongodb_url)
|
||||
mongodb = mongo_client[ConfSettings.mongodb_db]
|
||||
# Conexión a MongoDB para Reportes (Instancia Separada - Puerto 27017)
|
||||
mongo_client_reports = MongoClient(ConfSettings.mongodb_reports_url)
|
||||
mongodb_reports = mongo_client_reports[ConfSettings.mongodb_reports_db]
|
||||
|
||||
# Conexión a MongoDB para Notificaciones (Instancia Separada - Puerto 27018)
|
||||
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"]
|
||||
"""Obtiene la colección de reportes desde MongoDB (Instancia 1)"""
|
||||
return mongodb_reports["reportes"]
|
||||
|
||||
def get_notifications_collection() -> Collection:
|
||||
"""Obtiene la colección de notificaciones desde MongoDB (Instancia 2)"""
|
||||
return mongodb_notifications["notificaciones"]
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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 bson import ObjectId
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class NotificationRepositoryMongo(NotificationRepository):
|
||||
"""Implementación del repositorio de Notificaciones usando MongoDB (Instancia Separada)"""
|
||||
|
||||
def __init__(self):
|
||||
self.collection = get_notifications_collection()
|
||||
|
||||
def create(self, notification: Notification) -> str:
|
||||
"""Crea una nueva notificación"""
|
||||
notification_dict = {
|
||||
"id_usuario": notification.id_usuario,
|
||||
"id_reporte": notification.id_reporte,
|
||||
"message": notification.message,
|
||||
"fecha": notification.fecha or datetime.utcnow(),
|
||||
"read": notification.read
|
||||
}
|
||||
result = self.collection.insert_one(notification_dict)
|
||||
return str(result.inserted_id)
|
||||
|
||||
def get_by_id(self, notification_id: str) -> Optional[Notification]:
|
||||
"""Obtiene una notificación por ID"""
|
||||
try:
|
||||
doc = self.collection.find_one({"_id": ObjectId(notification_id)})
|
||||
if doc:
|
||||
return self._to_domain(doc)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_by_user(self, user_id: int, limit: int = 50, offset: int = 0) -> List[Notification]:
|
||||
"""Obtiene notificaciones de un usuario ordenadas por fecha descendente"""
|
||||
docs = self.collection.find(
|
||||
{"id_usuario": user_id}
|
||||
).sort("fecha", -1).skip(offset).limit(limit)
|
||||
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"""
|
||||
try:
|
||||
result = self.collection.update_one(
|
||||
{"_id": ObjectId(notification_id)},
|
||||
{"$set": {"read": True}}
|
||||
)
|
||||
return result.modified_count > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def mark_all_as_read(self, user_id: int) -> int:
|
||||
"""Marca todas las notificaciones de un usuario como leídas"""
|
||||
result = self.collection.update_many(
|
||||
{"id_usuario": user_id, "read": False},
|
||||
{"$set": {"read": True}}
|
||||
)
|
||||
return result.modified_count
|
||||
|
||||
def delete(self, notification_id: str) -> bool:
|
||||
"""Elimina una notificación"""
|
||||
try:
|
||||
result = self.collection.delete_one({"_id": ObjectId(notification_id)})
|
||||
return result.deleted_count > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_unread_count(self, user_id: int) -> int:
|
||||
"""Obtiene el número de notificaciones no leídas para un usuario"""
|
||||
return self.collection.count_documents({
|
||||
"id_usuario": user_id,
|
||||
"read": False
|
||||
})
|
||||
|
||||
def _to_domain(self, doc: dict) -> Notification:
|
||||
"""Convierte un documento de MongoDB a un objeto de dominio"""
|
||||
return Notification(
|
||||
id_notificacion=str(doc.get("_id")),
|
||||
id_usuario=doc.get("id_usuario"),
|
||||
id_reporte=doc.get("id_reporte"),
|
||||
message=doc.get("message"),
|
||||
fecha=doc.get("fecha"),
|
||||
read=doc.get("read", False)
|
||||
)
|
||||
@@ -17,6 +17,7 @@ class ReportEventType(str, Enum):
|
||||
"""Types of report events"""
|
||||
CREATE = "report.create"
|
||||
UPDATE_VISIBILITY = "report.update_visibility"
|
||||
UPDATE_STATUS = "report.update_status"
|
||||
DELETE = "report.delete"
|
||||
|
||||
|
||||
@@ -69,6 +70,10 @@ class ReportMessage:
|
||||
estado: Optional[str] = None # Estado del reporte: "en proceso", "no resuelto", "resuelto"
|
||||
fecha_creacion: Optional[str] = None # ISO format datetime string
|
||||
penalize_author: Optional[bool] = None # For update_visibility event
|
||||
old_estado: Optional[str] = None # Estado anterior (para UPDATE_STATUS)
|
||||
new_estado: Optional[str] = None # Nuevo estado (para UPDATE_STATUS)
|
||||
old_visibility: Optional[float] = None # Visibilidad anterior (para UPDATE_VISIBILITY)
|
||||
new_visibility: Optional[float] = None # Nueva visibilidad (para UPDATE_VISIBILITY)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary"""
|
||||
|
||||
0
src/infrastructure/api/notifications/__init__.py
Normal file
0
src/infrastructure/api/notifications/__init__.py
Normal file
14
src/infrastructure/api/notifications/app.py
Normal file
14
src/infrastructure/api/notifications/app.py
Normal file
@@ -0,0 +1,14 @@
|
||||
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 de reportes"
|
||||
)
|
||||
app.include_router(router)
|
||||
return app
|
||||
148
src/infrastructure/api/notifications/notifications.py
Normal file
148
src/infrastructure/api/notifications/notifications.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends, Header
|
||||
from infrastructure.api.notifications.schemas import (
|
||||
NotificationCreateRequest,
|
||||
NotificationResponse,
|
||||
NotificationListResponse,
|
||||
UnreadCountResponse,
|
||||
NotificationMarkAsReadRequest
|
||||
)
|
||||
from application.services.notification_services import NotificationService
|
||||
from typing import Optional
|
||||
|
||||
router = APIRouter()
|
||||
notification_service = NotificationService()
|
||||
|
||||
|
||||
@router.post("/", response_model=NotificationResponse, tags=["notifications"])
|
||||
async def create_notification(request: NotificationCreateRequest):
|
||||
"""Crea una nueva notificación (uso interno)"""
|
||||
try:
|
||||
notification_id = notification_service.create_notification(
|
||||
id_usuario=request.id_usuario,
|
||||
id_reporte=request.id_reporte,
|
||||
message=request.message
|
||||
)
|
||||
|
||||
notification = notification_service.get_notification(notification_id)
|
||||
if not notification:
|
||||
raise HTTPException(status_code=500, detail="Error creating notification")
|
||||
|
||||
return NotificationResponse(
|
||||
id_notificacion=notification.id_notificacion,
|
||||
id_usuario=notification.id_usuario,
|
||||
id_reporte=notification.id_reporte,
|
||||
message=notification.message,
|
||||
fecha=notification.fecha,
|
||||
read=notification.read
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=NotificationListResponse, tags=["notifications"])
|
||||
async def get_user_notifications(
|
||||
user_id: int,
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
offset: int = Query(0, ge=0)
|
||||
):
|
||||
"""
|
||||
Obtiene notificaciones de un usuario
|
||||
|
||||
Args:
|
||||
user_id: ID del usuario
|
||||
limit: Número máximo de notificaciones (default: 50, max: 100)
|
||||
offset: Desplazamiento de registros
|
||||
"""
|
||||
try:
|
||||
notifications = notification_service.get_user_notifications(
|
||||
user_id=user_id,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
unread_count = notification_service.get_unread_count(user_id)
|
||||
total = len(notifications) + offset # Aproximado
|
||||
|
||||
notification_responses = [
|
||||
NotificationResponse(
|
||||
id_notificacion=n.id_notificacion,
|
||||
id_usuario=n.id_usuario,
|
||||
id_reporte=n.id_reporte,
|
||||
message=n.message,
|
||||
fecha=n.fecha,
|
||||
read=n.read
|
||||
)
|
||||
for n in notifications
|
||||
]
|
||||
|
||||
return NotificationListResponse(
|
||||
total=total,
|
||||
unread_count=unread_count,
|
||||
notifications=notification_responses
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{user_id}/unread-count", response_model=UnreadCountResponse, tags=["notifications"])
|
||||
async def get_unread_count(user_id: int):
|
||||
"""Obtiene el número de notificaciones no leídas para un usuario"""
|
||||
try:
|
||||
unread_count = notification_service.get_unread_count(user_id)
|
||||
return UnreadCountResponse(unread_count=unread_count)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{notification_id}/read", response_model=NotificationResponse, tags=["notifications"])
|
||||
async def mark_notification_as_read(notification_id: str):
|
||||
"""Marca una notificación como leída"""
|
||||
try:
|
||||
success = notification_service.mark_as_read(notification_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Notification not found")
|
||||
|
||||
notification = notification_service.get_notification(notification_id)
|
||||
if not notification:
|
||||
raise HTTPException(status_code=404, detail="Notification not found")
|
||||
|
||||
return NotificationResponse(
|
||||
id_notificacion=notification.id_notificacion,
|
||||
id_usuario=notification.id_usuario,
|
||||
id_reporte=notification.id_reporte,
|
||||
message=notification.message,
|
||||
fecha=notification.fecha,
|
||||
read=notification.read
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{user_id}/read-all", response_model=dict, tags=["notifications"])
|
||||
async def mark_all_as_read(user_id: int):
|
||||
"""Marca todas las notificaciones de un usuario como leídas"""
|
||||
try:
|
||||
updated_count = notification_service.mark_all_as_read(user_id)
|
||||
return {
|
||||
"message": "All notifications marked as read",
|
||||
"updated_count": updated_count
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{notification_id}", response_model=dict, tags=["notifications"])
|
||||
async def delete_notification(notification_id: str):
|
||||
"""Elimina una notificación"""
|
||||
try:
|
||||
success = notification_service.delete_notification(notification_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Notification not found")
|
||||
|
||||
return {"message": "Notification deleted successfully"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
19
src/infrastructure/api/notifications/root.py
Normal file
19
src/infrastructure/api/notifications/root.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class HealthCheck(BaseModel):
|
||||
"""Health check response"""
|
||||
status: str
|
||||
service: str
|
||||
|
||||
|
||||
@router.get("/", response_model=HealthCheck, tags=["health"])
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {
|
||||
"status": "ok",
|
||||
"service": "Notificaciones Microservice"
|
||||
}
|
||||
17
src/infrastructure/api/notifications/router.py
Normal file
17
src/infrastructure/api/notifications/router.py
Normal file
@@ -0,0 +1,17 @@
|
||||
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"]
|
||||
)
|
||||
43
src/infrastructure/api/notifications/schemas.py
Normal file
43
src/infrastructure/api/notifications/schemas.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class NotificationCreateRequest(BaseModel):
|
||||
"""Solicitud para crear una notificación"""
|
||||
id_usuario: int
|
||||
id_reporte: str
|
||||
message: str
|
||||
|
||||
|
||||
class NotificationResponse(BaseModel):
|
||||
"""Respuesta con datos de notificación"""
|
||||
id_notificacion: str
|
||||
id_usuario: int
|
||||
id_reporte: str
|
||||
message: str
|
||||
fecha: datetime
|
||||
read: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NotificationListResponse(BaseModel):
|
||||
"""Respuesta con lista de notificaciones"""
|
||||
total: int
|
||||
unread_count: int
|
||||
notifications: list[NotificationResponse]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NotificationMarkAsReadRequest(BaseModel):
|
||||
"""Solicitud para marcar como leída"""
|
||||
pass
|
||||
|
||||
|
||||
class UnreadCountResponse(BaseModel):
|
||||
"""Respuesta con conteo de no leídas"""
|
||||
unread_count: int
|
||||
26
src/main.py
26
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 tres APIs en paralelo: Usuarios (puerto 8000), Reportes (puerto 8001) y 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,6 +53,10 @@ def run_reports_consumer():
|
||||
consumer = ReportConsumer()
|
||||
consumer.start()
|
||||
|
||||
def run_notifications_consumer():
|
||||
consumer = NotificationConsumer()
|
||||
consumer.start()
|
||||
|
||||
|
||||
def run():
|
||||
"""Inicia ambas APIs en threads separados"""
|
||||
@@ -49,25 +66,32 @@ def run():
|
||||
|
||||
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