jguillaumesio
webdevarchitecture

Arrêtez de rembourser des paiements que vous n'auriez jamais dû encaisser

Le flux encaisser-puis-rembourser est partout, et il est presque toujours mauvais. Autorisez d'abord, faites vos vérifications, puis capturez. Voici la propriété qui le permet.

Autoriser, valider, puis capturer, contre encaisser puis rembourser

J’ai un jour livré un tunnel de paiement qui débitait la carte à l’instant où le client cliquait sur payer, puis validait la commande ensuite. Vérification du stock, validation de l’adresse, une heuristique antifraude, un appel de disponibilité à un tiers. Quand l’un de ces contrôles échouait, le code faisait la chose évidente : il remboursait le paiement.

Ça marchait. Ça générait aussi un filet régulier d’e-mails confus et agacés. “Pourquoi m’avez-vous débité 89 euros pour me rembourser trois jours plus tard ?” Pour le client, un débit suivi d’un remboursement ne se lit pas comme “nous avons détecté un problème”. Ça se lit comme “cette entreprise est douteuse et mon argent est bloqué pour une semaine”.

Le correctif tenait dans une propriété que j’ignorais depuis des années : capture_method: manual.

L’antipattern encaisser-puis-rembourser

Voici le flux que presque tous les tutoriels enseignent, et celui que j’avais livré :

const intent = await stripe.paymentIntents.create({
  amount: 8900,
  currency: 'eur',
  payment_method: paymentMethodId,
  confirm: true,
});

// l'argent a déjà quitté le compte du client

const order = await validateOrder(cart);
if (!order.ok) {
  await stripe.refunds.create({ payment_intent: intent.id });
  throw new Error('Validation de la commande échouée après encaissement');
}

Le problème, c’est le commentaire. Au moment où validateOrder s’exécute, l’argent est parti. Le débit a touché le relevé du client, votre relevé, et dans les cas transfrontaliers une conversion de devise. Si la validation échoue, vous n’annulez pas une erreur, vous émettez un second événement financier qui doit se régler sur son propre calendrier.

Cela a des coûts réels :

  • Le client voit un débit puis un remboursement, à plusieurs jours d’intervalle, et perd confiance
  • Les remboursements peuvent prendre 5 à 10 jours ouvrés pour réapparaître sur le relevé
  • Vous ne récupérez pas toujours tous les frais, et les variations de change font que le montant remboursé peut différer du débit
  • Une série de débits-puis-remboursements est exactement ce que les réseaux de cartes signalent comme un risque

Vous avez tout fait pour être honnête et ça paraît quand même louche. C’est le flux le problème, pas vos intentions.

Autoriser, puis capturer

Les cartes supportent depuis toujours un modèle en deux temps : l’autorisation place une empreinte sur les fonds sans les déplacer, et la capture déplace réellement l’argent. Les hôtels et les loueurs de voitures l’utilisent depuis toujours. L’empreinte reste sur la carte du client, l’argent ne bouge pas tant que vous ne le décidez pas, et si vous ne capturez jamais, l’empreinte expire simplement.

Stripe expose cela avec une seule propriété sur le PaymentIntent :

const intent = await stripe.paymentIntents.create({
  amount: 8900,
  currency: 'eur',
  payment_method: paymentMethodId,
  capture_method: 'manual',
  confirm: true,
});

// les fonds sont AUTORISÉS, pas capturés. Rien n'a bougé.

const order = await validateOrder(cart);

if (order.ok) {
  await stripe.paymentIntents.capture(intent.id);
} else {
  await stripe.paymentIntents.cancel(intent.id);
}

cancel libère l’empreinte. Aucun débit n’est apparu, donc il n’y a aucun remboursement à expliquer. Le client voit une autorisation en attente qui disparaît discrètement, ce à quoi tout porteur de carte est habitué. Vous avez déplacé la logique métier risquée là où elle doit être : entre l’autorisation et la capture, tant que l’argent est encore réversible sans laisser de trace.

Ce que vous obtenez gratuitement

Une fois le paiement autorisé mais non capturé, plusieurs choses deviennent possibles que le flux débit-d’abord rend pénibles.

Capture partielle. Vous pouvez capturer moins que ce que vous avez autorisé. Autorisez 89 euros pour un panier, découvrez qu’un article est en rupture, capturez 64 euros et le reste de l’empreinte se libère automatiquement :

await stripe.paymentIntents.capture(intent.id, {
  amount_to_capture: 6400,
});

Une vraie fenêtre de validation. Stripe conserve une autorisation de carte non capturée pendant environ 7 jours avant qu’elle n’expire d’elle-même. C’est largement suffisant pour une vérification de stock synchrone, et assez pour certains flux asynchrones. Si vous ne capturez jamais, vous n’avez jamais débité.

Une piste d’audit propre. “Autorisé, puis annulé” est une histoire cohérente dans vos journaux comme dans l’esprit du client. “Débité, puis remboursé” est deux événements à réconcilier, et c’est dans cette réconciliation que l’argent disparaît discrètement.

Ce n’est pas propre à Stripe

Le nom de la propriété change, mais tout prestataire de paiement sérieux expose le même modèle autoriser-puis-capturer. Si vous construisez quelque chose d’agnostique au prestataire, c’est le pattern à standardiser :

  • Stripe : capture_method: 'manual' sur le PaymentIntent, puis paymentIntents.capture() ou paymentIntents.cancel()
  • Adyen : configurez un délai de capture manuelle sur le compte ou la requête, puis appelez /payments pour autoriser et /payments/{id}/captures pour capturer
  • Braintree : passez submitForSettlement: false à transaction.sale, puis transaction.submitForSettlement(id) plus tard
  • PayPal : créez la commande avec intent: 'AUTHORIZE', puis capturez l’autorisation

Le vocabulaire diffère, la forme est identique : obtenir une empreinte, faire son travail, régler ou libérer.

Quand ne pas l’utiliser

La capture manuelle n’est pas sans compromis, et c’est le mauvais outil dans quelques cas.

  • Biens numériques instantanés. Si vous livrez à l’instant où le paiement réussit et qu’il n’y a rien à valider, l’aller-retour supplémentaire n’apporte rien. Capturez immédiatement.
  • Abonnements et facturation récurrente. Ils tournent sur leur propre flux de capture automatique. N’ajoutez pas la capture manuelle à un abonnement sans raison précise.
  • Validation plus lente que l’empreinte. Si vos contrôles peuvent dépasser la fenêtre d’autorisation, l’empreinte risque d’expirer avant la capture. Accélérez les contrôles ou repensez le flux.
  • Moyens de paiement non compatibles. Certains moyens de paiement hors carte ne proposent pas d’autorisation et de capture séparées. Vérifiez la compatibilité des moyens que vous acceptez avant de construire dessus.

La règle que je suis maintenant

S’il existe une logique métier qui peut échouer après que le client a payé mais avant que vous acceptiez de garder son argent, cette logique se place entre une autorisation et une capture. Pas avant un débit, et surtout pas après.

Le flux encaisser-puis-rembourser est le défaut de presque tous les exemples en ligne, et c’est précisément pour ça qu’il finit en production. Une seule propriété déplace le travail risqué du bon côté de l’argent, et les e-mails agacés s’arrêtent.

Si les frais de Stripe sont aussi un problème pour votre volume, voir Alternatives à Stripe en Europe pour des prestataires qui supportent le même modèle d’autorisation puis capture.