añadido los estados de reporte

This commit is contained in:
2026-04-19 19:08:43 -06:00
parent 6083ab34ca
commit 30efa0e098
17 changed files with 1173 additions and 1047 deletions

View File

@@ -35,6 +35,11 @@ class ReportRepository(ABC):
"""Actualiza la visibilidad de un reporte""" """Actualiza la visibilidad de un reporte"""
pass pass
@abstractmethod
def update_estado(self, report_id: str, new_estado: str) -> None:
"""Actualiza el estado de un reporte"""
pass
@abstractmethod @abstractmethod
def delete(self, report_id: str) -> bool: def delete(self, report_id: str) -> bool:
"""Elimina un reporte""" """Elimina un reporte"""

View File

@@ -19,7 +19,8 @@ class CreateReport:
def execute(self, id_usuario: int, tipo_reporte: int, descripcion: str, def execute(self, id_usuario: int, tipo_reporte: int, descripcion: str,
ubicacion: Optional[str] = None, lat: Optional[float] = None, ubicacion: Optional[str] = None, lat: Optional[float] = None,
lng: Optional[float] = None, image_filename: Optional[str] = None) -> Dict[str, Any]: lng: Optional[float] = None, image_filename: Optional[str] = None,
estado: str = "en proceso") -> Dict[str, Any]:
""" """
Sends a create report message to RabbitMQ. Sends a create report message to RabbitMQ.
Valida previamente: Valida previamente:
@@ -44,6 +45,14 @@ class CreateReport:
"message": "El tipo de reporte debe estar entre 1 y 5" "message": "El tipo de reporte debe estar entre 1 y 5"
} }
# Validación: estado válido
estados_validos = ["en proceso", "no resuelto", "resuelto"]
if estado not in estados_validos:
return {
"status": "error",
"message": f"El estado del reporte debe ser uno de: {', '.join(estados_validos)}"
}
# Validación: usuario existe (CRÍTICO) # Validación: usuario existe (CRÍTICO)
try: try:
user = self.user_repo.find_by_id(id_usuario) user = self.user_repo.find_by_id(id_usuario)
@@ -73,6 +82,7 @@ class CreateReport:
lng=lng, lng=lng,
image_filename=image_filename, image_filename=image_filename,
visibilidad=50.0, # Visibilidad inicial neutral visibilidad=50.0, # Visibilidad inicial neutral
estado=estado,
fecha_creacion=fecha_creacion.isoformat() fecha_creacion=fecha_creacion.isoformat()
) )
@@ -244,3 +254,57 @@ class DeleteReport:
"status": "error", "status": "error",
"message": "Error al enviar eliminación del reporte a la cola de procesamiento" "message": "Error al enviar eliminación del reporte a la cola de procesamiento"
} }
class UpdateReportStatus:
"""Use case para actualizar el estado de un reporte"""
def __init__(self, repo: ReportRepository):
if not isinstance(repo, ReportRepository):
raise TypeError("repo must implement ReportRepository")
self.repo = repo
def execute(self, report_id: str, new_estado: str) -> Dict[str, Any]:
"""
Actualiza el estado de un reporte.
Valida previamente:
- Reporte existe
- Estado es válido
Returns:
Dictionary with status and message
"""
# Validación: estado válido
estados_validos = ["en proceso", "no resuelto", "resuelto"]
if new_estado not in estados_validos:
return {
"status": "error",
"message": f"El estado del reporte debe ser uno de: {', '.join(estados_validos)}"
}
# 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)}"
}
# Actualizar estado
try:
self.repo.update_estado(report_id, new_estado)
return {
"status": "success",
"message": f"Estado del reporte actualizado a '{new_estado}'",
"id_reporte": report_id,
"nuevo_estado": new_estado
}
except Exception as e:
return {
"status": "error",
"message": f"Error al actualizar estado del reporte: {str(e)}"
}

View File

@@ -1,6 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, Literal
@dataclass @dataclass
class Report: class Report:
@@ -14,4 +14,5 @@ class Report:
lng: Optional[float] = None lng: Optional[float] = None
image_filename: Optional[str] = None # Nombre del archivo de imagen WebP image_filename: Optional[str] = None # Nombre del archivo de imagen WebP
visibilidad: float = 0.0 # 0-100 (puntuación comunitaria) visibilidad: float = 0.0 # 0-100 (puntuación comunitaria)
estado: Literal["en proceso", "no resuelto", "resuelto"] = "en proceso" # Estado del reporte
fecha_creacion: Optional[datetime] = None fecha_creacion: Optional[datetime] = None

View File

@@ -23,6 +23,7 @@ class ReportRepositoryMongo(ReportRepository):
"lng": report.lng, "lng": report.lng,
"image_filename": report.image_filename, "image_filename": report.image_filename,
"visibilidad": report.visibilidad, "visibilidad": report.visibilidad,
"estado": report.estado,
"fecha_creacion": report.fecha_creacion or datetime.utcnow() "fecha_creacion": report.fecha_creacion or datetime.utcnow()
} }
result = self.collection.insert_one(report_dict) result = self.collection.insert_one(report_dict)
@@ -59,6 +60,13 @@ class ReportRepositoryMongo(ReportRepository):
{"$set": {"visibilidad": new_visibility}} {"$set": {"visibilidad": new_visibility}}
) )
def update_estado(self, report_id: str, new_estado: str) -> None:
"""Actualiza el estado de un reporte"""
self.collection.update_one(
{"id_reporte": report_id},
{"$set": {"estado": new_estado}}
)
def delete(self, report_id: str) -> bool: def delete(self, report_id: str) -> bool:
"""Elimina un reporte""" """Elimina un reporte"""
result = self.collection.delete_one({"id_reporte": report_id}) result = self.collection.delete_one({"id_reporte": report_id})
@@ -83,5 +91,6 @@ class ReportRepositoryMongo(ReportRepository):
lng=doc.get("lng"), lng=doc.get("lng"),
image_filename=doc.get("image_filename"), image_filename=doc.get("image_filename"),
visibilidad=doc.get("visibilidad"), visibilidad=doc.get("visibilidad"),
estado=doc.get("estado", "en proceso"),
fecha_creacion=doc.get("fecha_creacion") fecha_creacion=doc.get("fecha_creacion")
) )

View File

@@ -65,6 +65,7 @@ class ReportMessage:
lng: Optional[float] = None lng: Optional[float] = None
image_filename: Optional[str] = None # Nombre del archivo de imagen guardado image_filename: Optional[str] = None # Nombre del archivo de imagen guardado
visibilidad: Optional[float] = None visibilidad: Optional[float] = None
estado: Optional[str] = None # Estado del reporte: "en proceso", "no resuelto", "resuelto"
fecha_creacion: Optional[str] = None # ISO format datetime string fecha_creacion: Optional[str] = None # ISO format datetime string
penalize_author: Optional[bool] = None # For update_visibility event penalize_author: Optional[bool] = None # For update_visibility event

View File

@@ -1,8 +1,8 @@
from fastapi import APIRouter, HTTPException, status, UploadFile, File, Form from fastapi import APIRouter, HTTPException, status, UploadFile, File, Form
from infrastructure.api.reports.schemas import ReportCreateRequest, ReportUpdateVisibilityRequest, ReportResponse from infrastructure.api.reports.schemas import ReportCreateRequest, ReportUpdateVisibilityRequest, ReportUpdateStatusRequest, ReportResponse
from application.services.report_services import ( from application.services.report_services import (
CreateReport, GetReportById, GetReportsByUser, ListAllReports, CreateReport, GetReportById, GetReportsByUser, ListAllReports,
UpdateReportVisibility, GetShadowbannedReports, DeleteReport UpdateReportVisibility, GetShadowbannedReports, DeleteReport, UpdateReportStatus
) )
from infrastructure.adapters.persistence.report_repository_mongo import ReportRepositoryMongo from infrastructure.adapters.persistence.report_repository_mongo import ReportRepositoryMongo
from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL
@@ -28,6 +28,7 @@ def _report_to_response(report) -> dict:
"lng": report.lng, "lng": report.lng,
"image_url": image_storage.get_image_url(report.image_filename) if report.image_filename else None, "image_url": image_storage.get_image_url(report.image_filename) if report.image_filename else None,
"visibilidad": report.visibilidad, "visibilidad": report.visibilidad,
"estado": report.estado,
"fecha_creacion": report.fecha_creacion "fecha_creacion": report.fecha_creacion
} }
@@ -39,6 +40,7 @@ async def create_report(
ubicacion: Optional[str] = Form(None), ubicacion: Optional[str] = Form(None),
lat: Optional[float] = Form(None), lat: Optional[float] = Form(None),
lng: Optional[float] = Form(None), lng: Optional[float] = Form(None),
estado: str = Form("en proceso"),
file: Optional[UploadFile] = File(None) file: Optional[UploadFile] = File(None)
): ):
"""Crea un nuevo reporte - envía a cola de procesamiento con validaciones previas""" """Crea un nuevo reporte - envía a cola de procesamiento con validaciones previas"""
@@ -57,7 +59,8 @@ async def create_report(
ubicacion=ubicacion, ubicacion=ubicacion,
lat=lat, lat=lat,
lng=lng, lng=lng,
image_filename=image_filename image_filename=image_filename,
estado=estado
) )
if result["status"] == "error": if result["status"] == "error":
@@ -225,3 +228,40 @@ async def delete_report(report_id: str):
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error interno del servidor" detail="Error interno del servidor"
) )
@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"""
try:
update_use_case = UpdateReportStatus(report_repo)
result = update_use_case.execute(
report_id=report_id,
new_estado=status_data.estado
)
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
)
# 200 OK: estado actualizado correctamente
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error al actualizar estado del reporte {report_id}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error interno del servidor"
)

View File

@@ -1,6 +1,6 @@
from pydantic import BaseModel from pydantic import BaseModel
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, Literal
from fastapi import Form, UploadFile, File from fastapi import Form, UploadFile, File
class ReportCreateRequest(BaseModel): class ReportCreateRequest(BaseModel):
@@ -11,6 +11,7 @@ class ReportCreateRequest(BaseModel):
ubicacion: Optional[str] = None ubicacion: Optional[str] = None
lat: Optional[float] = None lat: Optional[float] = None
lng: Optional[float] = None lng: Optional[float] = None
estado: Literal["en proceso", "no resuelto", "resuelto"] = "en proceso" # Estado del reporte
# file se recibe como UploadFile en el endpoint, no en el modelo # file se recibe como UploadFile en el endpoint, no en el modelo
class ReportUpdateVisibilityRequest(BaseModel): class ReportUpdateVisibilityRequest(BaseModel):
@@ -18,6 +19,10 @@ class ReportUpdateVisibilityRequest(BaseModel):
new_visibility: float new_visibility: float
penalize_author: bool = False penalize_author: bool = False
class ReportUpdateStatusRequest(BaseModel):
"""Solicitud para actualizar el estado de un reporte"""
estado: Literal["en proceso", "no resuelto", "resuelto"]
class ReportResponse(BaseModel): class ReportResponse(BaseModel):
"""Respuesta con datos de reporte""" """Respuesta con datos de reporte"""
id_reporte: str id_reporte: str
@@ -29,6 +34,7 @@ class ReportResponse(BaseModel):
lng: Optional[float] lng: Optional[float]
image_url: Optional[str] = None # URL pública para acceder a la imagen image_url: Optional[str] = None # URL pública para acceder a la imagen
visibilidad: float visibilidad: float
estado: Literal["en proceso", "no resuelto", "resuelto"] = "en proceso" # Estado del reporte
fecha_creacion: Optional[datetime] fecha_creacion: Optional[datetime]
class Config: class Config: