"""Application error type + standardized error envelope. All errors are returned as: {"error": {"code", "message", "field"}} (contracts: expert-opinion-p0.md «General Shared Contracts»). """ class AppError(Exception): """Base error carrying an HTTP status and the contract error envelope.""" def __init__(self, status_code: int, code: str, message: str, field: str | None = None): super().__init__(message) self.status_code = status_code self.code = code self.message = message self.field = field @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 ValidationError(AppError): """400 VALIDATION_ERROR — bad input (types, business validation).""" def __init__(self, message: str, field: str | None = None): super().__init__(400, "VALIDATION_ERROR", message, field) class NotFoundError(AppError): """404 NOT_FOUND.""" def __init__(self, message: str = "رکورد یافت نشد", field: str | None = None): super().__init__(404, "NOT_FOUND", message, field) class ConflictError(AppError): """409 CONFLICT — duplicates, invalid state transitions, protected deletes.""" def __init__(self, message: str, field: str | None = None): super().__init__(409, "CONFLICT", message, field) class ForbiddenError(AppError): """403 FORBIDDEN — role / ownership checks.""" def __init__(self, message: str, field: str | None = None): super().__init__(403, "FORBIDDEN", message, field)