ENG-001.5B — Délégation des alertes Actor vers Municipal Management — Rapport d'implémentation
| Mission | Municipal-B — Délégation des alertes Actor vers Municipal Management |
| Rattaché à | ENG-001.5 — Extraction de Municipal Management §12, §13.A ; Audit API alertes |
| Dépôt | dmv_api |
| Branche | feature/eng-001-5b-municipal-alert-delegation (basée sur main + fondations Municipal-A non commitées) |
| Statut | Implémenté |
| Comportement fonctionnel | Inchangé pour tout utilisateur déjà correctement autorisé — durci pour deux cas limites précédemment non contrôlés (voir §5) |
0. Écart procédural préalable
ENG-001-5A-municipal-management-foundation.md, cité comme lecture obligatoire, reste introuvable
(déjà signalé en ENG-001.5A, non résolu depuis). Non bloquant : le corps de la mission, combiné à
ENG-001-5-municipal-management.md et au rapport ENG-001.5A déjà produit, suffit à cadrer le
travail sans ambiguïté.
1. Responsabilités déplacées
ActeurModulesController (module Actor) ne contient plus aucune logique métier pour les
alertes :
| Avant | Après |
|---|---|
MairieAlerte::query()->where('acteur_id', ...)->get() | $this->municipalReader->getAlertesByActeur($acteurId) |
MairieAlerte::create([...]) | $this->municipalWriter->createAlerte($acteurId, [...]) |
MairieAlerte::where('acteur_id', ...)->findOrFail(...)->update(...) | $this->municipalWriter->updateAlerte($acteurId, $alerteId, $data) |
MairieAlerte::where('acteur_id', ...)->findOrFail(...)->update(['is_active' => false]) | $this->municipalWriter->desactiverAlerte($acteurId, $alerteId) |
MunicipalManagementReadService/WriteService implémentent réellement ces cinq opérations
(lecture par commune, lecture par acteur, création, modification, désactivation) contre la table
mairie_alertes, via DB::table() — pas via le modèle Eloquent Mairie\Models\MairieAlerte,
pour ne pas importer l'ORM d'un autre contexte (ADR-014 §11). La table elle-même n'est pas
déplacée, conformément au périmètre.
2. Logique restant dans Actor
ActeurModulesController conserve, exactement comme demandé par la mission :
- Validation de la requête — les règles de validation
$request->validate([...])(titre,contenu,niveau,is_active,expires_at) sont inchangées, y compris leurs différences déjà documentées par rapport à l'API Mairie (contenunullable,expires_atsans contrainteafter:now,is_activerespecté à la création) — l'audit ne demandait pas de les aligner, seulement de corriger les deux contrôles d'autorisation manquants. - Autorisation générique —
$this->authorize('update', $acteur)(ActeurPolicy::update(), permissionmanage_actor), strictement inchangée. findMairieActeurOrFail()— vérifiekind === 'mairie', inchangé.- Résolution de
commune_id— lit$acteur->commune_id(donnée Actor déjà chargée par le contrôleur) et l'inclut dans le payload transmis àcreateAlerte(). Délibéré : évite que Municipal Management ait besoin de lire Actor via un contrat pour cette seule information, gardant le périmètre de cette PR minimal (voir §7).
Aucune logique de lecture/écriture de mairie_alertes, aucune date de résolution de ressource,
aucune construction de réponse au-delà de $dto->toArray().
3. Contrôles ajoutés (référence désormais unique)
Nouvelle méthode privée ensureAlertesAccess(Acteur $acteur) dans ActeurModulesController,
appelée par storeAlerte/updateAlerte/desactiverAlerte (pas par la lecture, publique comme
avant) :
private function ensureAlertesAccess(Acteur $acteur): void
{
$profile = auth()->user();
if ($profile->isAdmin()) {
return;
}
if (! $this->accessService->hasPermission($profile, $acteur->id, 'manage_alertes')) {
throw new HttpResponseException(response()->json(['error' => 'mairie_permission_denied'], 403));
}
if (! $this->accessService->hasModule($acteur->id, 'alertes')) {
throw new HttpResponseException(response()->json(['error' => 'mairie_module_disabled'], 403));
}
}
Reproduit exactement la logique déjà en place côté Mairie (EnsureMairieAccess) : même code de
permission (manage_alertes), même code de module (alertes), même court-circuit pour les
admins DMV (isAdmin() bypasse les deux contrôles, comme EnsureMairieAccess::handle() le fait
déjà pour toute la chaîne). Ces deux contrôles étaient absents de l'API Actor avant cette PR
(constat de ENG-001-5-alert-api-consumer-audit.md §6) ; ils sont désormais présents des deux
côtés (Actor et Mairie), avec un comportement identique.
4. Contrats utilisés
Extensions par rapport à Municipal-A (divergences documentées avant implémentation)
MunicipalManagementReader::getAlertesByActeur(string $acteurId): Collection— ajoutée.getAlertes(string $communeId)(Municipal-A) ne suffisait pas : la route Actor lit paracteur_id, pas parcommune_id. Une commune pouvant avoir plusieurs acteurs candidats en cas d'ambiguïté (CommuneMairieResolverService), interroger par commune aurait pu retourner les alertes d'un acteur différent de celui demandé — remplacer la clé de lecture aurait changé le comportement observable, ce que la mission interdit explicitement.MunicipalManagementWriter::updateAlerte()/desactiverAlerte()— signature modifiée pour inclure$acteurId(Municipal-A ne l'avait pas). Nécessaire pour reproduire exactement la vérification d'appartenance que faisaitActeurModulesControlleravant cette PR (MairieAlerte::where('acteur_id', $acteurId)->findOrFail($alerteId)) — sans ce paramètre, n'importe quel gestionnaire autorisé sur un acteur mairie aurait pu modifier l'alerte d'un autre acteur mairie en devinant son identifiant. Testé explicitement (§6, test « cross-acteur → 404 »).
Ces deux extensions modifient des signatures introduites par Municipal-A, elle-même non encore mergée (aucune PR fusionnée ne les consommait) — traité comme une correction avant publication, pas comme la modification d'un contrat déjà stabilisé. Documenté ici conformément à « toute divergence concernant les contrats… doit être remontée immédiatement ».
Signatures finales utilisées par la façade
MunicipalManagementReader::getAlertesByActeur(string $acteurId): Collection
MunicipalManagementWriter::createAlerte(string $acteurId, array $data): MunicipalAlertDTO
MunicipalManagementWriter::updateAlerte(string $acteurId, string $alerteId, array $data): MunicipalAlertDTO
MunicipalManagementWriter::desactiverAlerte(string $acteurId, string $alerteId): void
5. Impacts
Comportement inchangé pour les utilisateurs déjà correctement autorisés
- URLs, méthodes HTTP, noms de routes : strictement identiques (vérifié par
php artisan route:list, voir §9). - Format de réponse : identique —
toArray()du DTO produit exactement les mêmes clés que la sérialisation JSON du modèle Eloquent utilisée avant cette PR (id,commune_id,acteur_id,titre,contenu,niveau,is_active,expires_at,created_at), avec les dates reformatées en ISO8601 pour reproduire exactement le format que produisait la sérialisation Carbon automatique d'Eloquent (DB::table()ne bénéficiant pas des casts — reformatage manuel ajouté spécifiquement pour cette raison, voir le code deMunicipalManagementReadService). - Règles de validation : inchangées (voir §2).
test_mairie_actor_can_keep_using_actor_alert_route, test préexistant (avant cette mission) exerçant précisément cette route avec un rôleowner(qui possèdemanage_alertespar construction du seed RBAC — vérifié), reste vert sans modification.
Comportement durci pour deux cas limites, délibérément (instruction explicite de la mission)
- Un collaborateur disposant de
manage_actormais pas demanage_alertesobtient désormais403 mairie_permission_deniedsur les trois opérations d'écriture — auparavant, il pouvait écrire des alertes sans détenir cette permission spécifique. - Un acteur mairie dont le module
alertesest désactivé obtient désormais403 mairie_module_disabledsur les trois opérations d'écriture — auparavant, la désactivation du module n'avait aucun effet sur cette route.
Aucun rôle standard du seed RBAC (owner, admin, editor — tous les trois incluent déjà
manage_alertes) n'est affecté par ce durcissement dans l'usage courant ; seul un rôle
personnalisé construit sans cette permission, ou un acteur avec le module explicitement
désactivé, verrait son accès changer — exactement l'écart que l'audit avait identifié comme
non-intentionnel.
6. Fichiers créés
| Fichier | Contenu |
|---|---|
tests/Feature/MunicipalManagement/MunicipalAlertDelegationTest.php | 12 tests (voir §8) |
dmv-docs/docs/19-engineering/specs/ENG-001-5B-implementation-report.md | Ce document |
7. Fichiers modifiés
| Fichier | Changement |
|---|---|
app/Modules/MunicipalManagement/Contracts/MunicipalManagementReader.php | Ajout getAlertesByActeur() (voir §4) |
app/Modules/MunicipalManagement/Contracts/MunicipalManagementWriter.php | Signatures updateAlerte()/desactiverAlerte() modifiées (voir §4) |
app/Modules/MunicipalManagement/Services/MunicipalManagementReadService.php | getAlertes()/getAlertesByActeur() réellement implémentées ; getServices/getElus/getCollectes/getInfos inchangés (toujours LogicException) |
app/Modules/MunicipalManagement/Services/MunicipalManagementWriteService.php | createAlerte()/updateAlerte()/desactiverAlerte() réellement implémentées ; 11 autres méthodes inchangées |
app/Modules/Actor/Controllers/ActeurModulesController.php | Façade — voir §1, §2, §3 ; import Mairie\Models\MairieAlerte supprimé |
tests/Feature/MunicipalManagement/MunicipalManagementFoundationTest.php | 2 tests ajustés : getAlertes/createAlerte ne font plus partie de l'échantillon « lève LogicException » (remplacés par getServices/createService, toujours non implémentées) |
Aucun autre fichier applicatif touché — ni Mairie, ni Territory, ni Admin, ni aucune migration, ni aucun frontend.
8. Tests créés
MunicipalAlertDelegationTest.php, 12 tests :
lecture_publique_retourne_les_alertes_de_lacteurlecture_ne_retourne_pas_les_alertes_dun_autre_acteurcreation_par_owner_avec_module_active_delegue_a_municipal_management(délégation + preuve d'écriture réelle en base)creation_sans_permission_manage_alertes_retourne_403creation_avec_module_alertes_desactive_retourne_403creation_par_admin_plateforme_bypasse_permission_et_modulecreation_sur_acteur_non_mairie_retourne_403modification_par_owner_delegue_a_municipal_managementmodification_dune_alerte_dun_autre_acteur_retourne_404(preuve que le paramètre$acteurIdajouté au contrat, §4, protège réellement contre un accès cross-acteur)desactivation_par_owner_delegue_a_municipal_managementdesactivation_sans_permission_retourne_403_et_ne_modifie_rienle_controleur_actor_ne_contient_plus_de_logique_metier_alertes— garde-fou demandé explicitement par la mission : scanne le code source du contrôleur à la recherche deMairieAlerteou d'un accès direct àDB::table('mairie_alertes'), et vérifie la présence des deux contrats.
Plus 2 tests ajustés dans MunicipalManagementFoundationTest.php (§7).
9. Validations exécutées
| Commande | Résultat |
|---|---|
php -l sur tous les fichiers créés/modifiés | OK |
php artisan about (bootstrap complet) | OK |
Résolution container de ActeurModulesController (nouvelles dépendances) via php artisan tinker | OK |
php artisan route:list --except-vendor | grep alertes | 8 routes, strictement identiques avant/après (4 Actor + 4 Mairie) |
php artisan route:list --except-vendor | grep municipal | Vide — aucune route publique créée |
vendor/bin/pint --test sur tous les fichiers créés/modifiés | OK (1 correction automatique appliquée, revérifiée) |
php artisan test --filter=ActorAccessPhaseOneTest | 19 tests, 37 assertions — vert, y compris le test préexistant test_mairie_actor_can_keep_using_actor_alert_route |
php artisan test --filter=MunicipalAlertDelegationTest | 12 tests, 35 assertions — vert |
php artisan test --filter=MunicipalManagementFoundationTest | 7 tests, 11 assertions — vert |
php artisan test (suite complète) | 468 tests, 1736 assertions, 0 échec (456 préexistants après ENG-001.5A + 12 nouveaux) |
vendor/bin/phpstan analyse | Absent du projet, comme constaté à chaque mission précédente |
composer analyse | Non défini dans composer.json |
10. Divergences
- §0 — fichier de lecture obligatoire manquant, non bloquant (déjà signalé en ENG-001.5A).
- §4 — deux extensions de contrat par rapport à Municipal-A (
getAlertesByActeur, paramètre$acteurIdsurupdateAlerte/desactiverAlerte), nécessaires pour préserver exactement le comportement existant (clé de lecture par acteur, protection contre l'accès cross-acteur). Documentées avant implémentation, pas après ; aucune des deux signatures n'était encore consommée par une PR mergée.
Aucune autre divergence. Aucune décision d'ownership nouvelle : la table mairie_alertes reste
physiquement dans le domaine de données historique de Mairie, seule la responsabilité applicative
de lecture/écriture est désormais portée par Municipal Management, conformément à l'objectif de
cette PR.
11. Confirmations
Aucune route publique n'a changé. Les 4 routes Actor (GET/POST /acteurs/{acteurId}/alertes,
PATCH /acteurs/{acteurId}/alertes/{alerteId}, PATCH .../desactiver) et les 4 routes Mairie
existent avec les mêmes URIs, mêmes verbes, mêmes noms qu'avant cette PR — vérifié par
php artisan route:list. Zéro route créée pour Municipal Management.
Aucun frontend ne nécessite de modification. La façade préserve : mêmes URLs, mêmes payloads
de requête (mêmes règles de validation), même forme de réponse JSON (mêmes clés, même format de
date). Les trois consommateurs identifiés par ENG-001-5-alert-api-consumer-audit.md
(dmv-workspace en écriture, dmv-public en lecture ×2) continuent de fonctionner sans
adaptation — non re-testé en conditions réelles dans cette mission (hors périmètre : uniquement
dmv_api), mais aucun changement de contrat HTTP observable ne le justifierait.
Aucun commit n'a été créé. Branche feature/eng-001-5b-municipal-alert-delegation, tous les
fichiers ci-dessus en working tree, aucun git add/git commit exécuté. git status ne montre
que les fichiers de cette mission et de Municipal-A (non committée non plus), plus les deux
fichiers pré-existants sans rapport déjà signalés dans tous les rapports précédents
(database/seeders/DatabaseSeeder.php, database/seeders/ActorNotorietyConfigSeeder.php).