"""PM error envelope — same contract as the ERP: {"error": {code, message, field}}.""" from fastapi import FastAPI, Request from fastapi.responses import JSONResponse class AppError(Exception): status_code = 400 code = "BAD_REQUEST" def __init__(self, message: str, field: str | None = None, code: str | None = None): super().__init__(message) self.message = message self.field = field if code: self.code = code @property def body(self) -> dict: error = {"code": self.code, "message": self.message} if self.field is not None: error["field"] = self.field return {"error": error} class AuthError(AppError): status_code, code = 401, "UNAUTHORIZED" class ValidationError(AppError): status_code, code = 400, "VALIDATION_ERROR" class ForbiddenError(AppError): status_code, code = 403, "FORBIDDEN" class NotFoundError(AppError): status_code, code = 404, "NOT_FOUND" class ConflictError(AppError): status_code, code = 409, "CONFLICT" class UpstreamError(AppError): """Upstream service unreachable / errored.""" status_code, code = 503, "UPSTREAM_UNAVAILABLE" def install_error_handlers(app: FastAPI) -> None: @app.exception_handler(AppError) async def _app_error(_: Request, exc: AppError) -> JSONResponse: return JSONResponse(status_code=exc.status_code, content=exc.body)