## Backend Changes - Add new API endpoints: combustible, pois, mantenimiento, video, configuracion - Fix vehiculos endpoint to return paginated response with items array - Add /vehiculos/all endpoint for non-paginated list - Add /geocercas/all endpoint - Add /alertas/configuracion GET/PUT endpoints - Add /viajes/activos and /viajes/iniciar endpoints - Add /reportes/stats, /reportes/templates, /reportes/preview endpoints - Add /conductores/all and /conductores/disponibles endpoints - Update router.py to include all new modules ## Frontend Changes - Fix authentication token handling (snake_case vs camelCase) - Update vehiculosApi.listAll to use /vehiculos/all - Fix FuelGauge component usage in Combustible page - Fix chart component exports (named + default exports) - Update API client for proper token refresh ## Infrastructure - Rename services from ADAN to ATLAS - Configure Cloudflare tunnel for atlas.consultoria-as.com - Update systemd service files - Configure PostgreSQL with TimescaleDB - Configure Redis, Mosquitto, Traccar, MediaMTX ## Documentation - Update installation guides - Update API reference - Rename all ADAN references to ATLAS Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
281 lines
8.2 KiB
Python
281 lines
8.2 KiB
Python
"""
|
|
Excepciones personalizadas para la aplicación.
|
|
|
|
Define excepciones específicas del dominio y handlers
|
|
para convertirlas en respuestas HTTP apropiadas.
|
|
"""
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
from fastapi import HTTPException, Request, status
|
|
from fastapi.responses import JSONResponse
|
|
|
|
|
|
class AtlasException(Exception):
|
|
"""Excepción base para todas las excepciones de la aplicación."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
code: str = "ATLAS_ERROR",
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
self.message = message
|
|
self.code = code
|
|
self.details = details or {}
|
|
super().__init__(self.message)
|
|
|
|
|
|
class NotFoundError(AtlasException):
|
|
"""Recurso no encontrado."""
|
|
|
|
def __init__(
|
|
self,
|
|
resource: str,
|
|
identifier: Any = None,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
message = f"{resource} no encontrado"
|
|
if identifier:
|
|
message = f"{resource} con id '{identifier}' no encontrado"
|
|
super().__init__(message, "NOT_FOUND", details)
|
|
self.resource = resource
|
|
self.identifier = identifier
|
|
|
|
|
|
class AlreadyExistsError(AtlasException):
|
|
"""El recurso ya existe."""
|
|
|
|
def __init__(
|
|
self,
|
|
resource: str,
|
|
field: str,
|
|
value: Any,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
message = f"{resource} con {field}='{value}' ya existe"
|
|
super().__init__(message, "ALREADY_EXISTS", details)
|
|
self.resource = resource
|
|
self.field = field
|
|
self.value = value
|
|
|
|
|
|
class ValidationError(AtlasException):
|
|
"""Error de validación de datos."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
field: Optional[str] = None,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
super().__init__(message, "VALIDATION_ERROR", details)
|
|
self.field = field
|
|
|
|
|
|
class AuthenticationError(AtlasException):
|
|
"""Error de autenticación."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Credenciales inválidas",
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
super().__init__(message, "AUTHENTICATION_ERROR", details)
|
|
|
|
|
|
class AuthorizationError(AtlasException):
|
|
"""Error de autorización (permisos insuficientes)."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "No tiene permisos para realizar esta acción",
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
super().__init__(message, "AUTHORIZATION_ERROR", details)
|
|
|
|
|
|
class ExternalServiceError(AtlasException):
|
|
"""Error al comunicarse con un servicio externo."""
|
|
|
|
def __init__(
|
|
self,
|
|
service: str,
|
|
message: str,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
full_message = f"Error en servicio {service}: {message}"
|
|
super().__init__(full_message, "EXTERNAL_SERVICE_ERROR", details)
|
|
self.service = service
|
|
|
|
|
|
class GeocercaViolationError(AtlasException):
|
|
"""Violación de geocerca detectada."""
|
|
|
|
def __init__(
|
|
self,
|
|
geocerca_id: int,
|
|
geocerca_nombre: str,
|
|
tipo_violacion: str, # 'entrada' o 'salida'
|
|
vehiculo_id: int,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
message = f"Vehículo {vehiculo_id} {tipo_violacion} de geocerca '{geocerca_nombre}'"
|
|
super().__init__(message, "GEOCERCA_VIOLATION", details)
|
|
self.geocerca_id = geocerca_id
|
|
self.geocerca_nombre = geocerca_nombre
|
|
self.tipo_violacion = tipo_violacion
|
|
self.vehiculo_id = vehiculo_id
|
|
|
|
|
|
class SpeedLimitExceededError(AtlasException):
|
|
"""Límite de velocidad excedido."""
|
|
|
|
def __init__(
|
|
self,
|
|
vehiculo_id: int,
|
|
velocidad: float,
|
|
limite: float,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
message = f"Vehículo {vehiculo_id} excedió límite de velocidad: {velocidad} km/h (límite: {limite} km/h)"
|
|
super().__init__(message, "SPEED_LIMIT_EXCEEDED", details)
|
|
self.vehiculo_id = vehiculo_id
|
|
self.velocidad = velocidad
|
|
self.limite = limite
|
|
|
|
|
|
class DeviceConnectionError(AtlasException):
|
|
"""Error de conexión con dispositivo."""
|
|
|
|
def __init__(
|
|
self,
|
|
dispositivo_id: int,
|
|
message: str,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
full_message = f"Error de conexión con dispositivo {dispositivo_id}: {message}"
|
|
super().__init__(full_message, "DEVICE_CONNECTION_ERROR", details)
|
|
self.dispositivo_id = dispositivo_id
|
|
|
|
|
|
class VideoStreamError(AtlasException):
|
|
"""Error con stream de video."""
|
|
|
|
def __init__(
|
|
self,
|
|
camara_id: int,
|
|
message: str,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
full_message = f"Error de video en cámara {camara_id}: {message}"
|
|
super().__init__(full_message, "VIDEO_STREAM_ERROR", details)
|
|
self.camara_id = camara_id
|
|
|
|
|
|
class MaintenanceRequiredError(AtlasException):
|
|
"""Mantenimiento requerido para el vehículo."""
|
|
|
|
def __init__(
|
|
self,
|
|
vehiculo_id: int,
|
|
tipo_mantenimiento: str,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
message = f"Vehículo {vehiculo_id} requiere mantenimiento: {tipo_mantenimiento}"
|
|
super().__init__(message, "MAINTENANCE_REQUIRED", details)
|
|
self.vehiculo_id = vehiculo_id
|
|
self.tipo_mantenimiento = tipo_mantenimiento
|
|
|
|
|
|
class DatabaseError(AtlasException):
|
|
"""Error de base de datos."""
|
|
|
|
def __init__(
|
|
self,
|
|
operation: str,
|
|
message: str,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
):
|
|
full_message = f"Error de base de datos en {operation}: {message}"
|
|
super().__init__(full_message, "DATABASE_ERROR", details)
|
|
self.operation = operation
|
|
|
|
|
|
# ============================================================================
|
|
# Exception Handlers para FastAPI
|
|
# ============================================================================
|
|
|
|
|
|
async def atlas_exception_handler(request: Request, exc: AtlasException) -> JSONResponse:
|
|
"""Handler para excepciones base de Atlas."""
|
|
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
|
|
if isinstance(exc, NotFoundError):
|
|
status_code = status.HTTP_404_NOT_FOUND
|
|
elif isinstance(exc, AlreadyExistsError):
|
|
status_code = status.HTTP_409_CONFLICT
|
|
elif isinstance(exc, ValidationError):
|
|
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
elif isinstance(exc, AuthenticationError):
|
|
status_code = status.HTTP_401_UNAUTHORIZED
|
|
elif isinstance(exc, AuthorizationError):
|
|
status_code = status.HTTP_403_FORBIDDEN
|
|
elif isinstance(exc, ExternalServiceError):
|
|
status_code = status.HTTP_502_BAD_GATEWAY
|
|
|
|
return JSONResponse(
|
|
status_code=status_code,
|
|
content={
|
|
"error": {
|
|
"code": exc.code,
|
|
"message": exc.message,
|
|
"details": exc.details,
|
|
}
|
|
},
|
|
)
|
|
|
|
|
|
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
|
"""Handler para HTTPException estándar de FastAPI."""
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={
|
|
"error": {
|
|
"code": "HTTP_ERROR",
|
|
"message": exc.detail,
|
|
"details": {},
|
|
}
|
|
},
|
|
)
|
|
|
|
|
|
async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
"""Handler para excepciones no capturadas."""
|
|
# En producción, loguear el error completo pero no exponerlo al cliente
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
return JSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content={
|
|
"error": {
|
|
"code": "INTERNAL_ERROR",
|
|
"message": "Error interno del servidor",
|
|
"details": {},
|
|
}
|
|
},
|
|
)
|
|
|
|
|
|
def register_exception_handlers(app) -> None:
|
|
"""
|
|
Registra los handlers de excepciones en la aplicación FastAPI.
|
|
|
|
Args:
|
|
app: Instancia de FastAPI.
|
|
"""
|
|
app.add_exception_handler(AtlasException, atlas_exception_handler)
|
|
app.add_exception_handler(HTTPException, http_exception_handler)
|
|
app.add_exception_handler(Exception, general_exception_handler)
|