Aller au contenu principal

ENG-001.3 — Découplage Actor → Monetization (PR-002) — Rapport d'implémentation

EpicEPIC-001 — Restructuration de DMV Core
MissionENG-001.3 — Découplage Actor → Monetization
PR ciblePR-002
DépendanceENG-001.2 (PR-001) — non commité
StatutImplémenté
Comportement fonctionnelInchangé (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 (null systématique) ;
  • statut pending en attente d'un webhook, jamais active immé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

FichierContenu
dmv-docs/docs/19-engineering/specs/ENG-001-3-actor-monetization-decoupling.mdSpec formalisée a posteriori (constat, ownership cible, arbitrage, périmètre, idempotence, critères d'acceptation, tests, exclusions)

Fichiers modifiés

FichierChangement
api/app/Modules/Monetization/Contracts/SubscriptionManager.phpAjout 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.phpImplé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.phpInjection 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.mdCe document
dmv-docs/docs/19-engineering/epics/EPIC-001-dmv-core-restructuration.mdLigne de backlog ENG-001.3 corrigée (voir §9)
api/tests/Feature/Monetization/MonetizationTest.php6 tests ajoutés pour ensureFreeSubscription() (voir §6)

Fichiers supprimés

FichierJustification
api/app/Modules/Actor/Models/ActorSubscription.phpModè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.php ne retourne plus qu'une occurrence dans un commentaire de docblock, aucune requête DB::table.
  • grep -rn "Stripe" api/app/Modules/Actor/ ne retourne aucune occurrence.
  • Le modèle Actor\Models\ActorSubscription n'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

CommandeRésultat
php -l sur tous les fichiers créés/modifiésOK
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ésOK
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=MonetizationTestOK — 17 tests, 39 assertions (11 préexistants + 6 nouveaux pour ensureFreeSubscription)
php artisan test --filter=ActeurWriteTestOK — 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 analyseAbsent du projet (vendor/bin/phpstan inexistant), comme déjà constaté en ENG-001.2
composer analyseNon 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
1Aucun abonnement existant → création activeensure_free_subscription_cree_abonnement_actif_si_aucun_abonnement
2Second appel → aucun doublonensure_free_subscription_est_idempotente_au_second_appel
3Abonnement gratuit actif existant → aucune modificationensure_free_subscription_ne_modifie_pas_abonnement_gratuit_actif_existant
4Abonnement payant actif existant → aucune rétrogradationensure_free_subscription_ne_retrograde_pas_abonnement_payant_actif
5Aucun appel Stripeensure_free_subscription_n_appelle_jamais_stripe
6Création d'acteur conservée, abonnement toujours initialisécreate_acteur_cree_abonnement_gratuit (préexistant, ActeurWriteTest.php, inchangé et toujours vert)
7Résolution container de SubscriptionManagersubscription_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 :

MissionAncien numéroNouveau numéro
Territory Bounded Context(collision avec ENG-001.3)ENG-001.4
Municipal ManagementENG-001.4ENG-001.5
PublicationENG-001.5ENG-001.6
ModerationENG-001.6ENG-001.7
SearchENG-001.7ENG-001.8
Platform ServicesENG-001.8ENG-001.9
Event BusENG-001.9ENG-001.10
Validation finaleENG-001.10ENG-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ées ENG-001.3 ajouté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 status ne montre aucun fichier sous Routes/).
  • 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 — seuls Monetization/Contracts/SubscriptionManager.php et Monetization/Services/MonetizationWriteService.php, déjà dans cet ensemble, ont reçu les modifications décrites en §3.
  • Rien n'a été commité (git status uniquement, aucun git add/git commit exécuté).

Risques et suivi

  • La collision de numérotation ENG-001.3 (§9) est résolue depuis le 2026-08-01 ; le backlog EPIC-001 et ENG-001-2-implementation-report.md reflètent désormais la numérotation définitive.
  • L'écart de type intstring sur ensureFreeSubscription (§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 par MonetizationWriteService::syncActorBadge(), déjà seul appelé en production (webhooks Stripe).