Roadmap de migration vers l'architecture cible de DMV Core
Statut
Document de planification — v1.0 — rédigé le 2026-07-22. Ce n'est pas une ADR : aucune
décision d'architecture n'est prise ici. L'architecture cible est considérée comme figée
(ADR-012, ADR-014) et par les décisions produit arbitrées ci-dessous, désormais actées et
non rediscutées dans ce document. Ce document planifie uniquement comment l'implémentation
existante rejoint cette cible. Aucun code n'a été modifié, aucune migration Laravel créée,
aucun fichier déplacé pour produire ce document — il s'appuie entièrement sur les constats déjà
établis et cités dans 28-dmv-core-bounded-context-map.md, complétés par les décisions produit
ci-dessous.
Convention de lecture identique à celle du document 28 : [Constat] (observé dans le code), [Décision produit] (acté par la présente mission, non remis en question), [Plan] (proposition de séquencement).
1. Résumé
Le code de DMV Core diverge aujourd'hui de l'architecture cible sur un nombre limité mais concentré de points : trois modules (Actor, Territory, Monetization) écrivent directement dans les tables d'un autre contexte plutôt que d'utiliser un contrat publié ; deux modules (Actor, Publication) déclenchent des conséquences externes (XP, récompenses) au lieu de publier un fait ; le mécanisme d'autorisation est dupliqué indépendamment dans quatre modules ; et un module entier (Mairie) mélange deux natures de responsabilités qui doivent être séparées en deux Bounded Contexts distincts (Actor et le nouveau Municipal Management).
Aucun de ces écarts ne remet en cause la faisabilité de la cible : toutes les violations identifiées sont des raccourcis d'accès aux données (écriture croisée, appel direct), jamais une incompatibilité structurelle. La migration proposée est strictement incrémentale, en sept phases indépendantes, sans réécriture, sans rupture de compatibilité, et sans grand soir. Les deux premières phases (quick wins et extraction du Platform Service Authorization) peuvent démarrer immédiatement. Les phases 4 et 5 (extraction de Municipal Management, refonte du rafraîchissement SIRENE automatisé) sont les chantiers les plus lourds et dépendent de clarifications produit préalables, détaillées en Phase 3.
2. Architecture cible (rappel synthétique)
Bounded Contexts de DMV Core, tels qu'actés par les décisions produit de cette mission (qui
priment sur toute proposition antérieure de 28-dmv-core-bounded-context-map.md en cas de
divergence) :
| Bounded Context | Possède | Référence |
|---|---|---|
| Identity | Session, profil, rôles bruts | — |
| Actor | Identité de l'organisation, collaborateurs, rôles organisationnels, revendications (Claims) | Territory (lecture), Authorization (lecture) |
| Territory | Uniquement le territoire : commune, code INSEE, nom, géographie, codes postaux, identité territoriale | — |
| Municipal Management (nouveau) | Élus, collectes, informations pratiques, services municipaux, alertes municipales | Territory, Actor |
| Community | Contributeurs, groupes, favoris | Identity, Actor (lecture) |
| Publication | Cycle de vie éditorial, modération | Actor (référence) |
| Monetization | Plans, abonnements, boosts, facturation | Actor (référence) |
| Rewards | Règles de récompense, octrois, XP | consomme des événements |
| AssoSuite, PlayLoop | Déjà isolés, candidats à devenir des applications (ADR-000) | — |
Authorization n'est pas un Bounded Context : c'est un Platform Service (au sens de
26-platform-architecture.md) qui fournit le mécanisme générique de résolution
rôle/permission. Les règles métier de qui a le droit de quoi restent dans chaque Bounded
Context ; Authorization ne fait qu'exécuter le calcul.
Claims n'est pas un Bounded Context autonome : sous-domaine d'Actor, dont le workflow peut
être orchestré par un Process Manager explicite si des conséquences multi-contextes garanties
l'exigent (ADR-014 principe 3).
Chat reste hors périmètre : système de ticketing interne transitoire, aucun Bounded Context Messaging n'est créé à ce stade.
acteur_alertes est un héritage à supprimer : les alertes deviennent une capacité de
Municipal Management ; la diffusion publique reste du ressort de Publication, inchangé.
AI, Analytics, Settings, Import restent des Platform Services / adaptateurs, pas des Bounded Contexts métier (inchangé depuis le document 28). Admin reste un adaptateur pour le Backoffice, sous réserve de l'audit de ses écritures, toujours non réalisé (voir Phase 1).
3. Analyse des écarts, par contexte
Identity — écart mineur
[Constat] ProfileService::resolveManagedCommunes()
(app/Modules/Identity/Services/ProfileService.php:111-164) compose une lecture croisée
Territory + Actor directement dans Identity, via jointure SQL. Lecture seule, aucune écriture
croisée.
[Plan] Faire consommer par Identity des contrats de lecture explicites d'Actor et de Territory plutôt qu'une jointure directe. Écart non structurant — ne bloque rien d'autre.
Actor — écarts multiples, tous corrigibles indépendamment
[Constat] Trois écarts trouvés : écriture directe dans MairieAlerte
(ActeurModulesController.php:249-284), appel synchrone direct à
RewardEngineService::recordEvent() (ActeurWriteService.php:158,163), et absence de modèle
Claim propre malgré une écriture directe dans DB::table('claims')
(ActeurWriteService::submitClaim()).
[Décision produit] Claims reste un sous-domaine d'Actor — cet écart n'est donc pas une question de propriétaire (déjà le bon), mais d'implémentation manquante (pas de modèle, pas de service applicatif dédié).
[Plan] Voir Phases 1 (conversion en événement), 4 (redirection vers Municipal Management), 6 (formalisation de Claims).
Territory — écart de périmètre et écriture croisée
[Constat] Territory possède aujourd'hui CommuneElu, CommuneCollecte, CommuneInfo —
qui, selon la décision produit ci-dessus, n'appartiennent plus à Territory mais à Municipal
Management. Territory écrit aussi directement dans acteurs
(CommuneMairieDataRefreshService.php:148,158).
[Décision produit] Territory ne possède que le territoire au sens strict. Les modèles
CommuneElu/CommuneCollecte/CommuneInfo, bien que physiquement présents dans le module
Territory aujourd'hui, doivent être considérés comme un écart à résorber, pas comme un
périmètre acquis.
[Plan] Voir Phases 4 (transfert vers Municipal Management) et 5 (refonte du rafraîchissement automatisé, qui touche Territory, Actor et Municipal Management à la fois).
Municipal Management — Bounded Context à créer, pas à renommer
[Constat] Aucun module MunicipalManagement n'existe. Le module Mairie actuel contient
un mélange : Alertes et Services municipaux (autonomes, bien isolés,
MairieWriteService.php:34-79,101-147), délégation correcte à Publication pour les
publications (MairieWriteService.php:157-212), et des écritures SQL brutes dans
communes.description/image_url et dans commune_elus/commune_collectes/commune_infos
(MairieWriteService.php:225,233-351) qui contournent les modèles Eloquent de Territory.
[Décision produit] Municipal Management possède élus, collectes, informations pratiques, services municipaux, alertes municipales. Il référence Territory et Actor mais ne possède ni la commune ni la mairie.
[Interprétation] Ceci implique deux choses distinctes, à ne pas confondre : (a) une
opération de renommage/déplacement de propriété pour Alertes et Services municipaux, déjà
autonomes et sans ambiguïté — un mouvement à faible risque ; (b) une véritable migration de
données possédées pour Élus/Collectes/Infos, qui change de Bounded Context propriétaire
(Territory → Municipal Management) et unifie trois chemins d'écriture concurrents en un seul —
un mouvement à risque plus élevé. Les champs description/image_url de communes restent
une zone grise non couverte par la liste explicite des décisions produit (voir Phase 3.1).
[Plan] Voir Phase 4, le chantier le plus structurant de cette roadmap.
Authorization — Platform Service à extraire, pas encore de dépendance interdite
[Constat] ActorAccessService (app/Modules/Actor/Services/ActorAccessService.php) est le
mécanisme le plus abouti et déjà réutilisé tel quel par Mairie. CommunityPolicy,
EnsureAssoAccess (AssoSuite) et les ensureOwner() de Monetization le réimplémentent chacun
indépendamment.
[Décision produit] Authorization n'est pas un Bounded Context, c'est un Platform Service. Les règles métier restent dans les Bounded Contexts ; Authorization ne fournit que le mécanisme générique.
[Interprétation] Ceci confirme et clarifie un écart déjà signalé dans le document 28 : le
problème n'est pas qu'Actor porte aujourd'hui ActorAccessService (c'est acceptable en l'état
transitoire), c'est que Mairie en dépend directement en tant que module d'un autre Bounded
Context, plutôt que via un Platform Service partagé — et que Community/AssoSuite/Monetization
ont chacun recréé une version incompatible du même mécanisme faute d'un tel service. Ce n'est
pas classé « violation directe d'ADR-014 » au sens strict aujourd'hui (aucune écriture croisée
n'en découle), mais c'est le principal risque de divergence silencieuse identifié dans le
document 28.
[Plan] Voir Phase 2.
Community, Publication, Monetization, Rewards — écarts déjà identifiés en doc 28, confirmés inchangés
Aucune décision produit nouvelle ne modifie l'analyse déjà faite dans
28-dmv-core-bounded-context-map.md §3 pour ces quatre contextes. Rappel des écarts
actionnables :
- Publication → appelle directement
ActorXpService/RewardEngineService(PublicationWriteService.php:122-147) au lieu de publier un fait. Voir Phase 1. - Monetization → écrit directement dans
acteurs(featured_until,stripe_account_id,badge_verifie/label—BoostUsageService.php:207-212,MonetizationWriteService.php:62-63,362-366,392-412) et duplique l'autorisation. Voir Phases 2 et 7. - Community → duplique l'autorisation (
CommunityPolicy), aucune écriture croisée. Voir Phase 2. - Rewards → invoqué en appel synchrone plutôt qu'en consommateur d'événements. Voir Phase 1.
Chat, AssoSuite, PlayLoop, AI, Analytics, Settings, Import, Admin — pas de migration requise à ce stade
[Décision produit] Chat reste hors périmètre — aucune action.
[Constat] AssoSuite et PlayLoop sont déjà isolés ; seule leur duplication d'autorisation les relie à la Phase 2 (participation optionnelle, faible priorité puisqu'ils n'ont pas d'écriture croisée). AI, Analytics, Settings, Import restent des Platform Services/adaptateurs sans écart identifié. Admin n'a pas été audité côté écritures (lacune déjà signalée en doc 28) — cet audit est un prérequis, pas une migration en soi. Voir Phase 1.
4. Graphe des dépendances
Phase 1 (Quick Wins)
│ aucun prérequis
│
├──► Phase 2 (Authorization — extraction du Platform Service)
│ │ prérequis : aucun (peut démarrer en parallèle de la Phase 1)
│ │
│ ├──► Phase 4.4 (bascule des middleware Mairie/Municipal Management vers Authorization)
│ └──► Phase 7.3 (bascule de Monetization vers Authorization)
│
├──► Phase 3 (clarifications produit : description/image_url de la commune, périmètre défibrillateurs)
│ │ prérequis : aucun techniquement, mais bloque la Phase 4 pour rester non ambiguë
│ │
│ └──► Phase 4 (extraction de Municipal Management)
│ │ prérequis : Phase 3 tranchée (4.5), Phase 2 disponible (4.4, non bloquant si reporté)
│ │
│ └──► Phase 5 (refonte du rafraîchissement SIRENE automatisé)
│ prérequis : Phase 4 stabilisée (Municipal Management doit exister
│ comme cible d'écriture avant que Territory cesse d'écrire lui-même
│ dans ses tables)
│
├──► Phase 6 (Claims — formalisation dans Actor)
│ prérequis : audit d'Admin réalisé (Phase 1.5) avant de toucher AdminClaimService
│
└──► Phase 7 (Monetization — suppression des écritures croisées vers acteurs)
prérequis : Phase 2 recommandée avant 7.3, non bloquante pour 7.1/7.2
Lecture du graphe. Les Phases 1, 2, 3 et 6 (hors audit Admin) n'ont aucune dépendance entre elles et peuvent être menées en parallèle par des personnes différentes. La Phase 4 est le seul véritable point de convergence : elle a besoin de la Phase 3 tranchée pour ne pas migrer un champ ambigu vers le mauvais propriétaire, et bénéficie (sans en dépendre strictement) de la Phase 2 pour ne pas migrer les middleware vers un mécanisme d'autorisation qui devra de toute façon être remplacé peu après. La Phase 5 ne peut pas commencer avant que Municipal Management existe réellement comme cible d'écriture (Phase 4), sous peine de créer un nouvel aller-retour de dette. La Phase 7 est indépendante de tout sauf de la disponibilité optionnelle d'Authorization pour sa dernière étape.
5. Roadmap
Chaque phase est livrable et testable indépendamment ; aucune ne requiert qu'une autre phase soit terminée pour laisser l'application dans un état fonctionnel.
Phase 1 — Quick Wins (fondations, risque quasi nul)
- 1.1 Supprimer
ActeurAlerte/acteur_alertes(aucun point d'appel dans le code — confirmé en doc 28 §6). Suppression de la table via une migration Supabase (jamaisphp artisan migrate, conformément à la règle déjà en vigueur sur ce projet). - 1.2 Faire lire
RankingServiceviaSettingsReadServiceau lieu deDB::table('app_settings')brut (RankingService.php:53). - 1.3 Convertir les trois appels directs vers Rewards/XP
(
ActeurWriteService.php:158,163etPublicationWriteService.php:122-147) en événements internes (dispatch en mémoire, mécanisme déjà disponible dans Laravel), avec Rewards qui s'y abonne. Explicitement : ceci n'introduit pas de Transactional Outbox durable (ADR-014principe 7) — c'est une étape de découplage en mémoire, à documenter comme telle, la durabilité restant un chantier séparé et non urgent tant que Rewards et son émetteur restent dans le même déploiement. - 1.4 Auditer les services d'écriture d'Admin (
AdminActeurService,AdminClaimService, etc.) pour vérifier s'ils dupliquent des règles métier plutôt que de déléguer aux modules propriétaires — prérequis de la Phase 6, pas une correction en soi.
Phase 2 — Extraction du Platform Service Authorization
- 2.1 Extraire le mécanisme générique de
ActorAccessServicevers un Platform Service Authorization à interface stable, en conservant exactement le même comportement observable. - 2.2 Faire consommer ce Platform Service par Actor lui-même (pour sa propre
ActeurPolicy), puis par Community, AssoSuite, Monetization — un module à la fois, chaque bascule validée par les tests existants du module avant de retirer son ancienne implémentation. - 2.3 Faire consommer ce Platform Service par les middleware
EnsureMairieAccess/EnsureCommuneManager(qui appellent déjàActorAccessServicedirectement aujourd'hui — la bascule change uniquement la source, pas le comportement).
Phase 3 — Clarifications produit préalables (aucun code modifié)
- 3.1 Décider du sort de
communes.description/image_url, actuellement écrits par Mairie : doublon du profil de l'acteur mairie à supprimer, ou champ réellement propre à Territory à faire migrer vers un contrat Territory ? - 3.2 Décider si
CommuneDefibrillateur/DefibrillateurCataloguerelèvent de Territory (référence publique) ou de Municipal Management (information pratique) — non couvert explicitement par la liste de décisions produit fournie.
Phase 4 — Extraction du Bounded Context Municipal Management
- 4.1 Créer le module Municipal Management (structure vide, sans logique).
- 4.2 Déplacer Alertes et Services municipaux du module Mairie vers Municipal Management — capacités déjà autonomes, mouvement à faible risque.
- 4.3 Déplacer les modèles
CommuneElu/CommuneCollecte/CommuneInfode Territory vers Municipal Management, et migrer les écritures aujourd'hui en SQL brut deMairieWriteService.php:233-351vers ces modèles dans leur nouveau module — unifiant les trois chemins d'écriture concurrents identifiés en doc 28 en un seul. - 4.4 Basculer les middleware d'accès mairie vers le Platform Service Authorization (Phase 2), si celle-ci est déjà disponible ; sinon reporter cette seule sous-étape sans bloquer le reste de la Phase 4.
- 4.5 Appliquer la décision de la Phase 3.1 : migrer ou supprimer l'écriture de
communes.description/image_url. - 4.6 Rediriger l'écriture croisée d'Actor vers
MairieAlerte(ActeurModulesController.php:249-284) vers un contrat publié par Municipal Management.
Phase 5 — Refonte du rafraîchissement automatisé SIRENE (chantier structurant)
- 5.1 Faire publier par Territory un fait (
CommuneSireneRefreshedou équivalent) au lieu d'écrire directement dansacteurs(CommuneMairieDataRefreshService.php:148,158) et dans les tables désormais possédées par Municipal Management (CommuneMairieDataRefreshService.php:218,232,246,260,314,321,323). - 5.2 Faire consommer ce fait par Actor (mise à jour de
kind/categorie_id/numero_siretvia son propre service applicatif) et par Municipal Management (mise à jour des élus/infos), chacun de manière idempotente. - 5.3 Retirer les écritures croisées directes du service de rafraîchissement une fois 5.1/5.2 validés sur un environnement de test avec des données SIRENE réelles, en conservant le comportement observable identique pour les utilisateurs finaux.
Phase 6 — Claims comme sous-domaine explicite d'Actor
- 6.1 Créer un modèle
Claimet un service applicatif dédié dans Actor (aucun n'existe aujourd'hui malgré l'écriture directe dansDB::table('claims')). - 6.2 Migrer
ActeurWriteService::submitClaim()et les services de traitement d'Admin vers ce service applicatif, une fois l'audit d'Admin (Phase 1.4) réalisé. - 6.3 Si le workflow (soumission → revue → décision) a des conséquences multi-contextes
garanties (ex. octroi d'un rôle collaborateur), l'orchestrer par un Process Manager explicite
et nommé plutôt que de le laisser implicite dans Actor ou Admin (
ADR-014principe 3).
Phase 7 — Monetization : suppression des écritures croisées vers acteurs
- 7.1 Exposer côté Actor des contrats explicites (service applicatif) pour
featured_until,stripe_account_id,badge_verifie/badge_label. - 7.2 Faire consommer ces contrats par Monetization au lieu de
DB::table('acteurs')->update(). - 7.3 Remplacer les
ensureOwner()dupliqués de Monetization par un appel au Platform Service Authorization (Phase 2).
6. Quick Wins
Corrections à très faible risque, gain architectural immédiat, aucune dépendance :
- 1.1 Suppression d'
acteur_alertes— zéro consommateur, suppression pure. - 1.2
RankingService→ lecture viaSettingsReadService. - 1.3 Conversion des trois appels directs Actor/Publication → Rewards en événements en mémoire.
- 2.1 Extraction du Platform Service Authorization à partir d'
ActorAccessService— la seule étape de la Phase 2 qui n'attend rien d'autre, même si sa consommation complète (2.2/2.3) demande plusieurs itérations.
7. Chantiers structurants
Migrations à réaliser progressivement sur plusieurs itérations, chacune avec un blast radius réel :
- Phase 4 — Extraction de Municipal Management. Le chantier le plus étendu : crée un nouveau Bounded Context, migre la propriété de trois tables entre deux modules existants, et unifie trois chemins d'écriture concurrents en un seul. Dépend d'une clarification produit préalable (Phase 3).
- Phase 5 — Refonte du rafraîchissement SIRENE automatisé. Un job automatisé qui touche aujourd'hui trois futurs Bounded Contexts en une seule opération ; le blast radius est plus élevé qu'ailleurs car il s'exécute sans supervision humaine directe sur des données de production réelles.
- Phase 2 (déploiement complet, 2.2/2.3) — Convergence de l'autorisation. Techniquement simple étape par étape, mais étendue sur quatre modules indépendants (Actor, Community, AssoSuite, Monetization) plus les middleware Mairie, chacun nécessitant sa propre validation avant de retirer l'ancienne implémentation.
- Phase 6 — Formalisation de Claims. Dépend d'un audit préalable d'Admin dont l'issue n'est pas connue à ce stade ; peut révéler une duplication de règles métier plus large que prévu.
8. Validation finale par phase
| Phase | Critères de succès | Tests à exécuter | Risques |
|---|---|---|---|
| 1 — Quick Wins | Aucun consommateur cassé ; acteur_alertes absente de la base ; Rewards reçoit toujours ses événements | Suite de tests existante d'Actor, Publication, Rewards ; vérification manuelle qu'aucune route ne référence plus acteur_alertes | Très faible — aucune écriture croisée nouvelle, comportement observable inchangé |
| 2 — Authorization | Comportement d'accès strictement identique avant/après bascule, module par module | Tests d'autorisation existants de chaque module rejoués contre le nouveau Platform Service avant suppression de l'ancien code | Faible à modéré — une divergence de comportement lors de l'unification des quatre implémentations est le risque principal ; mitigé par bascule module par module, jamais en bloc |
| 3 — Clarifications produit | Décision actée et documentée pour 3.1 et 3.2 | Aucun test — validation par relecture produit | Aucun risque technique ; risque de blocage de la Phase 4 si non tranché à temps |
| 4 — Municipal Management | Élus/Collectes/Infos/Alertes/Services lus et écrits uniquement via Municipal Management ; plus aucune écriture SQL brute vers communes/commune_elus/commune_collectes/commune_infos hors de ce module | Tests de bout en bout du parcours de gestion municipale (dashboard mairie), avant/après chaque sous-étape | Le plus élevé de la roadmap — migration de propriété de données réelles, plusieurs consommateurs (Mairie actuel, Territory, futur MairieSuite) ; mitigé par le découpage en 6 sous-étapes indépendantes |
| 5 — Rafraîchissement SIRENE | Le job automatisé ne modifie plus jamais directement une table hors de Territory | Exécution du job sur un jeu de données SIRENE réel en environnement de test, comparaison des résultats avant/après | Élevé — job automatisé sans supervision humaine directe ; mitigé en validant 5.1/5.2 en parallèle avant de retirer l'ancien chemin (5.3) |
| 6 — Claims | Toute écriture sur claims passe par le service applicatif d'Actor | Tests du parcours de revendication (soumission → décision Admin) | Modéré — dépend de l'issue, non connue à l'avance, de l'audit des écritures d'Admin |
| 7 — Monetization | Plus aucune écriture directe de Monetization dans acteurs | Tests des parcours boost/abonnement/badge existants | Modéré — badge_verifie/label documente explicitement répliquer un trigger Supabase existant ; la migration doit vérifier que ce trigger reste cohérent avec le nouveau contrat Actor |
Contraintes respectées
Cette roadmap ne propose aucune réécriture complète, ne casse aucune compatibilité existante (chaque phase laisse l'application fonctionnelle), ne crée aucune dette supplémentaire (chaque étape réduit strictement le nombre d'écarts par rapport à la cible), et découpe systématiquement les chantiers les plus lourds (Phases 4 et 5) en sous-étapes indépendantes plutôt qu'en un seul mouvement.
Liens associés
docs/decisions/ADR-000-dmv-core-modular-applications.mddocs/decisions/ADR-012-architecture-foundation-frozen.mddocs/decisions/ADR-014-dmv-core-bounded-contexts.mddocs/06-architecture/26-platform-architecture.mddocs/06-architecture/27-architecture-governance.mddocs/06-architecture/28-dmv-core-bounded-context-map.md