documentation
Kohinos v3 Doc générée le commit 8d60b1904

Bounded contexts & frontières de service

Kohinos n'est pas un monolithe homogène : c'est une fédération de domaines (Wallet/eMLc, SEPA, SSA, Adhésion, Paiement externe, BOffice) qui partagent une base de données et un kernel Symfony, mais qui doivent rester isolés logiquement. Cette séparation n'est pas un luxe : c'est une condition de l'auto-hébergement (chaque MLCC n'utilise pas tous les contexts), du dev solo (un humain ne tient pas tout en tête) et surtout de l'intégrité financière (un bug dans HelloAsso ne doit pas pouvoir corrompre la chaîne d'Operation).

Carte des contexts

flowchart TB
  subgraph Core["Core monétaire (eMLc)"]
    WALLET["Wallet / Comptes<br/>Account*, Flux, Operation"]
  end

  subgraph Subs["Adhésion & Cotisation"]
    ADH["Adherent / Prestataire<br/>MlcSubscriptionService"]
  end

  subgraph Recur["Récurrence & SEPA"]
    SEPA["SepaProcessingService<br/>SepaMandate, Recurring*Request"]
  end

  subgraph SSA["SSA / Solidoume"]
    SSAFUND["SsaOrchestrator<br/>SolidoumeParameter, Strategy"]
  end

  subgraph Pay["Paiement externe"]
    HA["HelloAssoPaymentService<br/>PayZen (legacy)<br/>PendingPaymentData"]
  end

  subgraph BO["BOffice / SAV"]
    BOFF["BofficeApiClient<br/>TicketFix*"]
  end

  subgraph Notif["Notifications (transverse)"]
    MAIL["FluxNotificationService<br/>SepaNotificationService"]
  end

  ADH -- "création Account*" --> WALLET
  HA  -- "crédit eMLc<br/>(AchatMonnaie)" --> WALLET
  SEPA -- "Achat récurrent / Cotisation" --> WALLET
  SEPA -- "déclenche subscriptions" --> ADH
  SSAFUND -- "TransactionAdherent*Ssa<br/>prélèvement + redistribution" --> WALLET
  SSAFUND -- "cotisation participant" --> SEPA
  BOFF -. "lecture seule (API)" .-> WALLET

  WALLET -- "FluxEvent::CREATED" --> MAIL
  SEPA   -- "MLCEvents::SEPA_*"   --> MAIL
  SSAFUND-- "SsaExecutionResult"  --> MAIL
  HA     -- "MLCEvents::PAYMENT_*" --> MAIL
    
Six contexts métier + un context transverse (notifications). Les arêtes pleines représentent des appels de service ; les arêtes pointillées, un accès en lecture via API. Les notifications transitent uniquement par events.

Tableau récapitulatif

Pour chaque context : où vit son code, ce qu'il possède en propre, ce qu'il publie en événements et de qui il dépend / qui dépend de lui.

Context Namespaces Entités possédées Events publiés Dépendances entrantes Dépendances sortantes
Wallet / eMLc App\Entity\(Account*|Flux*|Operation*|Transaction*)
App\Repository\(Account*|Flux*|Operation*)Repository
AccountAdherent, AccountPrestataire, AccountComptoir, AccountSiege, AccountGroupe, Flux, Operation*, AchatMonnaie, Retrait, Change, Don, Transaction* FluxEvent::CREATED,
MLCEvents::FLUX_PERSISTED
SEPA, SSA, Adhésion, Paiement externe, BOffice Notifications (event)
Adhésion / Cotisation App\Service\Subscription\*
App\Entity\(Adherent|Prestataire|Cotisation*)
Adherent, Prestataire, Cotisation*, CotisationInfos, RecurringMlcSubscriptionRequest UserEvent::REGISTERED,
MLCEvents::COTISATION_PAID
SSA (cotisation participant), SEPA (subscription récurrente) Wallet (création Account*), Notifications
SEPA / Récurrence App\Service\Sepa\*
App\Service\Purchase\RecurringPurchaseService
App\Entity\(SepaMandate|Recurring*Request|IbanChangeRequest)
SepaMandate, RecurringPurchaseRequest, RecurringMlcSubscriptionRequest, RecurringSsaSubscriptionRequest, IbanChangeRequest MLCEvents::SEPA_MANDATE_VALIDATED,
MLCEvents::SEPA_PROCESSED,
MLCEvents::SEPA_REVOKED
SSA (cotisation participant), Adhésion (cotisation annuelle) Wallet (création AchatMonnaie / Cotisation), Notifications
SSA / SSA App\Service\Ssa\*
App\Entity\(SolidoumeParameter|SolidoumeItem|PrestataireSolidoumeParameter|CotisationSsaAdherent|TransactionAdherent*Ssa)
SolidoumeParameter, SolidoumeItem, PrestataireSolidoumeParameter, CotisationSsaAdherent, TransactionAdherentAdherentSsa, TransactionAdherentEmlctoSsa MLCEvents::SSA_EXECUTED,
MLCEvents::SSA_PARTICIPANT_DROPPED
Aucune (point d'entrée : commande kohinos:ssa:execute) Wallet (Transaction*), SEPA (cotisation participant), Notifications
Paiement externe App\Service\HelloAssoPaymentService
App\Service\HelloAssoWebhookAuthenticator
App\Service\PaymentDataManager
App\Entity\(HelloAsso|Payment|PaymentToken|PendingPaymentData)
HelloAsso, Payment, PaymentToken, PendingPaymentData MLCEvents::PAYMENT_RECEIVED,
MLCEvents::PAYMENT_FAILED
Adhésion (paiement cotisation), Wallet (achat eMLc) Wallet (création AchatMonnaie), Notifications
BOffice / SAV App\Service\BofficeApiClient
App\Entity\(TicketFix|TicketFixDestroy|TicketFixPrint)
TicketFix, TicketFixDestroy, TicketFixPrint — Wallet (lecture seule via API REST) Notifications
Notifications (transverse) App\Service\(FluxNotificationService|SepaNotificationService|CotisationNotificationService|RegistrationNotificationService|UserNotificationPreferenceService)
App\EventListener\*
— — Tous via MLCEvents, FluxEvent, UserEvent symfony/mailer (SMTP self-host)

Règles de communication entre contexts

Une fois la carte tracée, les règles deviennent simples (et appliquées par revue de PR / PHPStan quand possible) :

  1. Effets de bord = event dispatcher. Un context qui veut signaler un événement (cotisation payée, mandat validé, prélèvement exécuté) publie un event de App\Event\MLCEvents ou App\Event\FluxEvent. Il n'appelle jamais directement un service d'un autre context pour notifier.
  2. Appels directs autorisés seulement pour les opérations financières atomiques. SEPA → Wallet pour créer un AchatMonnaie reste un appel synchrone : la consistance financière exige la même transaction Doctrine, donc le même thread d'exécution.
  3. Pas de dépendance circulaire entre contexts. SSA peut appeler Wallet, mais Wallet ne connaît pas SSA. La rupture de cycle se vérifie sur les use de chaque App\Service\*.
  4. Un repository ne sort jamais de son context. SolidoumeItemRepository n'est injecté que dans App\Service\Ssa\*. Si SEPA a besoin d'une info SSA, il passe par un service du context SSA (Application Service), jamais par le repo directement.
  5. Les entités possédées par un context ne sont pas modifiées ailleurs. Personne en dehors de SEPA ne fait $mandate->setStatus(...). Personne en dehors du Wallet ne fait $account->applyOperation(...).
  6. Le BOffice est en lecture seule sur le Wallet. L'accès se fait via BofficeApiClient (HTTP), pas via injection de repos. Cela garantit qu'un futur déploiement BOffice séparé reste possible (souveraineté, sobriété).
⚠ Règle dure — DQL / QueryBuilder

Aucun $em->createQuery(...) ou QueryBuilder hors d'un repository du context propriétaire. Un Controller, un Admin Sonata, un Service applicatif ou une Command qui a besoin d'une requête métier appelle une méthode find* nommée du repo concerné. Cette règle est non négociable : elle conditionne la possibilité de refactorer un context sans casser les autres.

Anti-patterns observés (à corriger en priorité)

Où ranger un nouveau bout de code ?

Arbre de décision rapide :

1. Le code touche un solde, une Operation, un Flux ?
   → context Wallet. Service dans src/Service/, méthode find* dans Repository/.

2. Le code parle de mandat, d'IBAN, de prélèvement récurrent ?
   → context SEPA. src/Service/Sepa/*.

3. Le code parle de fonds Solidoume, cotisation SSA, redistribution ?
   → context SSA. src/Service/Ssa/* (et Strategy/ pour la stratégie).

4. Le code parle d'inscription, cotisation annuelle MLC, statut adhérent/prestataire ?
   → context Adhésion. src/Service/Subscription/*.

5. Le code parle de HelloAsso / PayZen / webhook / token de paiement ?
   → context Paiement externe. src/Service/HelloAsso*.php, PaymentDataManager.

6. Le code parle de ticket SAV ?
   → context BOffice. src/Service/BofficeApiClient.

7. Le code envoie un email ?
   → context Notifications. Écouter un event, ne pas appeler un context métier.

Évolutions visées