Aller au contenu principal

ENG-001.5B — Délégation des alertes Actor vers Municipal Management — Rapport d'implémentation

MissionMunicipal-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ôtdmv_api
Branchefeature/eng-001-5b-municipal-alert-delegation (basée sur main + fondations Municipal-A non commitées)
StatutImplémenté
Comportement fonctionnelInchangé 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 :

AvantAprè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 (contenu nullable, expires_at sans contrainte after:now, is_active respecté à 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(), permission manage_actor), strictement inchangée.
  • findMairieActeurOrFail() — vérifie kind === '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)

  1. MunicipalManagementReader::getAlertesByActeur(string $acteurId): Collection — ajoutée. getAlertes(string $communeId) (Municipal-A) ne suffisait pas : la route Actor lit par acteur_id, pas par commune_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.
  2. 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 faisait ActeurModulesController avant 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 de MunicipalManagementReadService).
  • 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ôle owner (qui possède manage_alertes par 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_actor mais pas de manage_alertes obtient désormais 403 mairie_permission_denied sur 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 alertes est désactivé obtient désormais 403 mairie_module_disabled sur 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

FichierContenu
tests/Feature/MunicipalManagement/MunicipalAlertDelegationTest.php12 tests (voir §8)
dmv-docs/docs/19-engineering/specs/ENG-001-5B-implementation-report.mdCe document

7. Fichiers modifiés

FichierChangement
app/Modules/MunicipalManagement/Contracts/MunicipalManagementReader.phpAjout getAlertesByActeur() (voir §4)
app/Modules/MunicipalManagement/Contracts/MunicipalManagementWriter.phpSignatures updateAlerte()/desactiverAlerte() modifiées (voir §4)
app/Modules/MunicipalManagement/Services/MunicipalManagementReadService.phpgetAlertes()/getAlertesByActeur() réellement implémentées ; getServices/getElus/getCollectes/getInfos inchangés (toujours LogicException)
app/Modules/MunicipalManagement/Services/MunicipalManagementWriteService.phpcreateAlerte()/updateAlerte()/desactiverAlerte() réellement implémentées ; 11 autres méthodes inchangées
app/Modules/Actor/Controllers/ActeurModulesController.phpFaçade — voir §1, §2, §3 ; import Mairie\Models\MairieAlerte supprimé
tests/Feature/MunicipalManagement/MunicipalManagementFoundationTest.php2 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 :

  1. lecture_publique_retourne_les_alertes_de_lacteur
  2. lecture_ne_retourne_pas_les_alertes_dun_autre_acteur
  3. creation_par_owner_avec_module_active_delegue_a_municipal_management (délégation + preuve d'écriture réelle en base)
  4. creation_sans_permission_manage_alertes_retourne_403
  5. creation_avec_module_alertes_desactive_retourne_403
  6. creation_par_admin_plateforme_bypasse_permission_et_module
  7. creation_sur_acteur_non_mairie_retourne_403
  8. modification_par_owner_delegue_a_municipal_management
  9. modification_dune_alerte_dun_autre_acteur_retourne_404 (preuve que le paramètre $acteurId ajouté au contrat, §4, protège réellement contre un accès cross-acteur)
  10. desactivation_par_owner_delegue_a_municipal_management
  11. desactivation_sans_permission_retourne_403_et_ne_modifie_rien
  12. le_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 de MairieAlerte ou 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

CommandeRésultat
php -l sur tous les fichiers créés/modifiésOK
php artisan about (bootstrap complet)OK
Résolution container de ActeurModulesController (nouvelles dépendances) via php artisan tinkerOK
php artisan route:list --except-vendor | grep alertes8 routes, strictement identiques avant/après (4 Actor + 4 Mairie)
php artisan route:list --except-vendor | grep municipalVide — aucune route publique créée
vendor/bin/pint --test sur tous les fichiers créés/modifiésOK (1 correction automatique appliquée, revérifiée)
php artisan test --filter=ActorAccessPhaseOneTest19 tests, 37 assertions — vert, y compris le test préexistant test_mairie_actor_can_keep_using_actor_alert_route
php artisan test --filter=MunicipalAlertDelegationTest12 tests, 35 assertions — vert
php artisan test --filter=MunicipalManagementFoundationTest7 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 analyseAbsent du projet, comme constaté à chaque mission précédente
composer analyseNon défini dans composer.json

10. Divergences

  1. §0 — fichier de lecture obligatoire manquant, non bloquant (déjà signalé en ENG-001.5A).
  2. §4 — deux extensions de contrat par rapport à Municipal-A (getAlertesByActeur, paramètre $acteurId sur updateAlerte/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).