Files
plantillas-proyectos/backend/core/error_handlers.py
Galindo97 7b704e0744 feat: fix R1 pedimento duplication and improve invoice report exports
- Fix bidirectional R1 resolution in all report query builders (Temporary,
  Definitive, Repair, Export, ExportRepair): use JOIN on
  pedimento_rectification_origin in both directions so the rectified
  pedimento number is resolved correctly and not duplicated.
- Restore original CSV export format (ValorComercialMN, ValorMPTemp,
  ValorAgre as separate columns); compute them from item_line_financials
  SUM instead of invoice-level header totals which were always 0.
- Rename CSV column "PEDIMENTO RECTIFICACION" to "PEDIMENTO R1" in both
  backend csv_utils.py and frontend manual download.
- Harden temporary invoice update validator to safely handle null
  compliance_mx / logistics objects without crashing.
- Add R1 rectification fields (es_rectificacion, pedimento_original, etc.)
  to the pedimento other-data form and initialize their default state.
- Remove default companyId parameter from pedimentosApi methods to avoid
  hardcoded company ID 1.
- Minor: whitespace cleanup, error handler adjustments, keyboard manager
  fix.
2026-02-20 10:00:22 -06:00

183 lines
5.3 KiB
Python

"""
Manejadores globales de excepciones para FastAPI
"""
import logging
from typing import Any, Dict
from fastapi import Request, status
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from .exceptions import BaseAPIException
logger = logging.getLogger(__name__)
async def base_exception_handler(
request: Request,
exc: BaseAPIException,
) -> JSONResponse:
"""
Manejador para todas las excepciones personalizadas de la API
"""
logger.warning(
f"API Exception: {exc.error_code} - {exc.message}",
extra={
"path": request.url.path,
"method": request.method,
"status_code": exc.status_code,
},
)
# Log detailed errors if they exist
if hasattr(exc, "errors") and exc.errors:
logger.warning(f"Validation errors details: {exc.errors}")
return JSONResponse(
status_code=exc.status_code,
content=jsonable_encoder(exc.to_dict()),
)
async def validation_exception_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
"""
Manejador para errores de validación de Pydantic/FastAPI
"""
errors = []
for error in exc.errors():
field = ".".join(str(loc) for loc in error["loc"] if loc != "body")
errors.append(
{
"field": field,
"message": error["msg"],
"type": error["type"],
}
)
logger.warning(
f"Validation Error en {request.url.path}",
extra={"errors": errors},
)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"error": "VALIDATION_ERROR",
"message": "Error de validación en los datos recibidos",
"status_code": status.HTTP_422_UNPROCESSABLE_ENTITY,
"errors": errors,
},
)
async def integrity_error_handler(
request: Request,
exc: IntegrityError,
) -> JSONResponse:
"""
Manejador para errores de integridad de la base de datos
"""
logger.error(
f"Database Integrity Error: {str(exc.orig)}",
extra={
"path": request.url.path,
"method": request.method,
},
)
orig_msg = str(exc.orig).lower()
# Check for unique/duplicate key violations (English and Spanish)
if any(kw in orig_msg for kw in ["unique constraint", "duplicate key", "duplicada", "unicidad", "ya existe"]):
error_message = "El registro ya existe. Verifica los campos únicos (Año, Aduana, Patente, Número, etc.)."
# Check for foreign key violations (English and Spanish)
elif any(kw in orig_msg for kw in ["foreign key", "foránea", "referencia"]):
error_message = "Referencia inválida a otro registro. Verifica las categorías y catálogos seleccionados."
# Check for not null violations (English and Spanish)
elif any(kw in orig_msg for kw in ["not null", "no nulo", "valor nulo"]):
error_message = "Falta un campo requerido. Asegúrate de llenar todos los datos obligatorios."
else:
error_message = "Error de integridad en la base de datos"
return JSONResponse(
status_code=status.HTTP_409_CONFLICT,
content={
"error": "DATABASE_INTEGRITY_ERROR",
"message": error_message,
"status_code": status.HTTP_409_CONFLICT,
},
)
async def sqlalchemy_error_handler(
request: Request,
exc: SQLAlchemyError,
) -> JSONResponse:
"""
Manejador para errores generales de SQLAlchemy
"""
logger.error(
f"Database Error: {str(exc)}",
extra={
"path": request.url.path,
"method": request.method,
},
exc_info=True,
)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error": "DATABASE_ERROR",
"message": "Error en la operación de base de datos",
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
},
)
async def general_exception_handler(
request: Request,
exc: Exception,
) -> JSONResponse:
"""
Manejador para excepciones no capturadas
"""
logger.error(
f"Unhandled Exception: {str(exc)}",
extra={
"path": request.url.path,
"method": request.method,
},
exc_info=True,
)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error": "INTERNAL_SERVER_ERROR",
"message": "Error interno del servidor",
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
},
)
def register_exception_handlers(app) -> None:
"""
Registra todos los manejadores de excepciones en la aplicación FastAPI
Args:
app: Instancia de FastAPI
"""
app.add_exception_handler(BaseAPIException, base_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(IntegrityError, integrity_error_handler)
app.add_exception_handler(SQLAlchemyError, sqlalchemy_error_handler)
app.add_exception_handler(Exception, general_exception_handler)