================================== Shared Kernel — Couche Domaine ================================== Le Shared Kernel contient le code partagé entre tous les bounded contexts. La couche domaine définit les briques DDD fondamentales. Module : ``src.share_kernel.domain`` Entités de base =============== .. module:: src.share_kernel.domain.base_entity :synopsis: Classe de base pour les entités DDD .. class:: Entity Classe de base pour toutes les entités du domaine. Chaque entité possède un identifiant UUID unique. L'égalité entre entités est basée sur l'identité (``id``), pas sur les attributs. .. attribute:: id :type: uuid.UUID Identifiant unique de l'entité (UUID4). **Exemple :** .. code-block:: python from src.share_kernel.domain import Entity import uuid class Product(Entity): def __init__(self, name: str, price: float): super().__init__(id=uuid.uuid4()) self.name = name self.price = price p1 = Product("Widget", 9.99) p2 = Product("Widget", 9.99) assert p1 != p2 # Identités différentes Agrégats (Aggregate Root) ========================= .. module:: src.share_kernel.domain.base_aggregate :synopsis: Racine d'agrégat avec gestion des événements domaine .. class:: AggregateRoot(Entity) Étend ``Entity`` avec la gestion des événements domaine. Les agrégats collectent les événements via ``_record_event()`` et les exposent via ``pull_domain_events()`` pour publication par l'infrastructure. .. method:: _record_event(event: DomainEvent) -> None Enregistre un événement domaine dans la file interne de l'agrégat. .. method:: pull_domain_events() -> list[DomainEvent] Retourne et vide la liste des événements enregistrés. Appelée par l'infrastructure après la persistance. **Exemple :** .. code-block:: python from src.share_kernel.domain import AggregateRoot, DomainEvent from dataclasses import dataclass @dataclass(frozen=True) class OrderPlaced(DomainEvent): event_type = "order.placed" order_id: str = "" total: float = 0.0 class Order(AggregateRoot): def place(self): self._record_event(OrderPlaced( order_id=str(self.id), total=self.total, )) Value Objects ============= .. module:: src.share_kernel.domain.base_value_object :synopsis: Classe de base pour les Value Objects immuables .. class:: ValueObject Frozen dataclass servant de base pour tous les Value Objects. Les VO sont définis par leurs attributs, sans identité propre. .. method:: __post_init__() -> None Hook de validation appelé automatiquement après l'initialisation. Les sous-classes surchargeant cette méthode pour valider leurs invariants. Value Objects disponibles ------------------------- .. module:: src.share_kernel.domain.value_objects .. class:: Address Adresse postale avec validation du code pays ISO 3166-1. :param street: Rue :param city: Ville :param postal_code: Code postal :param country_code: Code pays ISO 3166-1 alpha-2 (ex: ``"FR"``, ``"CA"``) :param state_or_region: État ou région (optionnel) .. code-block:: python from src.share_kernel.domain.value_objects import Address addr = Address( street="123 Rue Principale", city="Montréal", postal_code="H2X 1Y4", country_code="CA", state_or_region="QC", ) .. class:: Email Adresse email avec normalisation automatique (minuscules, trim). :param value: L'adresse email .. attribute:: domain :type: str Retourne le domaine de l'email (après le ``@``). .. code-block:: python from src.share_kernel.domain.value_objects import Email email = Email(value="User@Example.COM") assert email.value == "user@example.com" assert email.domain == "example.com" .. class:: PhoneNumber Numéro de téléphone au format E.164 (``+[code pays][numéro]``). :param value: Le numéro au format E.164 .. code-block:: python from src.share_kernel.domain.value_objects import PhoneNumber phone = PhoneNumber(value="+14155551234") .. class:: Period Période temporelle avec validation ``end_date >= start_date``. :param start_date: Date de début :param end_date: Date de fin .. method:: contains(date) -> bool Vérifie si une date est dans la période. .. method:: overlaps(other: Period) -> bool Vérifie si deux périodes se chevauchent. .. method:: duration_days() -> int Retourne la durée en jours. Événements Domaine ================== .. module:: src.share_kernel.domain.domain_event :synopsis: Infrastructure des événements domaine .. class:: DomainEvent Classe de base pour tous les événements domaine. Les événements sont des dataclasses gelées (frozen) et sérialisables en JSON. .. attribute:: event_id :type: str Identifiant unique pour l'idempotence. .. attribute:: correlation_id :type: str Lie la requête HTTP d'origine aux tâches Celery en aval. .. attribute:: occurred_at :type: str Horodatage UTC ISO 8601. .. attribute:: tenant_id :type: str | None Contexte tenant (si applicable). .. attribute:: event_type :type: ClassVar[str] Nom lisible de l'événement. Défini par les sous-classes. .. method:: to_dict() -> dict[str, Any] Sérialise l'événement en dictionnaire JSON-compatible. .. classmethod:: from_dict(data: dict) -> DomainEvent Désérialise un événement depuis un dictionnaire. Utilise le registre global pour trouver la bonne classe. .. function:: register_event(event_cls: type[DomainEvent]) -> type[DomainEvent] Décorateur de classe pour enregistrer un événement dans le registre global. .. code-block:: python from src.share_kernel.domain.domain_event import DomainEvent, register_event from dataclasses import dataclass @register_event @dataclass(frozen=True) class InvoiceCreated(DomainEvent): event_type = "invoice.created" invoice_id: str = "" amount: float = 0.0 .. function:: get_event_class(event_type: str) -> type[DomainEvent] | None Recherche une classe d'événement enregistrée par son ``event_type``. Exceptions ========== .. module:: src.share_kernel.domain.exceptions :synopsis: Hiérarchie d'exceptions domaine et multi-tenant Exceptions DDD de base ---------------------- .. class:: DomainException(Exception) Base pour toutes les exceptions domaine. .. class:: EntityNotFoundException(DomainException) Entité non trouvée par son identifiant. :param entity_name: Nom de l'entité :param entity_id: Identifiant recherché .. class:: BusinessRuleViolationException(DomainException) Règle métier violée. .. class:: InvalidValueObjectException(DomainException) Value Object avec valeur invalide. .. class:: ConfigurationError(DomainException) Configuration invalide de la bibliothèque. Exceptions Multi-tenant ----------------------- .. class:: DjangoMultitenantError(Exception) Racine pour toutes les erreurs multi-tenant. .. class:: TenantError(DjangoMultitenantError) Base pour les erreurs liées aux tenants. .. class:: TenantNotFoundError(TenantError) Tenant introuvable. .. class:: TenantInactiveError(TenantError) Accès à un tenant suspendu ou archivé. .. class:: TenantSchemaError(TenantError) Erreur de provisionnement ou migration de schéma. .. class:: NoTenantContextError(TenantError) Opération nécessitant un contexte tenant, mais aucun n'est défini. .. class:: AuthorizationError(DjangoMultitenantError) Base pour les erreurs d'autorisation. .. class:: AccessDeniedError(AuthorizationError) Accès refusé par l'évaluation ABAC. :param reason: Raison du refus :param policy_id: Identifiant de la politique (optionnel) .. class:: ApprovalRequiredError(AuthorizationError) Action nécessitant une approbation. Le PEP intercepte cette exception et retourne HTTP 202. :param approval_request_id: ID de la demande d'approbation créée :param message: Message descriptif .. class:: MembershipError(DjangoMultitenantError) Base pour les erreurs de membership. .. class:: MembershipNotFoundError(MembershipError) L'utilisateur n'a pas de membership active dans le tenant courant. Interfaces (ABC) ================= .. module:: src.share_kernel.domain.interfaces :synopsis: Interfaces abstraites partagées .. class:: TenantIsolationBackend(ABC) Interface pour les stratégies d'isolation des tenants. .. method:: activate_tenant(tenant, connection) -> None :abstractmethod: Configure la connexion DB pour le tenant actif. .. method:: deactivate_tenant(connection) -> None :abstractmethod: Restaure la connexion DB à son état par défaut. .. method:: create_tenant_storage(tenant) -> None :abstractmethod: Provisionne le stockage pour un nouveau tenant. .. method:: destroy_tenant_storage(tenant) -> None :abstractmethod: Détruit le stockage d'un tenant. .. method:: migrate_tenant(tenant) -> None :abstractmethod: Applique les migrations pour un tenant spécifique. .. class:: ConditionEngine(ABC) Interface pour les moteurs d'évaluation de conditions ABAC. .. method:: evaluate(conditions: dict, attributes: dict) -> bool :abstractmethod: Évalue les conditions par rapport aux attributs fournis. .. class:: AttributeProvider(ABC) Interface pour les fournisseurs d'attributs personnalisés utilisés par le PIP. .. method:: get_attributes(context: dict) -> dict :abstractmethod: Retourne un dictionnaire d'attributs supplémentaires. .. class:: NotificationChannel(ABC) Interface pour les canaux de notification des approbations. .. method:: send_notification(recipients, subject, message, context=None) -> None :abstractmethod: Envoie une notification aux destinataires. Enums et Value Objects Multi-tenant ==================================== .. module:: src.share_kernel.domain.multitenant_value_objects :synopsis: Enums et types partagés entre les bounded contexts .. class:: TenantId Wrapper typé autour d'un UUID de tenant. .. class:: UserId Wrapper typé autour d'un UUID d'utilisateur. .. class:: PolicyId Wrapper typé autour d'un UUID de politique. .. class:: PolicyEffect(StrEnum) - ``ALLOW`` : La politique autorise l'accès - ``DENY`` : La politique refuse l'accès .. class:: ActionType(StrEnum) Actions ABAC standard mappées depuis les actions DRF : - ``LIST``, ``READ``, ``CREATE``, ``UPDATE``, ``DELETE``, ``WILDCARD`` .. class:: TenantStatus(StrEnum) - ``ACTIVE`` : Tenant actif - ``SUSPENDED`` : Tenant suspendu - ``ARCHIVED`` : Tenant archivé .. class:: MembershipStatus(StrEnum) - ``ACTIVE`` : Membership active - ``INVITED`` : Invitation en attente - ``SUSPENDED`` : Membership suspendue .. class:: ApprovalStatus(StrEnum) - ``PENDING``, ``APPROVED``, ``REJECTED``, ``EXPIRED``, ``CANCELLED``, ``EXECUTION_FAILED`` .. class:: PolicyScope(StrEnum) - ``GLOBAL`` : Politique globale (tous les tenants) - ``TENANT`` : Politique spécifique au tenant .. class:: IsolationBackendType(StrEnum) - ``SCHEMA``, ``RLS``, ``SHARED_FK`` .. data:: DRF_ACTION_MAPPING Mapping des actions DRF vers les actions ABAC : .. code-block:: python { "list": "list", "retrieve": "read", "create": "create", "update": "update", "partial_update": "update", "destroy": "delete", }