Journal
Technique3 min de lecture

Comment nous livrons des fonctionnalités qui touchent à l'argent sans modifier le prix de qui que ce soit par accident

Un bug de tarification, c'est le genre de bug qu'on ne peut plus rattraper une fois la carte débitée. Voici la discipline que nous appliquons pour ajouter des remises, des prix par praticien et des promotions au parcours de réservation sans qu'un seul prix bouge avant que nous l'ayons décidé.

LTL'équipe technique de Bookatu

La plupart des bugs vous coûtent des excuses. Un bug de tarification vous coûte un remboursement, une contestation bancaire, et un client qui ne fait plus confiance au montant affiché à l'écran. Le rapport de force est déséquilibré, donc le processus autour de tout ce qui touche à l'argent est délibérément plus lent que le reste de notre travail. Voici à quoi il ressemble, avec une fonctionnalité récente en exemple : la promotion par catégorie, qui permet à un salon de mettre tous ses soins du visage en promotion d'un coup.

REBUILT ON THE SERVER, EVERY BOOKINGBase priceStaff priceCategory saleBest couponCharged
Le prix de chaque réservation est construit dans le même ordre fixe, et toute la somme est refaite sur le serveur.

Désactivé par défaut, en production, sans rien faire

La première ligne de la fonctionnalité est un flag. Il lit une variable d'environnement et vaut désactivé par défaut. Ça veut dire que nous pouvons fusionner et déployer le code bien avant qu'il ne fasse quoi que ce soit, et chaque prix reste rigoureusement identique à celui de la veille. Il n'y a pas de grand lancement à redouter, parce que le lancement est une étape distincte et ennuyeuse que nous franchissons une fois que nous sommes sûrs de nous.

ts
export const FEATURE_LIVE =  process.env.FEATURE_LIVE === "true"; // Read at the one place it matters. When the flag is off, the branch// is skipped entirely: no extra query, no price change, nothing.if (FEATURE_LIVE) {  const pct = await activeCategorySalePercent(orgId, "service", service.category);  if (pct > 0) fullPriceCents = applyCategorySaleCents(fullPriceCents, pct);}
1Behind a flagoff by default2Merge & deploystill off3Audit passes0 money bugs4Flip the flagthe go-liveLive
Le lancement est une étape distincte et ennuyeuse. Le code est livré bien avant de faire quoi que ce soit.

Une fonction, une mission

L'arithmétique de la remise vit dans une seule fonction pure. Elle prend des centimes et un pourcentage, et elle renvoie des centimes. Elle n'a ni base de données, ni requête, ni horloge, ce qui rend trivial de tester chacun de ses cas limites isolément : une promotion à zéro, une promotion normale, un plafonnement en haut, un négatif qui ne devrait jamais arriver. Quand le calcul tient dans une petite fonction que vous gardez en tête, vous arrêtez de deviner.

ts
export function applyCategorySaleCents(  baseCents: number,  salePercent: number | null | undefined,): number {  const base = cents(baseCents);  const pct = Math.max(0, Math.min(100, Math.round(salePercent ?? 0)));  if (pct === 0) return base;  return cents((base * (100 - pct)) / 100);}

Recalculer sur le serveur, toujours

Le navigateur affiche un prix pour que le client sache ce qu'il va payer, mais ce montant n'est jamais qu'un affichage. Quand une réservation arrive, le serveur charge la vraie prestation, résout le prix du praticien, applique la promotion de catégorie en cours, puis calcule la remise, le tout à partir de données qu'il a récupérées lui-même. Si quelqu'un a modifié la requête en chemin, ça ne change rien, parce que le prix débité sur la carte n'a jamais été celui envoyé par le navigateur. C'est l'habitude la plus importante quand on écrit du code qui manipule de l'argent : la frontière de confiance, c'est le serveur, et c'est le serveur qui fait le calcul.

BROWSERShows a price so the clientknows what they will pay.A display only. Never trusted.requestSERVER · SOURCE OF TRUTH1 · Load the real service2 · Resolve staff + sale price3 · Apply the best coupon4 · Charge exactly that
Le montant affiché par le navigateur n'est qu'un affichage. Le serveur récupère ses propres données et refait le calcul.

Essayez de le casser avant qu'un client ne le fasse

Des tests qui passent vous disent que les cas auxquels vous avez pensé fonctionnent. Ils ne vous disent rien du cas que vous avez manqué. Alors, avant d'activer un flag de tarification, nous lançons une passe adverse dont le seul travail est de trouver un moyen de fausser l'argent. Des contrôles séparés attaquent chacun sous un angle différent : l'un cherche le moindre chemin qui surfacture le client, l'un cherche un chemin qui fait perdre de l'argent à l'entreprise, l'un vérifie que la fonctionnalité est réellement inerte quand le flag est désactivé, et l'un traque les arrondis et les dérives aux bornes. Une promotion empilée sur un coupon appliqué à un acompte, c'est exactement là qu'une fraction de centime se cache, donc c'est là que nous regardons le plus attentivement.

New pricing featureOvercharges?Loses money?Leaks when off?Rounding drift?Clean sweep → GO
Quatre sceptiques, chacun avec sa façon de se tromper. Seul un sans-faute vaut un GO.

Rien de tout ça n'est astucieux. C'est un flag, une petite fonction, un serveur qui fait ses propres calculs, et l'habitude d'essayer de casser son propre travail avant de le livrer. Mis bout à bout, c'est ce qui sépare le fait d'ajouter une fonctionnalité qui touche à l'argent du fait d'en perdre le sommeil.

feature flagscode sécurisé pour les paiementsconception d'un moteur de tarificationvalidation côté serveurfrontière de confiance
Envie de mettre tout ça en pratique ?

Bookatu vous donne une page de réservation à votre image, des acomptes, des abonnements, des cartes cadeaux et des rappels, avec 0 % de commission sur vos réservations.

Commencer gratuitement