ENG-001.3 — Découplage Actor → Monetization (PR-002) — Rapport d'implémentation
| Epic | EPIC-001 — Restructuration de DMV Core |
| Mission | ENG-001.3 — Découplage Actor → Monetization |
| PR cible | PR-002 |
| Dépendance | ENG-001.2 (PR-001) — non commité |
| Statut | Implémenté |
| Comportement fonctionnel | Inchangé (vérifié par tests, y compris le parcours HTTP de bout en bout) |
1. Divergence initiale (historique, résolue)
Une première tentative d'implémentation a été interrompue avant tout changement de code,
conformément à IA-GOUVERNANCE.md (« Gestion des divergences »). Constat : le contrat
SubscriptionManager::subscribe(), introduit par ENG-001.2/PR-001, est conçu exclusivement pour
le flux payant Stripe :
- appel réseau Stripe inconditionnel (
Subscription::create()) ; - dépendance à
acteur.stripe_customer_id, jamais renseigné à la création d'un acteur (nullsystématique) ; - statut
pendingen attente d'un webhook, jamaisactiveimmédiatement.
L'utiliser tel quel pour amorcer l'abonnement gratuit aurait cassé la création d'acteur en
production (appel Stripe avec customer: null) et changé un comportement fonctionnel supposé
rester identique. Un rapport de divergence détaillé (3 options analysées) a été produit et
soumis à l'architecte.
2. Arbitrage validé par l'architecte
Ajout au contrat public SubscriptionManager :
public function ensureFreeSubscription(int $actorId): void;
Sémantique imposée : idempotente, aucun appel Stripe/réseau, création uniquement si aucun
abonnement actif n'existe, statut active immédiat, jamais de remplacement d'un abonnement actif
existant, jamais de rétrogradation d'un abonnement payant, jamais de doublon, aucun modèle
Eloquent exposé dans le contrat public. Retour void confirmé.
Écart assumé, documenté avant implémentation (voir
ENG-001-3-actor-monetization-decoupling.md
« Arbitrage validé ») : la signature implémentée utilise string $actorId, et non int $actorId. Tous les identifiants d'acteur du code, y compris dans les trois autres méthodes de ce
même contrat, sont des chaînes UUID (acteurs.id, colonne uuid) ; un paramètre int aurait
provoqué une TypeError systématique (declare(strict_types=1)) dès le premier appel réel. Ce
n'est pas une réinterprétation de l'ownership, du périmètre ou de la sémantique arbitrée — la
forme du contrat (une seule méthode, un seul paramètre identifiant l'acteur, retour void) reste
strictement celle validée. Traité comme correction de type de Niveau 1
(IA-GOUVERNANCE.md) plutôt que comme nouvelle divergence à soumettre, dans la mesure où le
littéral int rendait la signature demandée non fonctionnelle avec les données réelles du
projet plutôt que d'exprimer un choix de conception alternatif.
3. Implémentation réalisée
Fichiers créés
| Fichier | Contenu |
|---|---|
dmv-docs/docs/19-engineering/specs/ENG-001-3-actor-monetization-decoupling.md | Spec formalisée a posteriori (constat, ownership cible, arbitrage, périmètre, idempotence, critères d'acceptation, tests, exclusions) |
Fichiers modifiés
| Fichier | Changement |
|---|---|
api/app/Modules/Monetization/Contracts/SubscriptionManager.php | Ajout de ensureFreeSubscription(string $actorId): void au contrat, avec docblock explicite sur l'idempotence et l'absence d'appel réseau |
api/app/Modules/Monetization/Services/MonetizationWriteService.php | Implémentation de ensureFreeSubscription() : vérifie l'absence d'abonnement actif (gratuit ou payant) via une requête sur status = 'active', insère plan_id = 'free' / status = 'active' sinon. Aucun appel Stripe |
api/app/Modules/Actor/Services/ActeurWriteService.php | Injection de SubscriptionManager au constructeur ; createActeur() appelle $this->subscriptionManager->ensureFreeSubscription($id) au lieu d'insérer directement dans actor_subscriptions ; suppression des méthodes privées mortes autoSubscribeFree() et syncActorBadge() (cette dernière déjà sans appelant avant cette mission) ; docblock de classe mis à jour |
dmv-docs/docs/19-engineering/specs/ENG-001-3-implementation-report.md | Ce document |
dmv-docs/docs/19-engineering/epics/EPIC-001-dmv-core-restructuration.md | Ligne de backlog ENG-001.3 corrigée (voir §9) |
api/tests/Feature/Monetization/MonetizationTest.php | 6 tests ajoutés pour ensureFreeSubscription() (voir §6) |
Fichiers supprimés
| Fichier | Justification |
|---|---|
api/app/Modules/Actor/Models/ActorSubscription.php | Modèle Eloquent orphelin mappé sur actor_subscriptions (propriété Monetization), présent dans le module Actor sans être référencé nulle part ailleurs dans le code — vérifié par recherche exhaustive (voir §5). Violation adjacente d'ADR-014 principe 11, non provoquée par cette mission mais dans son rayon direct puisqu'elle porte sur la même table |
4. Évolution exacte de SubscriptionManager
interface SubscriptionManager
{
public function createStripeAccount(string $acteurId, string $email): string;
public function subscribe(string $acteurId, string $planSlug): ActorSubscriptionDTO;
public function cancelSubscription(string $acteurId): void;
public function syncActorBadge(string $acteurId): void;
+
+ public function ensureFreeSubscription(string $actorId): void;
}
Aucune autre signature du contrat n'a été modifiée. Aucun autre contrat (ActorReader,
ActorWriter, PublicationReader, PublicationWriter, BoostManager, BoostReader,
TerritoryReader, IdentityReader, CommunityReader, RewardRecorder) n'a été touché.
5. Logique d'idempotence retenue
public function ensureFreeSubscription(string $actorId): void
{
$hasActiveSubscription = DB::table('actor_subscriptions')
->where('acteur_id', $actorId)
->where('status', 'active')
->exists();
if ($hasActiveSubscription) {
return;
}
DB::table('actor_subscriptions')->insert([
'id' => Str::uuid()->toString(),
'acteur_id' => $actorId,
'plan_id' => 'free',
'status' => 'active',
]);
}
La vérification porte sur tout abonnement actif (status = 'active'), pas seulement sur un
abonnement gratuit actif — c'est ce qui garantit à la fois l'absence de doublon et l'absence de
rétrogradation d'un abonnement payant, conformément à la sémantique arbitrée. Le slug 'free'
reprend la convention déjà utilisée sans ambiguïté à neuf autres endroits du code
(MonetizationWriteService, AdminStatsController, AdminDashboardController) : aucun
arbitrage supplémentaire sur l'identité du plan gratuit n'était nécessaire.
Aucun appel Stripe, aucun appel réseau : la méthode n'importe et n'invoque aucune classe du SDK Stripe.
6. Preuve qu'Actor n'écrit plus dans actor_subscriptions
grep -n "actor_subscriptions" api/app/Modules/Actor/Services/ActeurWriteService.phpne retourne plus qu'une occurrence dans un commentaire de docblock, aucune requêteDB::table.grep -rn "Stripe" api/app/Modules/Actor/ne retourne aucune occurrence.- Le modèle
Actor\Models\ActorSubscriptionn'existe plus. createActeur()délègue désormais explicitement :$this->subscriptionManager->ensureFreeSubscription($id).
7. Résultat de l'audit de Actor\Models\ActorSubscription
Recherche exhaustive (grep récursif, insensible à la casse, sur *.php, hors vendor/) de
ActorSubscription dans tout le dépôt : la seule occurrence en dehors du module Monetization
(qui possède son propre modèle Monetization\Models\ActorSubscription, légitime) était la
définition du fichier lui-même — aucun use App\Modules\Actor\Models\ActorSubscription, aucune
relation Eloquent, aucun ::class, aucune référence dans config/, routes/, database/ ou
tests/. Conclusion : aucune référence réelle → modèle supprimé, conformément à la règle de
la mission.
8. Tests et validations exécutés
| Commande | Résultat |
|---|---|
php -l sur tous les fichiers créés/modifiés | OK |
php artisan about (bootstrap complet de l'application) | OK |
Résolution container ciblée (app(SubscriptionManager::class) → MonetizationWriteService, app(ActorWriter::class) → ActeurWriteService, via php artisan tinker) | OK |
vendor/bin/pint --test sur les fichiers créés/modifiés | OK |
vendor/bin/pint --test sur l'ensemble du dépôt | Échec préexistant, hors périmètre, inchangé : database/seeders/ActorNotorietyConfigSeeder.php (déjà signalé non conforme dans le rapport ENG-001.2, non touché par cette mission) |
php artisan test --filter=MonetizationTest | OK — 17 tests, 39 assertions (11 préexistants + 6 nouveaux pour ensureFreeSubscription) |
php artisan test --filter=ActeurWriteTest | OK — 16 tests, 60 assertions, y compris le test de bout en bout create_acteur_cree_abonnement_gratuit (parcours HTTP réel, inchangé, toujours vert) |
php artisan test (suite complète) | OK — 449 tests, 1690 assertions, 0 échec |
vendor/bin/phpstan analyse | Absent du projet (vendor/bin/phpstan inexistant), comme déjà constaté en ENG-001.2 |
composer analyse | Non défini dans composer.json, comme déjà constaté en ENG-001.2 |
Correspondance avec les 7 scénarios de tests exigés par la mission
| # | Scénario exigé | Test |
|---|---|---|
| 1 | Aucun abonnement existant → création active | ensure_free_subscription_cree_abonnement_actif_si_aucun_abonnement |
| 2 | Second appel → aucun doublon | ensure_free_subscription_est_idempotente_au_second_appel |
| 3 | Abonnement gratuit actif existant → aucune modification | ensure_free_subscription_ne_modifie_pas_abonnement_gratuit_actif_existant |
| 4 | Abonnement payant actif existant → aucune rétrogradation | ensure_free_subscription_ne_retrograde_pas_abonnement_payant_actif |
| 5 | Aucun appel Stripe | ensure_free_subscription_n_appelle_jamais_stripe |
| 6 | Création d'acteur conservée, abonnement toujours initialisé | create_acteur_cree_abonnement_gratuit (préexistant, ActeurWriteTest.php, inchangé et toujours vert) |
| 7 | Résolution container de SubscriptionManager | subscription_manager_est_resolu_par_le_conteneur |
9. Divergence annexe : collision de numérotation ENG-001.3 — résolue le 2026-08-01
Le backlog EPIC-001-dmv-core-restructuration.md assignait déjà l'identifiant ENG-001.3 à
« Territory Bounded Context » (« Limiter Territory aux données territoriales »), et le rapport
ENG-001-2-implementation-report.md référençait « ENG-001.3, ENG-001.4, ENG-001.8 » comme
missions futures consommant TerritoryReader. La présente mission, explicitement nommée et
arbitrée « ENG-001.3 — Découplage Actor → Monetization » par l'architecte, entrait en collision
avec cette numérotation préexistante.
Décision de l'architecte (2026-08-01) : ENG-001.3 reste définitivement acté pour Actor →
Monetization. « Territory Bounded Context » devient ENG-001.4. Toutes les missions suivantes du
backlog sont décalées d'un rang de façon cohérente :
| Mission | Ancien numéro | Nouveau numéro |
|---|---|---|
| Territory Bounded Context | (collision avec ENG-001.3) | ENG-001.4 |
| Municipal Management | ENG-001.4 | ENG-001.5 |
| Publication | ENG-001.5 | ENG-001.6 |
| Moderation | ENG-001.6 | ENG-001.7 |
| Search | ENG-001.7 | ENG-001.8 |
| Platform Services | ENG-001.8 | ENG-001.9 |
| Event Bus | ENG-001.9 | ENG-001.10 |
| Validation finale | ENG-001.10 | ENG-001.11 |
Fichiers corrigés en conséquence (mission dédiée de réalignement documentaire, aucun code applicatif touché) :
dmv-docs/docs/19-engineering/epics/EPIC-001-dmv-core-restructuration.md— backlog renumeroté.dmv-docs/docs/19-engineering/README.md— entréesENG-001.3ajoutées à la table Specs (absentes jusqu'ici).dmv-docs/docs/19-engineering/specs/ENG-001-2-implementation-report.md— toutes les références prospectives aux anciens numéros (ENG-001.3àENG-001.9, dans le tableau des contrats créés et dans le texte) corrigées vers la nouvelle numérotation. Le contenu déjà livré par ENG-001.2 (contrats, bindings, validations) n'a pas été modifié — seuls les pointeurs vers des missions futures l'ont été.- Ce document (§9, ce paragraphe).
ENG-001.1 et ENG-001.2 eux-mêmes n'ont subi aucun changement de contenu ou de numérotation.
10. Confirmation qu'aucun autre périmètre n'a été modifié
- Aucune route HTTP créée, supprimée ou modifiée (
git statusne montre aucun fichier sousRoutes/). - Aucune migration créée (
database/migrations/inchangé). - Aucun changement dans Admin, Rewards, Publication, Stripe, les webhooks, les plans/tarifs.
- Aucun événement métier introduit.
- Aucune autre signature de contrat modifiée que celle explicitement arbitrée.
- Les fichiers déjà en attente de commit avant cette mission (issus de ENG-001.2/PR-001 :
contrats
Actor,Community,Identity,Publication,Rewards,Territory, providers,database/seeders/DatabaseSeeder.php,database/seeders/ActorNotorietyConfigSeeder.php) n'ont pas été altérés par cette mission — seulsMonetization/Contracts/SubscriptionManager.phpetMonetization/Services/MonetizationWriteService.php, déjà dans cet ensemble, ont reçu les modifications décrites en §3. - Rien n'a été commité (
git statusuniquement, aucungit add/git commitexécuté).
Risques et suivi
- La collision de numérotation
ENG-001.3(§9) est résolue depuis le 2026-08-01 ; le backlogEPIC-001etENG-001-2-implementation-report.mdreflètent désormais la numérotation définitive. - L'écart de type
int→stringsurensureFreeSubscription(§2) est documenté ; à confirmer explicitement par l'architecte si un doute subsiste. ActeurWriteService::syncActorBadge()était du code mort avant cette mission (aucun appelant) ; sa suppression ne change aucun comportement observable — le badge d'un acteur reste déterminé uniquement parMonetizationWriteService::syncActorBadge(), déjà seul appelé en production (webhooks Stripe).