---
title: "Comment nous livrons des fonctionnalités qui touchent à l'argent sans modifier le prix de qui que ce soit par accident"
description: "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é."
url: "https://bookatu.com/fr-fr/blog/shipping-money-features-safely"
language: "fr"
section: "Blog"
category: "Technique"
author: "L'équipe technique de Bookatu"
published: "2026-07-01"
updated: "2026-07-01"
tags: ["feature flags", "code sécurisé pour les paiements", "conception d'un moteur de tarification", "validation côté serveur", "frontière de confiance"]
---

# 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é.

> **En bref**
>
> - Tout ce qui peut faire bouger un prix est livré derrière un flag désactivé par défaut. Le code est en production mais ne fait rien tant que nous ne l'activons pas.
> - Le prix final est recalculé sur le serveur au moment du paiement. Un montant arrivé depuis le navigateur n'est jamais digne de confiance.
> - Le calcul de la remise vit dans une seule petite fonction pure avec ses propres tests, donc on peut raisonner dessus isolément.
> - Avant qu'une fonctionnalité de tarification passe en production, une série de contrôles adverses essaie de la faire surfacturer, sous-facturer ou passer sous zéro.

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.

*Diagram: The price pipeline: base price, then staff price, then category sale, then the best coupon, then the amount charged*

Diagram labels: REBUILT ON THE SERVER, EVERY BOOKING · Base price · Staff price · Category sale · Best coupon · Charged

*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);
}
```

*Diagram: The flag lifecycle: written behind a flag that is off by default, merged and deployed while still off, then after the audit passes the flag is flipped and the feature goes live*

Diagram labels: 1 · Behind a flag · off by default · 2 · Merge & deploy · still off · 3 · Audit passes · 0 money bugs · 4 · Flip the flag · the go-live · Live

*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.

*Diagram: The browser only displays a price; the server independently loads the service, resolves the staff and sale price, applies the best coupon, and charges that amount*

Diagram labels: BROWSER · Shows a price so the client · knows what they will pay. · A display only. Never trusted. · request · SERVER · SOURCE OF TRUTH · 1 · Load the real service · 2 · Resolve staff + sale price · 3 · Apply the best coupon · 4 · 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.

*Diagram: One new pricing feature is checked by four adversarial skeptics — does it overcharge, lose money, leak when the flag is off, or drift on rounding — and only a clean sweep is a go*

Diagram labels: New pricing feature · Overcharges? · 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.
