# Backend — Patterns : Stripe > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet. --- ## Pattern : Provider-Strategy pour intégrations tierces — périmètre complet - Objectif : isoler intégralement la logique propre à un prestataire (Stripe, Brevo, Firebase…) derrière une interface stable, pour éviter la contamination du domaine par le SDK tiers. - Contexte : backend NestJS/TypeScript avec 1+ prestataires externes (paiement, email, storage…). - Quand l'utiliser : dès qu'un service applicatif dépend d'un SDK tiers (et plus encore s'il y a des webhooks). - Quand l'éviter : intégration ponctuelle non critique sans effet de bord (rare) — sinon on perd vite le contrôle. - Avantage : - Testabilité : mock du provider, pas du SDK - Remplacement du prestataire sans refactor "en cascade" - Responsabilités claires : provider = "parle Stripe", service = "parle domaine" - Limites / vigilance : - L'interface doit exposer des **types normalisés** (pas de types Stripe) - Le provider gère aussi les webhooks : validation signature, parsing event, mapping - Validé le : 09-03-2026 - Contexte technique : NestJS v10+ / intégration Stripe (webhooks) — pattern généralisable ### Implémentation (exemple minimal) ```typescript // billing-provider.interface.ts (pas d'import Stripe) export type BillingPlan = 'MONTHLY' | 'ANNUAL'; export type BillingWebhookResult = { userId: string; externalId: string; plan: BillingPlan; status: 'ACTIVE' | 'INACTIVE' | 'CANCELLED'; currentPeriodEnd: Date | null; }; export interface BillingProvider { createCheckoutSession(userId: string, plan: BillingPlan): Promise<{ checkoutUrl: string }>; cancelSubscription(externalId: string): Promise; handleWebhook(rawBody: Buffer, signature: string): Promise; } // billing.service.ts (domaine uniquement) async handleWebhook(rawBody: Buffer, signature: string): Promise { const result = await this.billingProvider.handleWebhook(rawBody, signature); if (!result) return; await this.prisma.subscription.upsert({ /* données normalisées */ }); } ``` --- ## Pattern : Stripe — metadata sur `subscription_data`, pas sur la Session - Objectif : garantir que `userId` (ou tout identifiant métier) soit accessible dans les events `customer.subscription.*`, pas seulement dans `checkout.session.completed`. - Contexte : intégration Stripe Checkout avec webhooks abonnement. - Quand l'utiliser : systématiquement dès qu'on crée une Checkout Session liée à une Subscription. - Risque si ignoré : `metadata.userId` absent des events `customer.subscription.updated/deleted` → silent failure en prod. - Validé le : 09-03-2026 - Contexte technique : Stripe API v17+ / NestJS ### Implémentation ```typescript stripe.checkout.sessions.create({ metadata: { userId }, // pour checkout.session.completed subscription_data: { metadata: { userId } }, // pour customer.subscription.* }); ``` --- ## Pattern : Webhooks entrants — parsing unique (single `constructWebhookEvent`) - Objectif : appeler `constructWebhookEvent` une seule fois par requête, puis router vers des extracteurs purs. - Contexte : endpoint webhook recevant des events de plusieurs types (subscription, pack, facture…). - Quand l'utiliser : dès qu'on a 2+ handlers webhook sur le même endpoint. - Risque si ignoré : double vérification de signature + états partiels possibles (sub OK / pack KO). - Validé le : 09-03-2026 - Contexte technique : Stripe / NestJS ### Implémentation ```typescript // 1. Parser unique — 1 seul constructWebhookEvent(rawBody, sig) → event opaque // 2. Extracteurs purs, sans effet de bord : handleSubscriptionWebhookEvent(event): WebhookResult | null handlePackWebhookEvent(event): PackWebhookResult | null // 3. Orchestrateur unique appelle les extracteurs, persiste les résultats ``` --- ## Pattern : restauration d'achats Stripe en 3 étapes - Objectif : reconstruire un état local cohérent à partir de Stripe sans dépendre d'une hypothèse fragile. - Contexte : flux de restore purchases mobile/web avec état local potentiellement désynchronisé. - Quand l'utiliser : dès qu'un utilisateur peut restaurer des achats depuis un nouveau device ou après désynchronisation. - Quand l'éviter : si l'état Stripe n'est pas la source de vérité. - Avantage : - rend la réconciliation explicite - supporte retries et restaurations tardives - Limites / vigilance : - la pagination Stripe et l'idempotence d'écriture restent obligatoires - Validé le : 10-03-2026 - Contexte technique : Stripe API / backend Node/NestJS ### Implémentation (exemple minimal) ```txt 1. Résolution du customer Stripe (ID persisté en DB, fallback robuste si absent) 2. Reconstruction de l'état Stripe utile au domaine 3. Réconciliation et écritures locales idempotentes ``` ### Checklist - `stripeCustomerId` persistant côté app - Réconciliation explicite documentée - Upsert ou écriture idempotente --- ## Pattern : Sémantique explicite `Trial` vs `Paid` dans Subscription - Objectif : aligner le modèle métier, les guards et les jeux de tests sur une définition unique de l'abonnement payant actif. - Contexte : modèle `Subscription` où `trialEndsAt` matérialise un essai. - Quand l'utiliser : dès qu'un même enregistrement supporte trial et abonnement payant. - Quand l'éviter : si trial et abonnement payant sont modélisés par des entités distinctes. - Avantage : - évite les incohérences silencieuses dans les guards - rend les fixtures et mocks e2e cohérents avec la règle métier - Limites / vigilance : - toute logique `isActive` doit préciser si elle signifie "trial ou paid" ou "paid only" - Validé le : 10-03-2026 - Contexte technique : Backend agnostique / modèle d'abonnement ### Implémentation (exemple minimal) ```txt - Un abonnement payant actif n'est pas seulement status = ACTIVE - Il doit aussi avoir trialEndsAt = null - Les fixtures et mocks e2e d'un abonnement payant fixent toujours trialEndsAt: null ``` ### Checklist - Règle métier explicitée - Guards alignés sur la sémantique choisie - Fixtures et seeds cohérents --- ## Pattern : Prix défini en base → `price_data` inline pour un checkout dynamique - Objectif : faire d'un checkout Stripe one-shot la source de vérité d'un montant qui vit dans NOTRE base (saisi par un admin métier), sans Price pré-créé dans le dashboard Stripe. - Contexte : produit one-shot dont le prix est défini en back-office et non figé dans Stripe. - Quand l'utiliser : checkout `mode: 'payment'` dont le montant est piloté par la base. - Quand l'éviter : récurrent/abonnement (`mode: 'subscription'`) — garder un `Price` recurring pré-créé. - Avantage : - l'admin ne saisit qu'un montant (+ devise) ; aucun objet `Price` à gérer dans Stripe - un changement de prix prend effet immédiatement au prochain checkout - Limites / vigilance : - `unit_amount` est en **centimes** = stockage entier (jamais de flottant) - le montant venant de notre base, ajouter une garde anti-fraude best-effort (voir ci-dessous) - Validé le : 05-06-2026 - Contexte technique : Stripe Checkout / NestJS — app-alexandrie (ux-parcours-3/7 + bo-6) ### Implémentation ```typescript stripe.checkout.sessions.create({ mode: 'payment', line_items: [{ price_data: { currency, // ISO 4217 minuscules, normalisée à l'écriture unit_amount: amountInCents, // entier, centimes product_data: { name }, }, quantity: 1, }], }); ``` ### Garde-fous - **Anti-fraude (best-effort, non bloquant)** : au webhook, comparer `amount_total`/`currency` au montant attendu (retrouvé via metadata) et logguer tout écart (`stripe_amount_suspicious`). Stripe a déjà encaissé → log, pas de blocage. - **Validation devise stricte côté écriture** (ISO 4217 minuscules) sur TOUS les modèles qui pilotent un checkout : un schéma lâche sur l'un et strict sur l'autre = devise invalide passée à Stripe. Défense serveur : défauter la devise quand un montant est posé sans elle (un montant sans devise = produit non achetable).