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 viaStoredFile(composition) ; notifications via enveloppeNotification+ 7 tables filles par kind (compositionnotification_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 :Domainne dépend ni d'Applicationni 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ée — FinalizeCreditNoteCommand, 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]
Invoice→finalize(),markAsPaid(),markAsSent(),canBeEdited(); setters de cycle de vie en privé - [x]
CreditNote→finalize()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é dedocument_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é :
InvoiceLineItemcopie déjàinterventionDateetclassName.
1.4 Nettoyer le code mort
- [x] Supprimer
Domain/Entity/Invoice.php(legacy, tableinvoice, 4 champs, anémique) - [x] Supprimer
Notification/InvoiceNotification+ son repository et son interface : jamais instancié danssrc/, et seule référence vivante vers l'Invoicelegacy - [x] Corriger
Organization::$invoicesetTrainer::$invoices: ils pointent vers l'entité legacy, doncgetInvoices()renvoie silencieusement une collection vide au lieu des factures deinvoicing_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, ouUseCase/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 :
- Liens unidirectionnels vers le noyau. Un
ManyToOnedescendant (Invoice → Trainer) est acceptable — c'est un Shared Kernel assumé. UnOneToManyinverse remontant (Trainer → Collection<Invoice>) ne l'est jamais : il transforme le noyau en hub et rend le graphe cyclique. - Le noyau ne grossit pas. Si un contexte a besoin d'un champ sur
Trainer, ce champ lui appartient : il déménage. - Aucun contexte n'écrit dans le noyau. Pas d'appel de setter sur une entité du noyau depuis l'extérieur d'
Identity. Shared/ne contient aucune entité Doctrine. Uniquement VO sans identité (Address,PhoneNumber, montants), types temporels, identifiants (TrainerId,OrganizationId). SiTrainerfinit dansShared, le découpage perd tout intérêt.- 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.
Notificationen références polymorphes (targetType+targetUuid+payloadJSON) : supprime 6 liens inter-contextes d'un coup et fait deNotificationune 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/puisReservation/(les plus enchevêtrés, en dernier). - [ ] 9. Éclater
TrainerversProfile/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é |
|---|---|
TrainerOrganizationAssignment ↔ CalendarConfigurator |
Calendar → Directory |
InterventionEvent ↔ InvoiceLineItem / ActivityReportLineItem |
Invoicing → Calendar |
CalendarConfiguratorAssignment ↔ InvoiceProduct |
Invoicing → Calendar |
TrainerAvailabilityPlan ↔ TrainerPlanningShare |
Reservation → Calendar |
Reservation ↔ InterventionEvent |
à trancher |
2.7 Arbitrages à trancher avant de coder
- STI
Document—TrainerComplianceDocument(Compliance),TrainerDocumentCompanyetTrainerDocumentBillingAttachment(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). - STI
Notification— voir étape 4 ci-dessus. - 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. - 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>.yamlpar module au lieu duservices.yamlmonolithique
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(appelleGET /opportunity/proposal) - [ ]
api/income.js(appelleGET /dashboard/income) - [ ]
api/favoriteAcademy.js - [ ]
components/Dashboard/Trainer/Incomes.vueetActiveSchools.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/vstrainer/et(shared)/vsshared/ - [ ] 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 mvvia un nom temporaire, dans un commit dédié.
3.4 Éclater les fichiers God
- [ ]
api/trainer.js— 1874 lignes, classeTrainerAPIcouvrant 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.js— 100 routes dans un fichier, à éclater enroutes/{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(ouno-restricted-importspar zone), enwarnd'abord : views/**peut importermodules/*/{components,composables,stores}etui/**— jamaismodules/*/api/**en directmodules/a/**ne peut pas importermodules/b/**, saufidentity/etui/(le noyau, comme côté back)ui/**n'importe aucun module- aucun
axioshorsmodules/*/api/etapp/
Cette dernière règle existe déjà dans
.cursor/rules/frontend-vue.mdc(« Aucun appel API en dehors desrc/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.
- [ ]
complianceen 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.jscontiennent ~50vi.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
.tssur 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
- Découpage par métier, pas par acteur. L'axe acteur vit dans la couche de présentation de chaque contexte.
- Liens objets autorisés vers le noyau (
User,Trainer,Staff,Organization), en lecture seule et unidirectionnels (jamais deOneToManyinverse depuis le noyau). - Entre deux contextes non-noyau : identifiant + port.
- Le noyau ne grossit pas : un champ propre à un contexte y déménage.
Shared/: aucune entité Doctrine.- Snapshot uniquement pour l'immuabilité légale des documents de facturation, jamais comme technique de découplage générale.
- Les FK restent en base : le découplage est une propriété du code.
- Aucune étape sans Deptrac / ESLint boundaries pour la garder.
- 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çadeapi/trainer.jsreste valide en transition ; cible = plus d’import direct@/api/**depuis les vues. - [ ] ESLint boundaries en CI —
eslint.config.jsinterdit déjà@/api/**dansviews/**; ajouternpm run lint(ou équivalent) au jobfrontend-lint(aujourd’hui Prettier seul). - [ ] Imports
modules/*/api/**dans les vues — passer par stores/composables (règlewarnglobale déjà présente). - [ ] Code mort doc phase 3 —
api/opportunity.js,api/income.js,api/favoriteAcademy.js, composants Dashboard orphelins, barrelsapi/index.js(cf. section « Code mort » plus haut). - [ ] Polish conventions dossiers — doublons casse
Trainer/vstrainer/, composants encore sousviews/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 :
- Doctrine —
InterventionEvent: relationOneToOne+inversedBysur les line items (InvoiceLineItem,ActivityReportLineItem). - Tests API —
AbstractApiTestCaseexposeprofileSettings(remplace les accès directs aux champs déplacés surTrainer). - Twig — templates PDF/email basculés sur
profileSettings. - Profile —
getOrCreateForTrainer: flush manquant corrigé (persistance fiable à la création).