---
title: "Sådan sender vi funktioner, der rører ved penge, ud uden ved et uheld at ændre nogens pris"
description: "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."
url: "https://bookatu.com/da-dk/blog/shipping-money-features-safely"
language: "da"
section: "Blog"
category: "Teknik"
author: "Bookatus udviklingsteam"
published: "2026-07-01"
updated: "2026-07-01"
tags: ["feature flags", "sikker kode med penge", "design af prismotor", "validering på serveren", "tillidsgrænse"]
---

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

> **Kort fortalt**
>
> - Alt, der kan flytte en pris, sendes ud bag et flag, der som udgangspunkt er slået fra. Koden ligger i produktion, men gør ingenting, før vi tænder for den.
> - Den endelige pris regnes ud igen på serveren ved betaling. Et tal, der er kommet fra browseren, bliver der aldrig stolet på.
> - Rabatregnestykket ligger i én lille ren funktion med sine egne tests, så det kan gennemskues helt for sig selv.
> - Før en prisfunktion går live, forsøger en række fjendtlige tjek at få den til at opkræve for meget, for lidt eller gå under nul.

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.

*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

*Prisen på enhver booking bygges op i den samme faste rækkefølge, og hele regnestykket laves om på serveren.*

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

```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

*Lanceringen er et separat, kedeligt skridt. Koden sendes ud længe før den gør noget.*

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

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

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

*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

*Tallet, browseren viser, er kun en visning. Serveren henter sine egne data og laver regnestykket om.*

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

*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

*Fire skeptikere, hver med sin måde at tage fejl på. Kun et rent bord er et GO.*

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.
