Aller au contenu

Migration vers les Bounded Contexts — plan de route

Statut : Monolithe modulaire — 13 modules métier + Shared/ (VO + infra transverse mutualisée, ex-Platform). Métadonnées fichier via StoredFile (composition) ; notifications via enveloppe Notification + 7 tables filles par kind (composition notification_id, plus de JSON polymorphe inline). Périmètre : backend/src/ + frontend/src/ Principe : migration incrémentale (strangler), aucun big bang. 1859 fichiers PHP, 459 fichiers front.

Objectif

Découper le monolithe en contextes métier pour borner le rayon d'impact des modifications, et rendre les frontières vérifiables automatiquement plutôt que dépendantes de la discipline.

Principe directeur : découper par métier, pas par acteur

L'idée initiale (Formateur / OrganisationStaff / Public / Shared) a été écartée. Un Bounded Context se définit par une frontière de langage et la propriété exclusive de ses données ; or les trois acteurs partagent les mêmes agrégats.

Preuve : App\Domain\Entity\Trainer est importé dans ~185 fichiers, dont des handlers Application/Handler/Staff/ et des providers Api/State/Public/. Domain\Entity\Invoicing\Invoice référence à la fois Trainer, Staff et Organization. Un découpage par acteur produirait un Shared/ absorbant ~80 % du domaine, avec trois coquilles fines autour.

Test à appliquer mot par mot : « Facture » signifie la même chose pour le formateur (émise) et pour le staff (reçue) → un contexte Facturation avec deux read models, pas deux contextes.

L'axe acteur est conservé, mais il descend d'un étage : il vit dans Infrastructure/Api/{Trainer,Staff,Public}/ de chaque contexte (back) et dans views/{trainer,staff,public}/ (front). Les URLs ne changent pas.


Phase 1 — Corriger les violations DDD

Prérequis à la migration : on ne déplace rien avant que les règles soient outillées et que les agrégats protègent leurs invariants.

1.0 Outillage des frontières (bloquant)

  • [x] Installer Deptrac (composer require --dev qossmic/deptrac) et déclarer les 3 couches actuelles : Domain ne dépend ni d'Application ni d'Infrastructure.
  • [x] Générer une baseline sur l'existant (28 violations Application→Infrastructure DTOs), brancher sur la CI (non bloquant d'abord, puis bloquant).
  • [x] Ajouter la commande au Makefile (make lint-backend-deptrac) et à .cursor/rules/linters-keeper.mdc.

Aucun outil de contrôle de dépendances n'existe aujourd'hui : PHPStan ne vérifie pas les dépendances entre couches. Les règles DDD ne sont garanties par rien d'exécutable.

1.1 État des lieux — ce qui est déjà sain

À préserver, ne pas casser pendant la migration :

Point Vérification
Sens des dépendances 0 import App\Application\ ou App\Infrastructure\ dans tout Domain/
Étanchéité de l'Application 0 EntityManagerInterface, QueryBuilder ou Doctrine\ORM dans tout Application/
Vrais ports App\Domain\Event\EventDispatcherInterface, TrainerOgImageGeneratorInterface, SearchOrganizationInterface
Verrou pessimiste abstrait InvoiceRepositoryInterface::runWithPessimisticWriteLock()
API Platform Resource / Request / Response / Mapper / State en Infrastructure, DTO dédiés (pas d'entité sérialisée)

Conclusion de l'audit : l'architecture hexagonale en couches est propre et respectée. Ce qui manque, c'est le domaine lui‑même (agrégats anémiques). C'est la différence entre « architecture en couches » et « DDD ».

1.2 Remonter les invariants dans les agrégats (priorité n°1)

Invoice expose 54 méthodes publiques dont ~48 getters/setters. Seul comportement réel : regeneratePublicId(), getEffectiveStatus() et les 3 calculs de totaux.

Conséquence, dans Application/Command/Trainer/Invoicing/FinalizeCreditNoteCommand.php : les 4 invariants de l'avoir (brouillon, ≥ 1 ligne, motif obligatoire, montant > 0) vivent dans la Command. setStatus(), setCode() et setFinalizedAt() étant publics, n'importe quel code peut contourner les 4 règles.

Le risque n'est pas théorique : le statut d'une facture est modifié depuis au moins 6 points d'entréeFinalizeCreditNoteCommand, MarkInvoicePaidCommand, SendInvoiceCommand, UpdateInvoiceCommand, GenerateInvoiceFromActivityReportCommand, FinalizeInvoiceProcessor — chacun responsable de revérifier les mêmes règles sans que rien ne l'y oblige.

Périmètre volontairement restreint — seules les entités portant une machine à états :

  • [x] Invoicefinalize(), markAsPaid(), markAsSent(), canBeEdited() ; setters de cycle de vie en privé
  • [x] CreditNotefinalize() portant les 4 contrôles
  • [ ] ActivityReport
  • [ ] ReservationBatch
  • [ ] CalendarConfigurator

Ne pas généraliser. Les ~70 autres entités (TrainerEducation, TrainerSkill, TrainerExperience, référentiels…) sont des données : getters/setters y sont la bonne réponse, y ajouter des invariants serait de la cérémonie.

1.3 Figer l'émetteur et le client des documents de facturation

Ce n'est pas un choix d'architecture mais une obligation légale : une facture émise est immuable.

Bug actuel : Application/Service/Invoicing/BillingDocumentTemplatePdfDataFactory.php (lignes 38‑46) lit l'émetteur en direct via $invoice->getTrainer()->getCompany(). Si un formateur change de SIRET, d'adresse ou de raison sociale, le PDF de toutes ses factures déjà finalisées change rétroactivement.

  • [x] VO IssuerSnapshot + ClientSnapshot, écrits une fois à la finalisation (à côté de document_data), jamais resynchronisés
  • [x] Appliquer à Invoice, CreditNote, ActivityReport
  • [x] Invoice::finalize() devient le point de passage obligé (dépend de 1.2)

Distinction à garder en tête : un snapshot immuable (jamais resynchronisé, source de vérité locale après émission) n'a aucun coût de maintenance. Un cache synchronisé (nom d'affichage dupliqué à maintenir) en a un très élevé — à éviter. Le pattern est déjà utilisé sans être nommé : InvoiceLineItem copie déjà interventionDate et className.

1.4 Nettoyer le code mort

  • [x] Supprimer Domain/Entity/Invoice.php (legacy, table invoice, 4 champs, anémique)
  • [x] Supprimer Notification/InvoiceNotification + son repository et son interface : jamais instancié dans src/, et seule référence vivante vers l'Invoice legacy
  • [x] Corriger Organization::$invoices et Trainer::$invoices : ils pointent vers l'entité legacy, donc getInvoices() renvoie silencieusement une collection vide au lieu des factures de invoicing_invoice

Piège à retardement : le prochain appel de bonne foi à getInvoices() obtiendra zéro résultat, sans aucune erreur.

1.5 Corriger les incohérences de convention

Écart Constat Cible
Exceptions HTTP dans le Domain 8 exceptions de Domain/Exception/Invoicing/ étendent BadRequestHttpException / NotFoundHttpException / AccessDeniedHttpException ; 2 étendent \DomainException ; ~90 étendent RuntimeException Une seule convention (RuntimeException / DomainException), traduction HTTP dans un EventSubscriber d'Infrastructure
Validation framework dans une entité Invoice porte #[Assert\Expression] sur dueDate >= issueDate Invariant du constructeur / setter
Autorisation à deux endroits Contrôle inline dans FinalizeCreditNoteCommand vs MessageVoter ailleurs Voter pour l'autorisation, Command pour le métier
DTO à trois adresses Domain/ValueObject/**/*Dto, Domain/ValueObject/**/*ReadModel, Application/Dto/ VO métier dans Domain/ValueObject/, transport et lecture dans Application/Dto/ ou Application/ReadModel/
Repositories God InvoiceRepositoryInterface = 27 méthodes, InvoiceRepository = 1049 lignes Interface d'agrégat (find/save/lock, 4‑5 méthodes) + finders de lecture dédiés (InvoiceDashboardQueryInterface, StaffInvoiceListQueryInterface)

Bénéfice concret des exceptions HTTP au‑delà de la pureté : en l'état, ces 8 exceptions ne peuvent pas être levées depuis un worker Messenger ou une commande CLI sans embarquer HttpKernel.

1.6 Clarifier le nommage CQRS

FinalizeCreditNoteCommand n'est pas une Command au sens CQRS (DTO immuable d'intention) mais un service applicatif avec dépendances injectées et une méthode process(). La convention réelle est : Command = cas d'usage d'écriture, Handler = cas d'usage de lecture. La frontière fuit déjà (Application/Command/Trainer/Calendar/TrainerCalendarOAuthApplicationService.php).

  • [ ] Trancher : renommer (Command / Query, ou UseCase/Write / UseCase/Read) ou documenter la convention telle quelle dans .doc/backend/ARCHITECTURE.md
  • [ ] Ne pas introduire un bus Messenger pour « faire du CQRS » : aucun besoin

Phase 2 — Migration BC backend

2.1 Architecture cible

backend/src/
├── Shared/              # mutualisé : Application, Domain (ports, VO, Exception), Infrastructure
│
├── Identity/            # NOYAU — feuille du graphe, ne dépend d'aucun contexte métier
├── Notification/        # feuille aussi (références polymorphes)
│
├── Catalog/             # référentiels : Program, Module, SkillReference
├── Directory/           # la relation formateur ↔ organisation
├── Profile/             # parcours + vitrine publique
├── Calendar/            # configurateurs, événements, disponibilités, connecteurs
├── Reservation/         # lots de réservation, partages de planning
├── Invoicing/           # facture, avoir, CRA, gabarits, entreprise
├── Compliance/          # Coffre-Fort, partages, journal d'accès
├── Messaging/
├── AccountLifecycle/    # RGPD : export, effacement, inactivité
└── Reporting/           # lecture seule, aucune entité propre

Structure interne d'un contexte :

Invoicing/
├── Domain/
│   ├── Entity/  ValueObject/  Enum/  Exception/  Event/  Repository/  Service/
│   └── Port/            # contrats sortants (InterventionEventPort, TrainerDirectoryPort)
├── Application/
│   ├── Command/  Handler/  Dto/  Service/
└── Infrastructure/
    ├── Api/
    │   ├── Trainer/     # ← axe acteur, surface formateur
    │   ├── Staff/       # ← surface organisation
    │   └── Public/      # ← surface non authentifiée
    ├── Orm/             # repositories Doctrine
    ├── Port/            # implémentations des ports
    └── Pdf/

2.2 Répartition des entités

Contexte Entités
Identity (noyau) User, Trainer, Staff, Organization, OrganizationMembership, UserOptin, RefreshToken, SecurityToken, PasswordResetRequest, MagicLinkRequest, StaffInvitationAssociatedTrainer
Catalog Program, Module, ProgramModuleOrganization, SkillReference
Directory TrainerOrganizationAssignment, TrainerOrganizationContact
Profile TrainerExperience, TrainerEducation, TrainerCertification, TrainerSkill, TrainerPublicContactRequest
Calendar CalendarConfigurator, CalendarConfiguratorAssignment, Event + InterventionEvent / PersonalEvent / NoteEvent / RecurringEvent / UnavailabilityEvent, TrainerAvailability{Configuration,Plan,Rule,Slot}, Process
Reservation ReservationBatch, ReservationBatchGroup, Reservation, TrainerPlanningShare{,Link,Contact}
Invoicing Invoice, InvoiceLineItem, InvoiceProduct, CreditNote, CreditNoteLineItem, ActivityReport, ActivityReportLineItem, BillingTemplate, TrainerBillingSettings, TrainerCompany, TrainerDocumentCompany, TrainerDocumentBillingAttachment
Compliance TrainerComplianceDocument, TrainerComplianceShare{,Link}, TrainerComplianceAccessLog, TrainerHonorabilityReference
Messaging Conversation, ConversationParticipant, Message
Notification Notification + sous-classes
AccountLifecycle AccountDataExport, AccountDeletionLog, AccountInactivityNotice
Reporting aucune

2.3 Graphe de dépendances (à traduire en règles Deptrac)

Shared              → rien (sous-couches internes)
Identity            → Shared
Notification        → Shared
Catalog             → + Identity
Directory           → + Identity, Catalog
Profile             → + Identity, Catalog
Calendar            → + Identity, Directory, Catalog
Reservation         → + Identity, Directory, Calendar
Invoicing           → + Identity, Directory, Calendar
Compliance          → + Identity, Directory
Messaging           → + Identity
AccountLifecycle    → + Identity
Reporting           → tous, en lecture seule

2.4 Règles de traversée des frontières

La règle centrale :

Seules Identity\Domain\Entity\{User, Trainer, Staff, Organization} traversent une frontière en tant qu'objets, en lecture seule. Tout le reste traverse par identifiant + port.

Corollaires :

  1. Liens unidirectionnels vers le noyau. Un ManyToOne descendant (Invoice → Trainer) est acceptable — c'est un Shared Kernel assumé. Un OneToMany inverse remontant (Trainer → Collection<Invoice>) ne l'est jamais : il transforme le noyau en hub et rend le graphe cyclique.
  2. Le noyau ne grossit pas. Si un contexte a besoin d'un champ sur Trainer, ce champ lui appartient : il déménage.
  3. Aucun contexte n'écrit dans le noyau. Pas d'appel de setter sur une entité du noyau depuis l'extérieur d'Identity.
  4. Shared/ ne contient aucune entité Doctrine. Uniquement VO sans identité (Address, PhoneNumber, montants), types temporels, identifiants (TrainerId, OrganizationId). Si Trainer finit dans Shared, le découpage perd tout intérêt.
  5. Les clés étrangères restent en base. Le découplage est une propriété du code, pas du schéma. On retire l'association du mapping Doctrine, on conserve la contrainte FOREIGN KEY.

Les 4 patterns de traversée, du moins au plus cher :

Pattern Quand Coût
Snapshot immuable La donnée doit être figée au moment de l'événement (facture) Nul (jamais resynchronisé)
Référence par identité Vrai lien vivant, hors noyau Faible schéma (colonne identique), moyen en code (perte des jointures DQL)
Port de lecture Besoin d'affichage cross-contexte Faible, si findManyByIds() et pas de boucle
Domain event Réaction en chaîne (RGPD, notifications) Faible, infra déjà en place

Correction d'une idée reçue : le passage en référence par identité ne change pas le schéma. Avant : trainer_id INT + FK. Après : trainer_id INT + FK. Seul le mapping PHP change. Le vrai coût est l'ergonomie et les requêtes, pas la base.

2.5 Découpage du Trainer God aggregate

De ~1080 lignes à ~250.

Champ actuel Destination
lastCraNumber Invoicing
slug, headline, bio, languages, specialite, isProfilePublic, isSearchIndexable, publicProfileErasedAt, publicProfileViewCount, publicProfileLastViewedAt, allow* Profile
showPublicAgenda Profile (opt-in) + Calendar (calcul free/busy)
responseRateTargetPercentPreference Reporting
Collections inverses ($invoices, $events, $programs, $modules, $ownedOrganizations, $reservationBatches…) Supprimées

Reste dans Identity : identité, contact, roles, accountStatus, onboarding.

2.6 Ordre d'exécution

Les étapes 1 à 4 ne génèrent aucune migration Doctrine et cassent déjà la majorité des cycles.

  • [ ] 1. Deptrac + baseline (fait en phase 1.0). Rien ne bouge.
  • [ ] 2. Supprimer les collections inverses sans cascade de Trainer (détail ci-dessous).
  • [ ] 3. Casser les 5 cycles restants (détail ci-dessous).
  • [ ] 4. Notification en références polymorphes (targetType + targetUuid + payload JSON) : supprime 6 liens inter-contextes d'un coup et fait de Notification une feuille.
  • [ ] 5. Créer Messaging/ en module pilote (petit, autonome) pour figer la convention de dossiers et le style de port.
  • [ ] 6. Extraire Compliance/ (frontière nette, entités dédiées).
  • [ ] 7. Extraire Invoicing/ (rendu possible par les snapshots de la phase 1.3).
  • [ ] 8. Extraire Calendar/ puis Reservation/ (les plus enchevêtrés, en dernier).
  • [ ] 9. Éclater Trainer vers Profile / Invoicing / Reporting.

Détail étape 2 — collections inverses de Trainer

Le côté inverse (mappedBy) n'a aucune colonne : suppression sans DDL, la FK reste en place.

Gratuites (aucun cascade, aucun orphanRemoval) : $invoices (legacy), $events, $ownedOrganizations, $programs, $modules, $experiences, $educations, $certifications.

Le code appelant passe de $trainer->getEvents() à $eventRepository->findByTrainer($trainer).

⚠️ Traiter à part celles qui portent cascade / orphanRemoval$billingSettings, $honorabilityReference, $skills, $availabilityPlans, $reservationBatches — car il y a de la logique de cycle de vie à déplacer. Cf. .cursor/rules/backend-rgpd-export-erasure.mdc, qui met déjà en garde contre ces cascades.

Détail étape 3 — les 5 cycles à casser

Dans chaque cas : garder le ManyToOne, jeter le OneToMany inverse. Aucune migration.

Cycle Sens conservé
TrainerOrganizationAssignmentCalendarConfigurator Calendar → Directory
InterventionEventInvoiceLineItem / ActivityReportLineItem Invoicing → Calendar
CalendarConfiguratorAssignmentInvoiceProduct Invoicing → Calendar
TrainerAvailabilityPlanTrainerPlanningShare Reservation → Calendar
ReservationInterventionEvent à trancher

2.7 Arbitrages à trancher avant de coder

  1. STI DocumentTrainerComplianceDocument (Compliance), TrainerDocumentCompany et TrainerDocumentBillingAttachment (Invoicing) partagent une classe de base. Une STI ne peut pas être à cheval sur deux contextes. Recommandation : casser la STI, une table par contexte (ces types ne partagent quasiment aucun comportement).
  2. STI Notification — voir étape 4 ci-dessus.
  3. Périmètre Reporting — soit module de lecture privilégié qui lit tout (simple, recommandé tant que l'équipe est réduite), soit chaque contexte expose ses read models.
  4. Domicile de Process — Calendar (usage principal) ou Platform.

2.8 Ce qui ne change pas

  • Une seule base, un seul EntityManager, migrations Doctrine globales
  • Pas de microservices ni de base par contexte (à retirer de la section « Évolutions futures » de .doc/backend/ARCHITECTURE.md : rien ne le justifie)
  • Routes API identiques (/trainer/…, /staff/…)
  • Doctrine dérive les noms de tables du nom court de classe : déplacer les namespaces ne génère aucune migration, sauf pour les entités portant un #[ORM\Table(name: …)] explicite

Conventions à généraliser :

  • [ ] Préfixe de table par contexte, déjà commencé (invoicing_invoice, invoicing_invoice_line_item) → étendre à calendar_, reservation_, compliance_
  • [ ] Un config/services/<contexte>.yaml par module au lieu du services.yaml monolithique

Phase 3 — Migration BC frontend

Plus simple que le back : pas d'ORM, pas de migrations, pas de schéma ; le bundler signale un import cassé en quelques secondes ; entièrement réversible (aucune donnée).

Les étapes 3.2 à 3.5 ne dépendent pas du back et peuvent être lancées en parallèle de la phase 1.

3.1 Principe : deux axes séparés

Le front est une couche de présentation. L'axe acteur y est légitime au premier niveau pour la présentation uniquement (routes, layouts, vues). Tout ce qui porte de la logique métier suit les contextes du back.

Aujourd'hui le métier est rangé par persona, donc dupliqué :

Domaine Store formateur Store staff
Conformité useComplianceStore.js useStaffComplianceStore.js
Demandes useRequestsStore.js useStaffRequestsStore.js
Dashboard trainerDashboard.js staffDashboard.js

3.2 Supprimer le code mort

Aucune contrepartie backend (Opportunity et FavoriteAcademy : 0 occurrence dans backend/src) :

  • [ ] api/opportunity.js (appelle GET /opportunity/proposal)
  • [ ] api/income.js (appelle GET /dashboard/income)
  • [ ] api/favoriteAcademy.js
  • [ ] components/Dashboard/Trainer/Incomes.vue et ActiveSchools.vue — importés nulle part
  • [ ] services/(core)/opportunityService.js — référencé seulement par les barrels
  • [ ] Retirer les réexports correspondants de api/index.js (sinon ces modules partent dans le bundle)
  • [ ] Corriger .doc/backend/ARCHITECTURE.md, qui documente encore « Opportunity Management » comme bounded context

3.3 Unifier les conventions de dossiers

Cinq conventions concurrentes dans components/ : Trainer/ (majuscule), trainer/ (minuscule), par domaine (messages/, profile/, compliance/, Onboarding/, Auth/), par type (ui/, (forms)/, (shared)/), plus shared/ en doublon de (shared)/, plus des composants en vrac à la racine.

  • [ ] Résoudre les doublons de casse Trainer/ vs trainer/ et (shared)/ vs shared/
  • [ ] Remonter les composants qui vivent dans views/ (views/trainer/DocumentTemplateEditor/, views/trainer/components/)

⚠️ Sur un système de fichiers insensible à la casse (macOS, Windows), git peut fusionner ou perdre des fichiers. Faire le renommage en deux git mv via un nom temporaire, dans un commit dédié.

3.4 Éclater les fichiers God

  • [ ] api/trainer.js1874 lignes, classe TrainerAPI couvrant profil, planning, carrière, compétences, entreprise, réglages de facturation, gabarits, factures, disponibilités, conformité. Miroir exact du God aggregate. À découper par contexte : déplacements de méthodes, aucune conception.
  • [ ] router/index.js100 routes dans un fichier, à éclater en routes/{trainer,staff,public}.js

3.5 Architecture cible

frontend/src/
├── app/                    # bootstrap, axios, plugins, sentry
│   └── router/
│       ├── index.js
│       └── routes/{trainer,staff,public}.js
├── layouts/                # TrainerLayout, StaffLayout, PublicLayout
│
├── modules/                # ← miroir des contextes back
│   ├── identity/           # auth, session, onboarding
│   ├── profile/
│   ├── calendar/
│   ├── reservation/
│   ├── invoicing/
│   ├── compliance/
│   ├── directory/
│   ├── messaging/
│   ├── notification/
│   ├── catalog/
│   ├── public/             # façade endpoints publics multi-BC
│   └── reporting/          # dashboards formateur + staff
│
├── views/                  # ← axe ACTEUR, présentation pure
│   ├── trainer/  staff/  public/
│
└── ui/                     # design system, aucun métier
    └── base/  forms/  feedback/

Structure d'un module :

modules/invoicing/
├── api/          invoices.js, creditNotes.js, activityReports.js, billingTemplates.js
├── stores/       useInvoicesStore.js
├── composables/  useBillingDocumentForm.js
├── components/   InvoiceDetailDrawer.vue, LinkedInvoicesCell.vue, …
└── constants/

Deux règles : une vue assemble, elle ne contient pas de métier ; un module ne connaît ni les routes ni les layouts.

3.6 Outillage des frontières (équivalent Deptrac)

  • [ ] eslint-plugin-boundaries (ou no-restricted-imports par zone), en warn d'abord :
  • views/** peut importer modules/*/{components,composables,stores} et ui/**jamais modules/*/api/** en direct
  • modules/a/** ne peut pas importer modules/b/**, sauf identity/ et ui/ (le noyau, comme côté back)
  • ui/** n'importe aucun module
  • aucun axios hors modules/*/api/ et app/

Cette dernière règle existe déjà dans .cursor/rules/frontend-vue.mdc (« Aucun appel API en dehors de src/api/ ») : elle deviendrait vérifiable en CI au lieu de reposer sur la vigilance.

3.7 Fusionner les stores dupliqués (seule étape à risque)

Ce n'est pas un déplacement de fichiers mais la réconciliation de deux implémentations qui ont divergé pendant un an. C'est là que se logeront les régressions.

  • [ ] compliance en pilote (duplication la plus visible, gain le plus démonstratif)
  • [ ] Cible : une couche api/ unique par module, puis un store avec deux sélecteurs de lecture ou deux stores fins au-dessus

3.8 Précautions

  • Mocks de tests : 27 fichiers .spec.js contiennent ~50 vi.mock('<chemin>'). Le danger n'est pas le mock qui casse (bruyant, donc corrigé) mais celui qui, pendant la transition, cible encore l'ancien chemin toujours existant alors que le composant importe déjà le nouveau : il ne s'applique plus, le vrai module est appelé, et le test peut passer au vert sans rien tester. → Supprimer l'ancien fichier dans le même commit que le déplacement, jamais en deux temps.
  • Pas de TypeScript (2 fichiers .ts sur 459) : le filet est la résolution d'imports + les .spec.js, rien ne vérifie les contrats de props.
  • Supprimer les barrels (api/index.js, components/index.js, services/index.js) : ils font partir du code mort dans le bundle et masquent les imports orphelins au linter.

3.9 Ce qu'on ne fait pas

  • Pas 13 dossiers dans views/ : une vue est rattachée à un acteur et un layout, pas à un contexte
  • Pas de modules/trainer/ : ce serait refaire l'erreur écartée côté back, un étage plus haut
  • Pas de design system dans les modules : ui/ reste transverse et sans métier

Récapitulatif des règles à graver

  1. Découpage par métier, pas par acteur. L'axe acteur vit dans la couche de présentation de chaque contexte.
  2. Liens objets autorisés vers le noyau (User, Trainer, Staff, Organization), en lecture seule et unidirectionnels (jamais de OneToMany inverse depuis le noyau).
  3. Entre deux contextes non-noyau : identifiant + port.
  4. Le noyau ne grossit pas : un champ propre à un contexte y déménage.
  5. Shared/ : aucune entité Doctrine.
  6. Snapshot uniquement pour l'immuabilité légale des documents de facturation, jamais comme technique de découplage générale.
  7. Les FK restent en base : le découplage est une propriété du code.
  8. Aucune étape sans Deptrac / ESLint boundaries pour la garder.
  9. Strangler, jamais big bang : interdire les nouveaux fichiers hors modules, migrer à l'occasion des tickets qui touchent la zone.

Suivi

Phase Étape Statut
1 Outillage Deptrac ✅ baseline 28 violations
1 Invariants dans les 5 agrégats 🟡 Invoice + CreditNote
1 Snapshots facturation
1 Nettoyage code mort backend
1 Incohérences de convention 🟡 exceptions HTTP Invoicing migrées
1 Nommage CQRS tranché
2 Collections inverses supprimées ✅ (partiel : cascades conservées)
2 5 cycles cassés 🟡 partiel
2 Notification polymorphe
2 Messaging/ pilote
2 Compliance/ ✅ ~138 fichiers
2 Invoicing/ ✅ ~346 fichiers
2 Calendar/ + Reservation/ ✅ ~197 + ~208 fichiers
2 Catalog/ + Directory/ ✅ ~51 + ~94 fichiers
2 Profile/ ✅ ~153 fichiers
2 Notification/ ✅ ~51 fichiers (STI conservée)
2 AccountLifecycle/ ✅ ~33 fichiers
2 Reporting/ ✅ ~85 fichiers (lecture seule)
2 Identity/ ✅ ~218 fichiers
2 Shared/ (ex-Platform fusionné) ✅ ~52 fichiers transverse + VO
2 Drain Application/ + Infrastructure/ ✅ supprimés (0 fichier legacy)
2 Éclatement Trainer ✅ 856→474 lignes + 3 tables (profile_trainer_settings, invoicing_trainer_sequence, reporting_trainer_preferences)
2 Casser STI Document StoredFile (table stored_file) + StoredDocumentFieldsTrait (délégation) + migrations Version20260815150000 / Version20260815170000
2 Notification tables filles ✅ 7 tables composées (notification_id FK) + délégation getPayload() / migration Version20260815180000
2 config/services/<bc>.yaml ✅ 14 fichiers sous config/services/
3 Modules frontend + split api/trainer.js ✅ facade + 12 modules (identity, profile, calendar, reservation, invoicing, compliance, directory, messaging, notification, reporting, catalog, public)
3 Code mort frontend
3 Conventions de dossiers Trainer/ / trainer/ / stubs composants supprimés ; (shared)/ canonique (ConversationDrawer fusionné) ; shared/ supprimé
3 Drain stubs legacy ✅ réexports components/*, stores/*, composables/*, api/* (hors trainer.js + infra axios) supprimés ; api/index.js pointe vers modules/
3 api/trainer.js + router éclatés
3 ESLint boundaries error sur views/** (pas d’import @/api/** ni modules/*/api/**) ; warn ailleurs
3 modules/ + fusion des stores ✅ API/stores legacy déplacés + réexports @deprecated ; vues migrées vers @/modules/<bc>
3 Composants → modules ✅ identity (auth + onboarding + account), profile (public), calendar (connectors), compliance (staff header), invoicing (templates + CRA), reservation (wizards + modales public) ; tous les BC ont leurs composants métier sous modules/

Post-MEP — Phase 3 front (reporté, août 2026)

Périmètre volontairement hors merge/deploy refacto BC. À traiter dans un ticket dédié après MEP.

  • [ ] Migrer les imports @/api/trainer (~40 fichiers vues/stores/composants) → @/modules/<bc> (stores/composables). La façade api/trainer.js reste valide en transition ; cible = plus d’import direct @/api/** depuis les vues.
  • [ ] ESLint boundaries en CIeslint.config.js interdit déjà @/api/** dans views/** ; ajouter npm run lint (ou équivalent) au job frontend-lint (aujourd’hui Prettier seul).
  • [ ] Imports modules/*/api/** dans les vues — passer par stores/composables (règle warn globale déjà présente).
  • [ ] Code mort doc phase 3api/opportunity.js, api/income.js, api/favoriteAcademy.js, composants Dashboard orphelins, barrels api/index.js (cf. section « Code mort » plus haut).
  • [ ] Polish conventions dossiers — doublons casse Trainer/ vs trainer/, composants encore sous views/trainer/ à remonter si pertinent.

Références

  • .doc/backend/ARCHITECTURE.md — architecture actuelle (à mettre à jour au fil de la migration)
  • .cursor/rules/backend-ddd.mdc — règles de couches
  • .cursor/rules/backend-rgpd-export-erasure.mdc — cascades et effacement
  • .cursor/rules/frontend-vue.mdc — conventions front
  • .cursor/rules/linters-keeper.mdc — linters CI (à compléter avec Deptrac et ESLint boundaries)

Statut validation (15 août 2026)

Suite à l'éclatement Trainer et aux corrections de mapping post-migration :

Vérification Résultat
Tests unitaires 773 TU OK
Tests fonctionnels 301 TF OK

Corrections associées :

  • DoctrineInterventionEvent : relation OneToOne + inversedBy sur les line items (InvoiceLineItem, ActivityReportLineItem).
  • Tests APIAbstractApiTestCase expose profileSettings (remplace les accès directs aux champs déplacés sur Trainer).
  • Twig — templates PDF/email basculés sur profileSettings.
  • ProfilegetOrCreateForTrainer : flush manquant corrigé (persistance fiable à la création).