diff --git a/src/application/services/report_services.py b/src/application/services/report_services.py index f97bbee..ad892d7 100644 --- a/src/application/services/report_services.py +++ b/src/application/services/report_services.py @@ -21,17 +21,40 @@ class CreateReport: ubicacion: Optional[str] = None) -> Dict[str, Any]: """ Sends a create report message to RabbitMQ. - The actual database save will be done by the consumer. + Valida previamente: + - Usuario existe + - Descripción no está vacía + - Tipo de reporte válido Returns: Dictionary with status and message """ - # Verify user exists (we still need to check this before queuing) - user = self.user_repo.find_by_id(id_usuario) - if not user: + # Validación: descripción requerida + if not descripcion or not descripcion.strip(): return { "status": "error", - "message": f"Usuario con ID {id_usuario} no existe" + "message": "La descripción del reporte es requerida" + } + + # Validación: tipo de reporte válido (1-5) + if tipo_reporte < 1 or tipo_reporte > 5: + return { + "status": "error", + "message": "El tipo de reporte debe estar entre 1 y 5" + } + + # Validación: usuario existe (CRÍTICO) + try: + user = self.user_repo.find_by_id(id_usuario) + if not user: + return { + "status": "error", + "message": f"Usuario con ID {id_usuario} no existe" + } + except Exception as e: + return { + "status": "error", + "message": f"Error al validar usuario: {str(e)}" } id_reporte = str(uuid4()) @@ -43,7 +66,7 @@ class CreateReport: id_reporte=id_reporte, id_usuario=id_usuario, tipo_reporte=tipo_reporte, - descripcion=descripcion, + descripcion=descripcion.strip(), ubicacion=ubicacion, visibilidad=50.0, # Visibilidad inicial neutral fecha_creacion=fecha_creacion.isoformat() @@ -105,11 +128,27 @@ class UpdateReportVisibility: def execute(self, report_id: str, new_visibility: float, penalize_author: bool = False) -> Dict[str, Any]: """ Sends an update report visibility message to RabbitMQ. - The actual database update will be done by the consumer. + Valida previamente: + - Reporte existe + - Visibilidad en rango válido (0-100) Returns: Dictionary with status and message """ + # Validación: reporte existe + try: + report = self.repo.find_by_id(report_id) + if not report: + return { + "status": "error", + "message": f"Reporte con ID {report_id} no existe" + } + except Exception as e: + return { + "status": "error", + "message": f"Error al buscar reporte: {str(e)}" + } + # Validar rango de visibilidad if new_visibility < 0 or new_visibility > 100: return { @@ -161,11 +200,26 @@ class DeleteReport: def execute(self, report_id: str) -> Dict[str, Any]: """ Sends a delete report message to RabbitMQ. - The actual database deletion will be done by the consumer. + Valida previamente: + - Reporte existe Returns: Dictionary with status and message """ + # Validación: reporte existe + try: + report = self.repo.find_by_id(report_id) + if not report: + return { + "status": "error", + "message": f"Reporte con ID {report_id} no existe" + } + except Exception as e: + return { + "status": "error", + "message": f"Error al buscar reporte: {str(e)}" + } + # Create message object message = ReportMessage( event_type=ReportEventType.DELETE, diff --git a/src/application/services/user_services.py b/src/application/services/user_services.py index 7ae7c09..5cf69a8 100644 --- a/src/application/services/user_services.py +++ b/src/application/services/user_services.py @@ -4,6 +4,7 @@ from infrastructure.adapters.rabbitmq.sender import send_to_queue from infrastructure.adapters.rabbitmq.messages import UserMessage, UserEventType from datetime import datetime from typing import List, Optional, Dict, Any +import re class CreateUser: """Use case para crear un nuevo usuario - envía mensaje a RabbitMQ""" @@ -19,17 +20,61 @@ class CreateUser: Sends a create user message to RabbitMQ. The actual database save will be done by the consumer. + Valida previamente: + - Email válido + - Email no duplicado + - Campos no vacíos + Returns: Dictionary with status and message """ + # Validación de campos requeridos + if not nombre or not nombre.strip(): + return { + "status": "error", + "message": "El nombre es requerido" + } + if not apellido or not apellido.strip(): + return { + "status": "error", + "message": "El apellido es requerido" + } + if not email or not email.strip(): + return { + "status": "error", + "message": "El email es requerido" + } + + # Validación de formato email + email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + if not re.match(email_pattern, email): + return { + "status": "error", + "message": "El email no tiene un formato válido" + } + + # Validación de email duplicado (CRÍTICO) + try: + existing_user = self.repo.find_by_email(email) + if existing_user: + return { + "status": "error", + "message": f"El email '{email}' ya está registrado" + } + except Exception as e: + return { + "status": "error", + "message": f"Error al validar email: {str(e)}" + } + fecha_creacion = datetime.now() # Create message object message = UserMessage( event_type=UserEventType.CREATE, - nombre=nombre, - apellido=apellido, - email=email, + nombre=nombre.strip(), + apellido=apellido.strip(), + email=email.strip(), fecha_nacimiento=fecha_nacimiento.isoformat(), fecha_creacion=fecha_creacion.isoformat(), calificacion=50.0, @@ -94,17 +139,40 @@ class UpdateUser: url_foto_perfil: str = None, biografia: str = None) -> Dict[str, Any]: """ Sends an update user message to RabbitMQ. - The actual database update will be done by the consumer. + + Valida previamente: + - Usuario existe Returns: Dictionary with status and message """ + # Validación: usuario existe + try: + user = self.repo.find_by_id(user_id) + if not user: + return { + "status": "error", + "message": f"Usuario con ID {user_id} no existe" + } + except Exception as e: + return { + "status": "error", + "message": f"Error al buscar usuario: {str(e)}" + } + + # Validación: al menos un campo para actualizar + if not any([nombre, apellido, url_foto_perfil, biografia]): + return { + "status": "error", + "message": "Se requiere al menos un campo para actualizar" + } + # Create message object with only the fields to update message = UserMessage( event_type=UserEventType.UPDATE, user_id=user_id, - nombre=nombre, - apellido=apellido, + nombre=nombre.strip() if nombre else None, + apellido=apellido.strip() if apellido else None, url_foto_perfil=url_foto_perfil, biografia=biografia ) @@ -134,11 +202,27 @@ class DeleteUser: def execute(self, user_id: int) -> Dict[str, Any]: """ Sends a delete user message to RabbitMQ. - The actual database deletion will be done by the consumer. + + Valida previamente: + - Usuario existe Returns: Dictionary with status and message """ + # Validación: usuario existe + try: + user = self.repo.find_by_id(user_id) + if not user: + return { + "status": "error", + "message": f"Usuario con ID {user_id} no existe" + } + except Exception as e: + return { + "status": "error", + "message": f"Error al buscar usuario: {str(e)}" + } + # Create message object message = UserMessage( event_type=UserEventType.DELETE, diff --git a/src/infrastructure/api/reports/reports.py b/src/infrastructure/api/reports/reports.py index 686a242..3b1c266 100644 --- a/src/infrastructure/api/reports/reports.py +++ b/src/infrastructure/api/reports/reports.py @@ -6,14 +6,16 @@ from application.services.report_services import ( ) from infrastructure.adapters.persistence.report_repository_mongo import ReportRepositoryMongo from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL +import logging router = APIRouter() report_repo = ReportRepositoryMongo() user_repo = UserRepositorySQL() +logger = logging.getLogger(__name__) @router.post("/", status_code=status.HTTP_202_ACCEPTED) async def create_report(report_data: ReportCreateRequest): - """Crea un nuevo reporte - envía a cola de procesamiento""" + """Crea un nuevo reporte - envía a cola de procesamiento con validaciones previas""" try: create_use_case = CreateReport(report_repo, user_repo) result = create_use_case.execute( @@ -22,54 +24,98 @@ async def create_report(report_data: ReportCreateRequest): descripcion=report_data.descripcion, ubicacion=report_data.ubicacion ) + if result["status"] == "error": - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=result["message"] - ) + message = result["message"] + if "no existe" in message: + # 404 Not Found: usuario no existe + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=message + ) + else: + # 400 Bad Request: error de validación + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=message + ) + + # 202 Accepted: enviado a la cola correctamente return result + except HTTPException: raise except Exception as e: + logger.error(f"Error inesperado en create_report: {e}", exc_info=True) raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Error al crear reporte: {str(e)}" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" ) @router.get("/{report_id}", response_model=ReportResponse) async def get_report(report_id: str): """Obtiene un reporte por ID""" - get_use_case = GetReportById(report_repo) - report = get_use_case.execute(report_id) - if not report: + try: + get_use_case = GetReportById(report_repo) + report = get_use_case.execute(report_id) + if not report: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Reporte con ID {report_id} no encontrado" + ) + return report + except HTTPException: + raise + except Exception as e: + logger.error(f"Error al obtener reporte {report_id}: {e}", exc_info=True) raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Reporte con ID {report_id} no encontrado" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" ) - return report -@router.get("/user/{user_id}", response_model=list[ReportResponse]) +@router.get("/user/{user_id}") async def get_user_reports(user_id: int): - """Obtiene todos los reportes de un usuario""" - get_use_case = GetReportsByUser(report_repo) - reports = get_use_case.execute(user_id) - return reports + """Obtiene todos los reportes de un usuario - retorna lista vacía si no hay reportes""" + try: + get_use_case = GetReportsByUser(report_repo) + reports = get_use_case.execute(user_id) + return reports + except Exception as e: + logger.error(f"Error al obtener reportes 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("/", response_model=list[ReportResponse]) +@router.get("/") async def list_reports(): - """Obtiene todos los reportes""" - list_use_case = ListAllReports(report_repo) - return list_use_case.execute() + """Obtiene todos los reportes - retorna lista vacía si no hay reportes""" + try: + list_use_case = ListAllReports(report_repo) + return list_use_case.execute() + except Exception as e: + logger.error(f"Error al listar reportes: {e}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" + ) -@router.get("/shadowbanned/list", response_model=list[ReportResponse]) +@router.get("/shadowbanned/list") async def get_shadowbanned_reports(threshold: float = 20): - """Obtiene reportes shadowbaneados (baja visibilidad)""" - get_use_case = GetShadowbannedReports(report_repo) - return get_use_case.execute(threshold) + """Obtiene reportes shadowbaneados (baja visibilidad) - retorna lista vacía si no hay""" + try: + get_use_case = GetShadowbannedReports(report_repo) + return get_use_case.execute(threshold) + except Exception as e: + logger.error(f"Error al obtener reportes shadowbaneados: {e}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" + ) @router.put("/{report_id}/visibility", status_code=status.HTTP_202_ACCEPTED) async def update_report_visibility(report_id: str, visibility_data: ReportUpdateVisibilityRequest): - """Actualiza la visibilidad de un reporte - envía a cola de procesamiento""" + """Actualiza la visibilidad de un reporte - envía a cola de procesamiento con validaciones previas""" try: update_use_case = UpdateReportVisibility(report_repo, user_repo) result = update_use_case.execute( @@ -77,28 +123,64 @@ async def update_report_visibility(report_id: str, visibility_data: ReportUpdate new_visibility=visibility_data.new_visibility, penalize_author=visibility_data.penalize_author ) + if result["status"] == "error": - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=result["message"] - ) + message = result["message"] + if "no existe" in message: + # 404 Not Found: reporte no existe + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=message + ) + else: + # 400 Bad Request: error de validación + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=message + ) + + # 202 Accepted: enviado a la cola correctamente return result + except HTTPException: raise except Exception as e: + logger.error(f"Error al actualizar visibilidad del reporte {report_id}: {e}", exc_info=True) raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Error al actualizar visibilidad: {str(e)}" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" ) @router.delete("/{report_id}", status_code=status.HTTP_202_ACCEPTED) async def delete_report(report_id: str): - """Elimina un reporte - envía a cola de procesamiento""" - delete_use_case = DeleteReport(report_repo) - result = delete_use_case.execute(report_id) - if result["status"] == "error": + """Elimina un reporte - envía a cola de procesamiento con validaciones previas""" + try: + delete_use_case = DeleteReport(report_repo) + result = delete_use_case.execute(report_id) + + if result["status"] == "error": + message = result["message"] + if "no existe" in message: + # 404 Not Found: reporte no existe + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=message + ) + else: + # 400 Bad Request: error de validación + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=message + ) + + # 202 Accepted: enviado a la cola correctamente + return result + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error al eliminar reporte {report_id}: {e}", exc_info=True) raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=result["message"] + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" ) - return result diff --git a/src/infrastructure/api/users/users.py b/src/infrastructure/api/users/users.py index 79f2557..66c2e42 100644 --- a/src/infrastructure/api/users/users.py +++ b/src/infrastructure/api/users/users.py @@ -4,13 +4,15 @@ from application.services.user_services import ( CreateUser, GetUserById, GetUserByEmail, ListAllUsers, UpdateUser, DeleteUser ) from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL +import logging router = APIRouter() user_repo = UserRepositorySQL() +logger = logging.getLogger(__name__) @router.post("/", status_code=status.HTTP_202_ACCEPTED) async def create_user(user_data: UserCreateRequest): - """Crea un nuevo usuario - envía a cola de procesamiento""" + """Crea un nuevo usuario - envía a cola de procesamiento con validaciones previas""" try: create_use_case = CreateUser(user_repo) result = create_use_case.execute( @@ -21,76 +23,160 @@ async def create_user(user_data: UserCreateRequest): url_foto_perfil=user_data.url_foto_perfil, biografia=user_data.biografia ) + if result["status"] == "error": - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=result["message"] - ) + # Detectar tipo de error para código HTTP apropiado + message = result["message"] + if "ya está registrado" in message: + # 409 Conflict: email duplicado + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=message + ) + else: + # 400 Bad Request: error de validación + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=message + ) + + # 202 Accepted: enviado a la cola correctamente return result + except HTTPException: raise except Exception as e: + logger.error(f"Error inesperado en create_user: {e}", exc_info=True) raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Error al crear usuario: {str(e)}" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" ) @router.get("/{user_id}", response_model=UserResponse) async def get_user(user_id: int): """Obtiene un usuario por ID""" - get_use_case = GetUserById(user_repo) - user = get_use_case.execute(user_id) - if not user: + try: + get_use_case = GetUserById(user_repo) + user = get_use_case.execute(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Usuario con ID {user_id} no encontrado" + ) + return user + except HTTPException: + raise + except Exception as e: + logger.error(f"Error al obtener usuario {user_id}: {e}", exc_info=True) raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Usuario con ID {user_id} no encontrado" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" ) - return user @router.get("/email/{email}", response_model=UserResponse) async def get_user_by_email(email: str): """Obtiene un usuario por email""" - get_use_case = GetUserByEmail(user_repo) - user = get_use_case.execute(email) - if not user: + try: + get_use_case = GetUserByEmail(user_repo) + user = get_use_case.execute(email) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Usuario con email {email} no encontrado" + ) + return user + except HTTPException: + raise + except Exception as e: + logger.error(f"Error al obtener usuario por email {email}: {e}", exc_info=True) raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Usuario con email {email} no encontrado" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" ) - return user -@router.get("/", response_model=list[UserResponse]) +@router.get("/") async def list_users(): - """Obtiene todos los usuarios""" - list_use_case = ListAllUsers(user_repo) - return list_use_case.execute() + """Obtiene todos los usuarios - retorna lista vacía si no hay registros""" + try: + list_use_case = ListAllUsers(user_repo) + return list_use_case.execute() + except Exception as e: + logger.error(f"Error al listar usuarios: {e}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" + ) @router.put("/{user_id}", status_code=status.HTTP_202_ACCEPTED) async def update_user(user_id: int, user_data: UserUpdateRequest): - """Actualiza un usuario - envía a cola de procesamiento""" - update_use_case = UpdateUser(user_repo) - result = update_use_case.execute( - user_id=user_id, - nombre=user_data.nombre, - apellido=user_data.apellido, - url_foto_perfil=user_data.url_foto_perfil, - biografia=user_data.biografia - ) - if result["status"] == "error": - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=result["message"] + """Actualiza un usuario - envía a cola de procesamiento con validaciones previas""" + try: + update_use_case = UpdateUser(user_repo) + result = update_use_case.execute( + user_id=user_id, + nombre=user_data.nombre, + apellido=user_data.apellido, + url_foto_perfil=user_data.url_foto_perfil, + biografia=user_data.biografia + ) + + if result["status"] == "error": + message = result["message"] + if "no existe" in message: + # 404 Not Found: usuario no existe + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=message + ) + else: + # 400 Bad Request: error de validación + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=message + ) + + # 202 Accepted: enviado a la cola correctamente + return result + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error al actualizar usuario {user_id}: {e}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" ) - return result @router.delete("/{user_id}", status_code=status.HTTP_202_ACCEPTED) async def delete_user(user_id: int): - """Elimina un usuario - envía a cola de procesamiento""" - delete_use_case = DeleteUser(user_repo) - result = delete_use_case.execute(user_id) - if result["status"] == "error": + """Elimina un usuario - envía a cola de procesamiento con validaciones previas""" + try: + delete_use_case = DeleteUser(user_repo) + result = delete_use_case.execute(user_id) + + if result["status"] == "error": + message = result["message"] + if "no existe" in message: + # 404 Not Found: usuario no existe + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=message + ) + else: + # 400 Bad Request: error de validación + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=message + ) + + # 202 Accepted: enviado a la cola correctamente + return result + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error al eliminar usuario {user_id}: {e}", exc_info=True) raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=result["message"] + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error interno del servidor" ) - return result