Journal
Technik3 Min. Lesezeit

Wie wir Funktionen rund ums Geld ausliefern, ohne versehentlich jemandes Preis zu ändern

Ein Preisfehler ist die Sorte Fehler, die sich nicht zurücknehmen lässt, sobald eine Karte belastet wurde. Hier ist die Disziplin, mit der wir Rabatte, Mitarbeiterpreise und Aktionen in den Buchungsablauf bringen, ohne dass sich ein einziger Preis bewegt, bevor wir es wollen.

DBDas Bookatu-Technikteam

Die meisten Fehler kosten dich eine Entschuldigung. Ein Preisfehler kostet dich eine Erstattung, eine Rückbuchung und eine Kundin, die der Zahl auf dem Bildschirm nicht mehr traut. Das Risiko ist einseitig verteilt, deshalb ist der Prozess rund um alles, was Geld berührt, bewusst langsamer als der Rest unserer Arbeit. So sieht er aus, am Beispiel einer neueren Funktion: eine Aktion für eine ganze Kategorie, mit der ein Salon jede Gesichtsbehandlung auf einmal ins Angebot setzen kann.

REBUILT ON THE SERVER, EVERY BOOKINGBase priceStaff priceCategory saleBest couponCharged
Der Preis jeder Buchung entsteht in derselben festen Reihenfolge, und die ganze Summe wird auf dem Server neu gerechnet.

Standardmäßig aus, in der Produktion, ohne Wirkung

Die erste Zeile der Funktion ist ein Schalter. Er liest eine Umgebungsvariable und steht standardmäßig auf aus. Das heißt, wir können den Code lange, bevor er irgendetwas tut, zusammenführen und ausliefern, und jeder Preis bleibt Byte für Byte derselbe wie am Tag davor. Es gibt keinen großen Knall beim Start, vor dem man nervös sein müsste, denn der Start ist ein eigener, langweiliger Schritt, den wir gehen, sobald wir sicher sind.

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
Der Start ist ein eigener, langweiliger Schritt. Der Code geht raus, lange bevor er etwas tut.

Eine Funktion, eine Aufgabe

Die Rechnerei hinter dem Rabatt steckt in einer einzigen reinen Funktion. Sie nimmt Cent und ein Prozent und gibt Cent zurück. Sie hat keine Datenbank, keine Anfrage und keine Uhr, damit lässt sich jeder Grenzfall isoliert ganz einfach testen: keine Aktion, eine normale Aktion, eine Deckelung nach oben, ein negativer Wert, den es nie geben dürfte. Wenn die Rechnung eine kleine Funktion ist, die du im Kopf behalten kannst, hörst du auf, darüber zu rätseln.

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

Immer auf dem Server neu rechnen

Der Browser zeigt einen Preis, damit die Kundin weiß, was sie zahlen wird, aber diese Zahl ist immer nur Anzeige. Kommt eine Buchung herein, lädt der Server die echte Leistung, ermittelt den Mitarbeiterpreis, wendet eine laufende Kategorie-Aktion an und rechnet dann den Rabatt aus, alles aus Daten, die er sich selbst geholt hat. Wenn jemand die Anfrage unterwegs bearbeitet hat, ändert das nichts, denn der Preis, mit dem die Karte belastet wird, war nie der Preis, den der Browser geschickt hat. Das ist die wichtigste Gewohnheit in Code rund ums Geld: Die Vertrauensgrenze ist der Server, und der Server macht die Rechnung.

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
Die Zahl im Browser ist nur Anzeige. Der Server holt sich seine eigenen Daten und rechnet noch einmal.

Versuch es kaputtzumachen, bevor es ein Kunde tut

Bestandene Tests sagen dir, dass die Fälle funktionieren, an die du gedacht hast. Über den Fall, den du übersehen hast, sagen sie nichts. Bevor ein Preisschalter angeht, machen wir deshalb einen feindseligen Durchgang, dessen einzige Aufgabe es ist, einen Weg zu finden, das Geld falsch zu machen. Getrennte Prüfungen nehmen dabei jeweils eine andere Perspektive ein: eine sucht nach jedem Weg, auf dem die Kundin zu viel zahlt, eine nach einem Weg, auf dem das Geschäft Geld verliert, eine prüft, ob die Funktion bei ausgeschaltetem Schalter wirklich untätig bleibt, und eine geht auf Rundung und Abweichung an den Grenzen los. Eine Aktion auf einem Gutschein auf einer Anzahlung ist genau die Stelle, an der sich ein Bruchteil eines Cents versteckt, dort schauen wir also am genauesten hin.

New pricing featureOvercharges?Loses money?Leaks when off?Rounding drift?Clean sweep → GO
Vier Skeptiker, jeder auf eine andere Art falsch zu liegen. Nur ein sauberer Durchgang ist ein GO.

Nichts davon ist raffiniert. Es ist ein Schalter, eine kleine Funktion, ein Server, der seine Rechnung selbst macht, und die Gewohnheit, die eigene Arbeit kaputtmachen zu wollen, bevor sie rausgeht. Zusammengenommen ist das der Unterschied zwischen einer Funktion rund ums Geld und schlaflosen Nächten deswegen.

feature flagssicherer code beim geldaufbau preislogikprüfung auf dem serververtrauensgrenze
Willst du das umsetzen?

Bookatu gibt dir eine Buchungsseite in deinem eigenen Look, Anzahlungen, Mitgliedschaften, Gutscheine und Erinnerungen, mit 0 % Provision auf deine Buchungen.

Kostenlos starten