Files
MaksTinyWorkshop 9b7af9f1b0 Refonte Structure
2026-03-25 08:34:19 +01:00

6.4 KiB

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)

// 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<void>;
  handleWebhook(rawBody: Buffer, signature: string): Promise<BillingWebhookResult | null>;
}

// billing.service.ts (domaine uniquement)
async handleWebhook(rawBody: Buffer, signature: string): Promise<void> {
  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

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

// 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)

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 SubscriptiontrialEndsAt 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)

- 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