Aller au contenu

Idempotence

Sur un système de paiement, rejouer une requête (double-clic, retry réseau, redémarrage du worker) ne doit jamais débiter deux fois. L’idempotence est traitée à trois niveaux.

Pour les requêtes mutantes (POST/PATCH/DELETE), le client envoie un en-tête Idempotency-Key. Le plugin api/src/http/plugins/idempotency.ts :

flowchart TD
  A["Requête avec Idempotency-Key"] --> B{Clé déjà vue ?}
  B -->|oui, même corps| C["Renvoie la réponse mise en cache"]
  B -->|oui, corps différent| D["409 Conflict"]
  B -->|non| E["Exécute le handler"]
  E --> F["onSend: stocke statut + corps (TTL ~24h)"]
  • À l’entrée (preHandler) : si la clé existe et que le hash du corps correspond, on renvoie la réponse mémorisée sans ré-exécuter. Si le corps diffère, c’est un conflit.
  • À la sortie (onSend, succès seulement) : on stocke statut + corps de réponse, avec un TTL (~24 h).
  • Stockage : FirebirdIdempotencyStore (api/src/infrastructure/db/firebird-idempotency-store.ts), table idempotency_keys (migration 006).

2. Idempotence métier — la référence de session

Section intitulée « 2. Idempotence métier — la référence de session »

Créer une session est naturellement idempotent par sa reference marchand. Le use case create-session.ts fait d’abord findByReference(merchantId, reference) : si une session existe déjà pour cette référence, il la retourne au lieu d’en créer une seconde. Deux POST /v1/sessions avec la même référence donnent donc la même session.

3. Idempotence des opérations externes — clés dans l’outbox

Section intitulée « 3. Idempotence des opérations externes — clés dans l’outbox »

Comme une tâche d’outbox peut être rejouée (retry après échec, redémarrage du worker), chaque appel PSP/gift porte une clé d’idempotence stable, dérivée du leg :

Opération Clé d’idempotence (forme)
Débit gift debit:<legId>
Autorisation PSP authorize:<legId>
Capture PSP capture:<legId>

Cette clé est transmise au PSP (ex. idempotencyKey de Stripe). Réessayer une autorisation déjà passée ne crée pas un second paiement côté PSP : on récupère le même résultat.

Un PSP peut envoyer plusieurs fois le même événement. Le use case handle-psp-webhook.ts enregistre chaque eventId reçu (webhook_events, via FirebirdWebhookEventsRepo, migration 011/016). Si l’événement a déjà été vu pour ce marchand, il est ignoré — la session n’est pas mise à jour deux fois.

flowchart LR
  H["HTTP: Idempotency-Key<br/>(table idempotency_keys)"] 
  M["Métier: reference de session<br/>(findByReference)"]
  O["Outbox: debit/authorize/capture:legId<br/>(transmis au PSP)"]
  WH["Webhooks PSP: eventId<br/>(table webhook_events)"]

Chaque couche protège contre un type de rejouage différent : réseau client (HTTP), double création (métier), retry worker (outbox), double notification (webhooks).

À lire ensuite : Outbox & orchestration et Sécurité multi-tenant.