Files
_Assistant_Lead_Tech/knowledge/backend/risques/stripe.md
T
MaksTinyWorkshop f1b783407a docs(knowledge): capitalisation backend — intégration du triage local (mai-juin 2026)
Triage et intégration des propositions backend du buffer 95_a_capitaliser.md
(lot local RL799_V2 + app-alexandrie, mai-juin 2026), distinct de la capitalisation
remote antérieure (triage 2026-05-02).

~73 entrées intégrées sur knowledge/backend/, dont :
- patterns/auth.md : série "membrane d'auth fédérée BFF/OIDC" (9 patterns) + jose algo whitelist
- patterns/prisma.md : recette fusionnée "Migration String/Int → enum" (backfill + Cas A/B/C),
  row réactivable, endpoint replace atomique, updateMany conditionnel, etc.
- risques/general.md : 19 risques (epoch s vs ms, keepAliveTimeout=0, upsert+filtre liste,
  fail-safe catch-all, retrait asymétrique front/back, anti-énumération rate-limit, etc.)
- patterns/general, async, nestjs, contracts, tests + risques/auth, contracts, prisma, redis, stripe, tests
- compléments d'entrées existantes (authorize-after-fetch, P3014, cursor opaque, DI swc, Stripe v20...)
- README patterns/risques mis à jour

Doublons internes corrigés en relecture (suppression-champ .map() → general seul ;
e2e DB-based → tests.md seul). Doublons hors backend / entrées projet / rejets non intégrés.
Source 95_a_capitaliser.md non purgée à ce stade (purge en fin de capitalisation complète).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:25:02 +02:00

7.2 KiB

Backend — Risques & vigilance : Stripe

Extrait de la base de connaissance Lead_tech. Voir knowledge/backend/risques/README.md pour l'index complet.


Stripe (v17+) : confusion billing_cycle_anchor vs current_period_end

Risques

  • Stocker une date de fin de période incorrecte en DB (bug silencieux)
  • État d'abonnement incohérent (UI, relances, accès premium)

Symptômes

  • currentPeriodEnd correspond à une date "bizarre" (souvent proche de la création), ou à un jour du mois
  • Des accès premium expirent trop tôt / trop tard

Bonnes pratiques / mitigations

  • Ne jamais interpréter billing_cycle_anchor comme une date de fin de période
  • Utiliser subscription.current_period_end (timestamp) pour la fin de période courante
  • Ajouter un test sur un événement webhook/Subscription qui vérifie la date persistée

Nuance SDK v20 (API 2025-03-31.basil+) : current_period_end est par ITEM, plus à la racine

Depuis le SDK Stripe v20, Subscription.current_period_end n'existe plus au niveau racine : lire subscription.items.data[i].current_period_end (prendre le max des items pour borner au plus tard). Symptôme : currentPeriodEnd revient systématiquement null → un abo « ACTIVE » sans borne de période reste ouvert indéfiniment (abo « zombie ») si un event de renouvellement est manqué. Garde-fou complémentaire : isActive = status === 'ACTIVE' && currentPeriodEnd != null && currentPeriodEnd > now. Valider sur un event Stripe réel (l'API effective dépend de la clé/compte).


Stripe list() sans gestion de has_more

Risques

  • Pagination tronquée silencieusement
  • Réconciliation incomplète d'abonnements, achats ou moyens de paiement
  • Décisions métier prises sur un jeu de données partiel

Symptômes

  • Comportement correct sur petits comptes mais faux sur comptes plus chargés
  • Premiers éléments traités, les suivants ignorés
  • Absence de boucle de pagination ou d'auto-pagination

Bonnes pratiques / mitigations

  • Traiter explicitement has_more
  • Utiliser l'auto-pagination Stripe si adaptée
  • Tester au moins un cas avec plusieurs pages de résultats
  • Contexte technique : Stripe API — 10-03-2026

Concurrence entre activation locale et webhook sur transition trial → payant

Risques

  • Double création ou double attachement d'une ressource unique
  • Conflit P2002
  • État local différent de l'état Stripe pendant la transition

Symptômes

  • La transition fonctionne parfois, puis échoue aléatoirement
  • Un webhook Stripe et une action applicative écrivent la même mutation métier
  • Erreurs d'unicité lors de l'activation payante

Bonnes pratiques / mitigations

  • Définir une seule source autorisée pour chaque transition d'état
  • Rendre les écritures idempotentes
  • Sérialiser ou réconcilier explicitement les transitions pilotées à la fois par action utilisateur et webhook
  • Contexte technique : Stripe / Prisma / trial subscription — 10-03-2026

Non-idempotence sur opérations sensibles

Risques

  • Doubles paiements / doubles créations
  • Webhooks rejoués qui cassent l'état

Symptômes

  • Doublons de lignes en DB
  • Actions exécutées 2 fois après timeout/retry
  • Incidents difficiles à reproduire

Bonnes pratiques / mitigations

  • Idempotency key sur endpoints critiques
  • Protection anti-doublon côté DB (contraintes uniques)
  • Comportement défini en cas de retry

Webhooks entrants — répondre 200 pendant processing (event perdu)

Risques

  • Le provider (Stripe, etc.) arrête ses retries après un 2xx, même si le premier worker a échoué
  • Event non appliqué mais marqué "traité" → état incohérent silencieux

Symptômes

  • Webhook reçu, 200 retourné, mais l'état en base n'est pas mis à jour
  • Aucun retry du provider → impossible à détecter sans monitoring actif

Bonnes pratiques / mitigations

  • Lock DB (WebhookEvent) avec machine d'état : pendingprocessingprocessed / failed
  • Si processing détecté (concurrent) : attendre brièvement la transition processed, sinon répondre non-2xx (force retry provider)
  • Ne jamais passer à processed sans preuve d'un traitement effectif
  • Contexte technique : Stripe / NestJS — 09-03-2026

Remboursement lié à (user, produit) au lieu de la TRANSACTION (PaymentIntent)

Risques

  • Un garde-fou « ne pas ré-accorder l'accès si déjà remboursé » identifié par (userId, productId) casse le cas « rembourser puis racheter » : l'ancien refund contamine le nouvel achat
  • Un RACHAT légitime (nouveau paiement, nouveau PaymentIntent) du même produit par le même user est bloqué → client qui re-paie sans accès (perte de revenu + incident)

Symptômes

  • Garde-fou d'idempotence/révocation indexé sur (userId, packId) plutôt que sur le paymentIntentId
  • Un RefundRecord orphelin (refund arrivé avant completed) qui bloque tout achat futur du même produit

Bonnes pratiques / mitigations

  • Un remboursement concerne UNE transaction précise, pas une relation (user, produit) durable. La clé d'un refund et de son garde-fou d'idempotence/ordre = le paymentIntentId (ou l'id de charge), jamais (user, produit).

  • Propager le paymentIntentId depuis checkout.session.completed (session.payment_intent) jusqu'au garde-fou.

  • Corollaire (arrivée désordonnée refund-avant-completed via un RefundRecord) : matcher ce record par paymentIntentId à la création du UserPack, sinon il bloque tout achat futur du même produit.

  • Test obligatoire : achat → refund → rachat (nouveau PI) → accès accordé.

  • Contexte technique : Stripe / refund / webhooks — app-alexandrie 02-06-2026


Éligibilité « refund si peu consommé » mesurée sur la validation, pas le visionnage réel

Risques

  • Une politique de remboursement bornée par la consommation (ex : « < 20 % consommé ») qui mesure un drapeau de VALIDATION EXPLICITE (clic « terminer » → state COMPLETED) est contournable
  • L'utilisateur regarde 100 % du contenu sans cliquer « valider » → completionPct = 0 → remboursable malgré tout le contenu consommé (open-bar)

Symptômes

  • AC qui cite maxWatchedPct mais implémentation qui compte state === 'COMPLETED'
  • Divergence d'oracle entre le badge UI « remboursable » et ce que le serveur accepte réellement

Bonnes pratiques / mitigations

  • Mesurer la consommation EFFECTIVE (progression vidéo maxWatchedPct >= seuil, lecture réelle), JAMAIS un drapeau de validation cliqué par l'utilisateur (COMPLETED).

  • Règle de seuil : leçon vidéo consommée dès maxWatchedPct >= 90 (seuil de complétion vidéo), leçon texte dès COMPLETED.

  • Garder UNE seule source de mesure partagée par la décision serveur ET le badge UI « remboursable » (sinon divergence d'oracle).

  • Contexte technique : Stripe / refund / consommation contenu — app-alexandrie 04-06-2026