Sådan sender vi funktioner, der rører ved penge, ud uden ved et uheld at ændre nogens pris
En prisfejl er den slags, du ikke kan tage tilbage, når kortet først er trukket. Her er den disciplin, vi bruger til at lægge rabatter, medarbejderpriser og tilbud ind i bookingflowet, uden at en eneste pris flytter sig, før vi vil det.
De fleste fejl koster dig en undskyldning. En prisfejl koster dig en tilbagebetaling, en indsigelse på kortet og en kunde, der ikke længere stoler på tallet på skærmen. Risikoen er skæv, så processen omkring alt, der rører ved penge, er bevidst langsommere end resten af vores arbejde. Sådan ser den ud, med en nylig funktion som eksempel: et kategoritilbud, hvor en salon kan sætte alle ansigtsbehandlinger på tilbud på én gang.
Slået fra som udgangspunkt, i produktion, uden at gøre noget
Den første linje i funktionen er et flag. Det læser en miljøvariabel og står som udgangspunkt på fra. Det betyder, at vi kan merge og deploye koden længe før den gør noget som helst, og at hver eneste pris forbliver byte for byte den samme som dagen før. Der er ingen brag af en lancering at være nervøs over, for lanceringen er et separat, kedeligt skridt, vi tager, når vi er trygge ved det.
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);}Én funktion, én opgave
Regnestykket bag rabatten ligger i én enkelt ren funktion. Den tager cents og en procentsats ind, og den giver cents ud. Den har ingen database, ingen forespørgsel og intet ur, hvilket gør det ligetil at teste hver eneste yderkant af den isoleret: et tilbud på nul, et almindeligt tilbud, en øvre grænse, et negativt tal der aldrig burde kunne opstå. Når regnestykket er én lille funktion, du kan rumme i hovedet, holder du op med at gætte om det.
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);}Regn altid efter på serveren
Browseren viser en pris, så kunden ved, hvad hun kommer til at betale, men det tal er kun og altid en visning. Når en booking kommer ind, henter serveren den rigtige ydelse, finder medarbejderprisen, lægger et eventuelt aktivt kategoritilbud oveni og regner så rabatten ud, alt sammen ud fra data, den selv har hentet. Hvis nogen har rettet i forespørgslen på vejen ind, ændrer det ingenting, for den pris, kortet bliver trukket for, var aldrig den pris, browseren sendte. Det er den allervigtigste vane i kode, der rører ved penge: tillidsgrænsen er serveren, og det er serveren, der laver regnestykket.
Prøv at bryde det, før en kunde gør det
Tests, der består, fortæller dig, at de tilfælde du kom i tanke om virker. De fortæller dig ikke noget om det tilfælde, du overså. Derfor kører vi, før et prisflag bliver tændt, en fjendtlig gennemgang, hvis eneste opgave er at finde en måde at få pengene til at blive forkerte på. Adskilte tjek angriber hver deres vinkel: ét jager enhver vej, der opkræver for meget af kunden, ét jager en vej, der koster forretningen penge, ét kontrollerer, at funktionen virkelig er passiv, når flaget er slået fra, og ét går efter afrunding og skred ved grænserne. Et tilbud oven på en rabatkode oven på et depositum er præcis der, hvor en brøkdel af en cent gemmer sig, så det er der, vi kigger allerhårdest.
Intet af det her er specielt smart. Det er et flag, en lille funktion, en server der laver sit eget regnestykke, og en vane med at prøve at bryde sit eget arbejde, før det bliver sendt ud. Tilsammen er det forskellen på at tilføje en funktion, der rører ved penge, og at ligge søvnløs over den.