mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 10:03:40 +02:00
f1b783407a
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>
201 lines
8.4 KiB
Markdown
201 lines
8.4 KiB
Markdown
# Backend — Patterns : Stripe
|
|
|
|
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
|
|
|
---
|
|
|
|
<a id="pattern-provider-strategy-integrations-tierces"></a>
|
|
|
|
## 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<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 */ });
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
<a id="pattern-stripe-subscription-metadata"></a>
|
|
|
|
## 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.*
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
<a id="pattern-webhook-parsing-unique"></a>
|
|
|
|
## 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
|
|
```
|
|
|
|
---
|
|
|
|
<a id="pattern-restauration-achats-stripe"></a>
|
|
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="pattern-subscription-trial-vs-paid"></a>
|
|
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="pattern-price-data-inline-checkout-dynamique"></a>
|
|
|
|
## 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).
|