Le compte à rebours du panier a expiré pendant qu'ils payaient. On fait quoi ?
Un compte à rebours qui se déclenche en plein paiement est un bug minuscule au signal énorme : l'ingénieur a optimisé pour le système et oublié qu'il y avait une personne en face. Voici comment le gérer, et l'habitude produit qu'il révèle.
Voici un rapport de bug que j’ai reçu sous une forme ou une autre dans trois entreprises différentes. Le client ouvre le widget de paiement, saisit sa carte, clique sur payer. Pendant que la banque déroule sa danse 3-D Secure, le compte à rebours du panier atteint zéro. Le frontend, faisant exactement ce qu’on lui a dit, détruit la session. Le paiement réussit une seconde plus tard, contre un panier qui n’existe plus. Il y a maintenant de l’argent sur le compte et aucune commande rattachée.
Chaque ingénieur qui a construit ce minuteur l’a construit correctement. Le compte à rebours protège une ressource limitée : un siège de concert, une nuit d’hôtel, la dernière unité en stock. Le garder pour toujours laisserait un onglet abandonné affamer tous les autres clients. Alors on met un minuteur, et quand il se déclenche on libère l’empreinte. Propre, défendable, et complètement aveugle au fait qu’une vraie personne avait déjà engagé son argent.
Cet aveuglement est le vrai sujet ici. Le bug du compte à rebours est petit. L’habitude qui le produit ne l’est pas.
Le minuteur répond à une question que personne ne posait à ce moment
Prenons du recul et demandons à quoi sert le compte à rebours. Il existe pour résoudre une contention : deux personnes veulent le même siège, et l’empreinte décide qui a la priorité. C’est son seul travail. C’est un mécanisme d’équité entre des clients qui n’ont pas encore payé.
À l’instant où un client entre dans le flux de paiement, la question à laquelle le minuteur répond cesse d’être pertinente. La contention est terminée. Cette personne ne navigue pas, n’hésite pas, n’est pas assise sur un onglet qu’elle a oublié. Elle vous tend activement son argent pour la chose exacte que l’empreinte protégeait. Déclencher le minuteur maintenant ne sert aucun autre client, parce qu’aucun autre client ne peut être servi : le siège est sur le point d’être vendu. Ça ne sert que l’abstraction.
C’est ça, le signal. Un ingénieur qui pense en systèmes voit un minuteur arrivé à zéro et une règle qui dit “libérer à zéro”. Un ingénieur qui pense au produit voit une personne qui a tout fait bien et qui est sur le point d’être punie pour la latence de la banque. Même événement, deux lectures complètement différentes, et une seule des deux garde le client.
Alors, on les laisse finir ?
Oui. Presque toujours, oui. Si le paiement est autorisé, honorez-le.
Le raisonnement n’est pas sentimental. Une autorisation réussie est le signal d’intention le plus fort possible, bien plus fort que l’état “empreinte toujours active” que le minuteur gardait. Rejeter un paiement que vous avez déjà accepté crée le pire résultat pour tout le monde : vous devez maintenant rembourser, le client voit un cycle débit-puis-remboursement qui se lit comme “cette entreprise est louche”, et vous avez brûlé de la confiance pour appliquer une règle qui ne protégeait personne. J’ai écrit un article entier sur les raisons pour lesquelles le débit-puis-remboursement est un antipattern ; le forcer en laissant un minuteur tuer un paiement en cours est une version auto-infligée de la même blessure.
Le seul cas où vous ne pouvez vraiment pas l’honorer est la survente réelle : pendant que ce client était dans 3-D Secure, la dernière unité est réellement partie chez quelqu’un qui a fini plus vite. C’est un vrai conflit, pas un artefact de minuteur. Traitez-le comme un échec de stock, avec un remboursement immédiat et automatique et un message honnête, pas comme une erreur “votre temps est écoulé”. L’expérience du client doit être “nous avons eu une seconde de retard et voici votre argent instantanément”, jamais “vous avez été trop lent”.
Les deux questions qui suivent
Une fois que vous acceptez qu’un paiement en cours doit gagner, deux questions de conception suivent, et c’est là que la plupart des implémentations dérapent en silence.
Reverrouillez-vous l’offre juste après qu’ils ont payé ? Non. Celle-ci piège les gens parce que l’instinct est de garder la machine à états cohérente : l’empreinte a expiré, donc en sortant on devrait la ré-acquérir. Mais reverrouiller quelque chose que le client a déjà acheté n’a aucun sens. C’est à lui maintenant. Le paiement est l’état terminal, pas une étape de plus qui a besoin de réserver son créneau. Si votre code reverrouille après la capture, vous avez une machine à états qui ne sait pas que la transaction est finie, et ça produira sa propre classe de bugs (doubles empreintes, disponibilité fantôme) plus tard.
Lancez-vous un minuteur séparé pour l’étape finale ? Oui, et c’est le vrai correctif. Le compte à rebours de navigation et la fenêtre de paiement sont deux horloges différentes mesurant deux choses différentes. Le minuteur du panier gère la contention pendant que le client décide. À l’instant où il s’engage à payer, vous basculez vers une fenêtre de grâce de paiement : un minuteur séparé, plus généreux, dont le seul travail est de laisser à l’autorisation le temps de se résoudre. Redirections bancaires, 3-D Secure, confirmations de portefeuille, réseaux lents : une empreinte de 30 secondes qui paraissait urgente pendant la navigation est absurde quand quelqu’un fixe l’écran de confirmation de son appli bancaire. Donnez à l’étape finale sa propre horloge, dimensionnée pour le temps qu’un vrai paiement prend réellement.
Rendez l’expiration autoritaire côté serveur
Le bug en tête de cet article n’atteint la production que lorsque le minuteur vit dans le navigateur et que le navigateur a le droit de décider que la session est morte. Un compte à rebours frontend est un affichage. Il ne devrait jamais être la chose qui libère le stock.
Placez l’autorité sur le serveur. L’empreinte a un horodatage d’expiration que le serveur possède. Le frontend fait défiler le compte à rebours pour l’humain, mais la décision de libérer est une vérification côté serveur qui s’exécute quand ça compte vraiment : quand quelqu’un d’autre tente de prendre le siège, ou quand le webhook de paiement arrive. Quand le webhook de capture atterrit, le serveur le compare à l’empreinte et au stock, et décide. Un minuteur visuel qui atteint zéro n’est pas un événement qui devrait muter quoi que ce soit. C’est un indice pour l’utilisateur, rien de plus.
Ce seul geste d’architecture, les empreintes expirent sur le serveur, le frontend ne fait qu’afficher, élimine toute la catégorie. Le compte à rebours peut atteindre zéro et se réinitialiser en “traitement de votre paiement” sans jamais toucher à la réservation sous-jacente, parce que la réservation n’a jamais été au frontend de la libérer.
L’habitude derrière le bug
Rien de ce qui précède n’est difficile. La raison pour laquelle ça part cassé si souvent n’est pas la difficulté technique, c’est que le minuteur a été construit comme un pur problème de système. Ressource, contention, TTL, libération. Chaque ligne est correcte et leur somme échoue face à un client, parce que le modèle n’a jamais inclus l’expérience du temps vécue par le client. Pour le système, zéro c’est zéro. Pour la personne, elle était aux trois quarts du paiement.
C’est l’écart ingénieur contre produit, et il n’apparaît pas sur le chemin heureux. Il apparaît exactement dans ces coutures : ce qui se passe quand deux horloges ne sont pas d’accord, quand le réseau est lent, quand l’utilisateur fait la bonne chose au mauvais instant. Ces cas limites sont là où le produit vit réellement, et ils sont la première chose qu’une mentalité purement système laisse tomber, parce que de l’intérieur du système ils ressemblent à des états correctement gérés, pas à un être humain à qui on dit “trop tard” pour quelque chose qu’il a déjà fait.
Le correctif n’est pas “s’en soucier davantage”. C’est de faire d’une question une partie de la définition de terminé pour tout ce qui touche l’utilisateur : que vit la personne en face quand ça se déclenche ? Posez-la au sujet du minuteur et vous inventez la fenêtre de grâce tout seul. Sautez-la et vous livrez une machine à états qui a raison sur tout sauf sur la seule chose pour laquelle elle existait.
La règle que je suis maintenant
Tout minuteur, verrou ou expiration contre lequel un utilisateur peut être en train de travailler activement a besoin de deux choses : un serveur qui possède la vraie échéance, et une réponse à “que se passe-t-il pour quelqu’un en pleine action quand ça se déclenche”. Un compte à rebours qui peut annuler un paiement déjà en cours n’a ni l’un ni l’autre. Donnez à l’étape finale sa propre horloge, gardez l’autorité sur le serveur, et laissez un client engagé finir. L’abstraction n’a pas besoin d’être protégée. La personne, oui.