ENG-001.3 — Découplage Actor → Monetization
| Epic | EPIC-001 — Restructuration de DMV Core |
| Mission | ENG-001.3 |
| PR cible | PR-002 |
| Dépendance | ENG-001.2 — Contrats inter-contextes (PR-001) |
| Statut | Implémenté |
| Livrable | Rapport 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 dansactor_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 (
ActorCreatedou équivalent) — piste notée mais non retenue pour cette mission (voir30-dmv-core-migration-roadmap.mdPhase 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) :
- 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.
- Sinon, insérer une ligne
actor_subscriptionsavecplan_id = 'free'etstatus = 'active'immédiatement (aucun état intermédiaire, aucune dépendance à un webhook). - Aucun appel Stripe, aucun appel réseau, dans tous les cas.
- Deux appels successifs sur le même acteur ne produisent jamais de doublon.
- 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
ActeurWriteServicene référence plusactor_subscriptions(niDB::table('actor_subscriptions'), ni le modèleActorSubscription).ActeurWriteServicene 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
- Aucun abonnement existant → création d'un abonnement
free/active. - Second appel (même acteur) → aucun doublon, aucune modification.
- Abonnement
free/activedéjà existant → aucune modification. - Abonnement payant actif existant → aucune modification, aucune rétrogradation.
- Aucun appel Stripe déclenché par
ensureFreeSubscription(). - Création d'acteur de bout en bout (route HTTP) → comportement conservé, abonnement gratuit toujours initialisé.
- Résolution du contrat
SubscriptionManagerpar 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.