Files
Aegis/backend/app/domain/errors.py
Kitos 611e10620e
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
refactor(domain): introduce domain exceptions boundary
- Create domain/errors.py as canonical error hierarchy: DomainError, InvalidStateTransition, PermissionViolation, BusinessRuleViolation, EntityNotFoundError, DuplicateEntityError

- InvalidOperationError now inherits from BusinessRuleViolation for semantic consistency

- Convert domain/exceptions.py to backward-compatible re-export shim with legacy aliases (DomainException, InvalidTransitionError, AuthorizationError)

- Update error_handler.py to import from domain/errors.py and map all new error types

- Update main.py to register DomainError (new base) as the exception handler root
2026-02-18 13:44:47 +01:00

97 lines
3.3 KiB
Python

"""Canonical domain error hierarchy for Aegis.
Every service-layer error should be a subclass of :class:`DomainError`.
The global exception handler in ``app.middleware.error_handler`` maps
each concrete subclass to an appropriate HTTP status code so that
services never depend on FastAPI.
Existing code that imports from ``app.domain.exceptions`` continues to
work — that module re-exports everything defined here.
"""
from __future__ import annotations
class DomainError(Exception):
"""Base for all domain errors."""
def __init__(self, message: str, *, code: str = "DOMAIN_ERROR") -> None:
self.message = message
self.code = code
super().__init__(message)
# ── Entity lifecycle ──────────────────────────────────────────────────
class EntityNotFoundError(DomainError):
"""A requested entity does not exist."""
def __init__(self, entity: str, identifier: str) -> None:
super().__init__(f"{entity} not found: {identifier}", code="NOT_FOUND")
self.entity = entity
self.identifier = identifier
class DuplicateEntityError(DomainError):
"""Creating an entity that already exists."""
def __init__(self, entity: str, field: str, value: str) -> None:
super().__init__(
f"{entity} with {field}='{value}' already exists",
code="DUPLICATE",
)
# ── State machine ────────────────────────────────────────────────────
class InvalidStateTransition(DomainError):
"""A state-machine transition is not allowed."""
def __init__(
self,
current_state: str,
target_state: str,
valid_transitions: list[str] | None = None,
) -> None:
msg = f"Cannot transition from '{current_state}' to '{target_state}'"
if valid_transitions:
msg += f". Valid transitions: {valid_transitions}"
super().__init__(msg, code="INVALID_TRANSITION")
self.current_state = current_state
self.target_state = target_state
self.valid_transitions = valid_transitions or []
# ── Business rules ────────────────────────────────────────────────────
class BusinessRuleViolation(DomainError):
"""An operation violates a business invariant."""
def __init__(self, message: str) -> None:
super().__init__(message, code="BUSINESS_RULE_VIOLATION")
class InvalidOperationError(BusinessRuleViolation):
"""An operation is invalid in the current context.
Kept for backward compatibility; new code should prefer
:class:`BusinessRuleViolation` directly.
"""
def __init__(self, message: str) -> None:
super().__init__(message)
self.code = "INVALID_OPERATION"
# ── Authorization ────────────────────────────────────────────────────
class PermissionViolation(DomainError):
"""The user lacks permissions for an action."""
def __init__(self, message: str = "Insufficient permissions") -> None:
super().__init__(message, code="FORBIDDEN")