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.
1. Idempotence HTTP — le plugin Idempotency-Key
Section intitulée « 1. Idempotence HTTP — le plugin Idempotency-Key »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), tableidempotency_keys(migration006).
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.
4. Déduplication des webhooks PSP entrants
Section intitulée « 4. Déduplication des webhooks PSP entrants »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.
En résumé
Section intitulée « En résumé »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.