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é.
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.
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.
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);}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.
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.
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.
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.