Aller au contenu principal

ENG-001.3 — Découplage Actor → Monetization

EpicEPIC-001 — Restructuration de DMV Core
MissionENG-001.3
PR ciblePR-002
DépendanceENG-001.2 — Contrats inter-contextes (PR-001)
StatutImplémenté
LivrableRapport d'implémentation ENG-001.3

Ce document formalise a posteriori le constat déjà établi, l'arbitrage déjà validé par l'architecte et l'implémentation déjà réalisée pour PR-002. Il ne relance aucune étude : il consigne une décision prise.

Problème actuel

ActeurWriteService::autoSubscribeFree() (api/app/Modules/Actor/Services/ActeurWriteService.php) insère directement une ligne dans actor_subscriptions à la création de chaque acteur, en reproduisant l'ancien trigger Supabase auto_subscribe_free. actor_subscriptions est une table dont Monetization est le propriétaire cible (ADR-014, ENG-001-1-dmv-core-audit-report.md §3/§4/§8/§10 — PR-002).

Un modèle Eloquent orphelin App\Modules\Actor\Models\ActorSubscription, mappé sur cette même table, existait en plus dans le module Actor sans être appelé nulle part — une seconde violation adjacente d'ADR-014 principe 11 (aucun contexte ne possède le modèle ORM d'un autre).

Une première tentative d'implémentation (avant cette mission) a identifié que 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, dépendance à acteur.stripe_customer_id — jamais renseigné à la création d'un acteur, statut pending en attente de webhook). L'utiliser tel quel pour amorcer un abonnement gratuit aurait cassé la création d'acteur. Ce constat, détaillé avec preuves de code, est conservé dans ENG-001-3-implementation-report.md (section « Corrections apportées » historique) et n'est pas reproduit ici.

Ownership cible

Monetization est l'unique propriétaire de actor_subscriptions, y compris pour l'initialisation de l'abonnement gratuit par défaut. Actor ne connaît ni la table, ni son schéma, ni la notion de plan — il déclenche uniquement une capacité publiée par Monetization.

Conforme à ADR-014 principe 1 (ownership), principe 4 (dépendances via contrats publiés uniquement) et principe 11 (écriture croisée strictement interdite), et à 30-dmv-core-migration-roadmap.md §2 (Monetization possède « Plans, abonnements, boosts, facturation », Actor n'y figure qu'en référence).

Arbitrage validé

Ajout au contrat public SubscriptionManager (api/app/Modules/Monetization/Contracts/SubscriptionManager.php) :

public function ensureFreeSubscription(string $actorId): void;

Cette capacité appartient exclusivement au Bounded Context Monetization. Implémentation : MonetizationWriteService::ensureFreeSubscription().

Écart assumé par rapport au texte initial de l'arbitrage : la signature transmise portait int $actorId. Tous les identifiants d'acteur, dans l'intégralité du code actuel (les trois autres méthodes de ce même contrat comprises — subscribe(string $acteurId, ...), cancelSubscription(string $acteurId), createStripeAccount(string $acteurId, ...), syncActorBadge(string $acteurId)), sont des chaînes UUID, jamais des entiers. acteurs.id est un uuid (Str::uuid()->toString() à la création, voir ActeurWriteService::createActeur()). Un paramètre int provoquerait une TypeError PHP systématique (declare(strict_types=1) actif dans tous les fichiers concernés) dès le premier appel réel — ce n'est donc pas un choix de conception alternatif, mais une correction de type nécessaire à la compilation même de la signature demandée. Traité comme correction d'implémentation de Niveau 1 (IA-GOUVERNANCE.md), documentée ici plutôt qu'improvisée silencieusement. Le retour void de l'arbitrage est conservé inchangé.

Périmètre exact

Inclus :

  • Ajout de SubscriptionManager::ensureFreeSubscription(string $actorId): void.
  • Implémentation dans MonetizationWriteService, sans dépendance Stripe, sans appel réseau.
  • ActeurWriteService::createActeur() appelle ce contrat au lieu d'écrire directement dans actor_subscriptions.
  • Suppression du code mort devenu obsolète dans Actor (autoSubscribeFree() privé, syncActorBadge() privé jamais appelé) une fois le contrat branché.
  • Audit exhaustif et traitement de Actor\Models\ActorSubscription (modèle orphelin).

Exclu (hors périmètre, inchangé) :

  • Abonnements payants, Stripe, webhooks, plans/tarifs.
  • Événements métier (ActorCreated ou équivalent) — piste notée mais non retenue pour cette mission (voir 30-dmv-core-migration-roadmap.md Phase 1.3, non réalisée à ce jour).
  • Admin, Rewards, Publication.
  • Endpoints HTTP (aucune route modifiée).
  • Migrations de base de données (aucun schéma modifié).

Règles d'idempotence

ensureFreeSubscription(string $actorId) :

  1. Si l'acteur possède déjà un abonnement actif, quel que soit son plan (gratuit ou payant), ne rien faire — aucune insertion, aucune mise à jour.
  2. Sinon, insérer une ligne actor_subscriptions avec plan_id = 'free' et status = 'active' immédiatement (aucun état intermédiaire, aucune dépendance à un webhook).
  3. Aucun appel Stripe, aucun appel réseau, dans tous les cas.
  4. Deux appels successifs sur le même acteur ne produisent jamais de doublon.
  5. Un abonnement payant actif n'est jamais rétrogradé ni remplacé par cette méthode.

Le slug 'free' est repris de la convention déjà utilisée de manière cohérente dans neuf autres emplacements du module Monetization et un emplacement d'Admin (MonetizationWriteService, AdminStatsController, AdminDashboardController) — aucune ambiguïté sur l'identité du plan gratuit n'a été trouvée dans le code, aucun arbitrage supplémentaire n'était donc nécessaire sur ce point.

Critères d'acceptation

  • ActeurWriteService ne référence plus actor_subscriptions (ni DB::table('actor_subscriptions'), ni le modèle ActorSubscription).
  • ActeurWriteService ne référence Stripe sous aucune forme.
  • La création d'un acteur produit toujours un abonnement free/active, avec un comportement observable strictement identique à l'implémentation précédente.
  • SubscriptionManager::ensureFreeSubscription() est résoluble depuis le conteneur Laravel.
  • Aucun endpoint, aucune route, aucun schéma de base de données modifié.
  • Tests verts sur les scénarios listés ci-dessous.

Tests attendus

  1. Aucun abonnement existant → création d'un abonnement free/active.
  2. Second appel (même acteur) → aucun doublon, aucune modification.
  3. Abonnement free/active déjà existant → aucune modification.
  4. Abonnement payant actif existant → aucune modification, aucune rétrogradation.
  5. Aucun appel Stripe déclenché par ensureFreeSubscription().
  6. Création d'acteur de bout en bout (route HTTP) → comportement conservé, abonnement gratuit toujours initialisé.
  7. Résolution du contrat SubscriptionManager par le conteneur Laravel.

Exclusions de périmètre (rappel)

Voir « Périmètre exact » ci-dessus. Toute extension (événements métier, Admin, Rewards, Publication, plans payants) sort explicitement du périmètre de PR-002 et doit faire l'objet d'une mission ultérieure distincte.

Références