---
title: "Slik lanserer vi funksjoner som rører penger uten å endre noens pris ved et uhell"
description: "En prisfeil er den typen du ikke kan ta tilbake når kortet først er belastet. Her er disiplinen vi bruker for å legge rabatter, ansattpriser og tilbud inn i bestillingsflyten uten at én eneste pris flytter seg før vi mener det."
url: "https://bookatu.com/nb-no/blog/shipping-money-features-safely"
language: "nb"
section: "Blogg"
category: "Teknikk"
author: "Utviklerteamet i Bookatu"
published: "2026-07-01"
updated: "2026-07-01"
tags: ["feature flags", "trygg kode for betalinger", "design av prismotor", "validering på serveren", "tillitsgrense"]
---

# Slik lanserer vi funksjoner som rører penger uten å endre noens pris ved et uhell

En prisfeil er den typen du ikke kan ta tilbake når kortet først er belastet. Her er disiplinen vi bruker for å legge rabatter, ansattpriser og tilbud inn i bestillingsflyten uten at én eneste pris flytter seg før vi mener det.

> **Kort fortalt**
>
> - Alt som kan flytte en pris lanseres bak et flagg som er av som standard. Koden ligger i produksjon, men gjør ingenting før vi slår den på.
> - Den endelige prisen regnes ut på nytt på serveren ved betaling. Et tall som kom fra nettleseren blir aldri stolt på.
> - Rabattregningen ligger i én liten, ren funksjon med egne tester, så den kan vurderes helt for seg selv.
> - Før en prisfunksjon settes i drift, prøver en rekke fiendtlige sjekker å få den til å ta for mye, ta for lite eller havne under null.

De fleste feil koster deg en unnskyldning. En prisfeil koster deg en refusjon, en tilbakeføring, og en kunde som ikke lenger stoler på tallet på skjermen. Konsekvensene er skjeve, så prosessen rundt alt som rører penger er bevisst tregere enn resten av arbeidet vårt. Slik ser den ut, med en fersk funksjon som eksempel: et kategoritilbud, der en salong kan sette alle ansiktsbehandlinger 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å hver bestilling bygges i samme faste rekkefølge, og hele regnestykket gjøres på nytt på serveren.*

## Av som standard, i produksjon, uten å gjøre noe

Den første linjen i funksjonen er et flagg. Det leser en miljøvariabel og står som standard på av. Det betyr at vi kan flette inn og deploye koden lenge før den gjør noe som helst, og hver eneste pris er byte for byte identisk med dagen før. Det finnes ingen storslått lansering å være nervøs for, fordi lanseringen er et eget, kjedelig steg vi tar når vi er trygge.

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

*Lanseringen er et eget, kjedelig steg. Koden går ut lenge før den gjør noe.*

## Én funksjon, én jobb

Regnestykket for rabatten ligger i én eneste ren funksjon. Den tar imot øre og en prosent, og den returnerer øre. Den har ingen database, ingen forespørsel og ingen klokke, som betyr at det er trivielt å teste hver ytterkant av den isolert: et nulltilbud, et vanlig tilbud, en øvre grense, et negativt tall som aldri skal kunne skje. Når regningen er én liten funksjon du kan holde i hodet, slutter du å gjette på den.

```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 alltid ut på nytt på serveren

Nettleseren viser en pris så kunden vet hva de skal betale, men det tallet er aldri annet enn en visning. Når en bestilling kommer inn, henter serveren den ekte tjenesten, finner prisen for medarbeideren, legger på et eventuelt aktivt kategoritilbud og regner så ut rabatten, alt sammen fra data den har hentet selv. Hvis noen har endret forespørselen på veien inn, endrer det ingenting, fordi prisen kortet belastes med aldri var prisen nettleseren sendte. Dette er den aller viktigste vanen i kode som håndterer penger: tillitsgrensen er serveren, og serveren gjør 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 nettleseren viser er bare en visning. Serveren henter sine egne data og gjør regnestykket på nytt.*

## Prøv å knekke det før en kunde gjør det

Tester som går grønt forteller deg at tilfellene du tenkte på fungerer. De sier ingenting om tilfellet du overså. Så før et prisflagg slås på, kjører vi en fiendtlig gjennomgang som bare har én jobb: å finne en måte å gjøre pengene feil på. Egne sjekker tar hver sin vinkel. Én jakter på enhver vei som tar for mye av kunden, én jakter på en vei som taper penger for bedriften, én sjekker at funksjonen virkelig er død når flagget er av, og én går løs på avrunding og glidning i grensetilfellene. Et tilbud oppå en kupong oppå et depositum er nøyaktig der et brøkøre gjemmer seg, så det er der vi leter hardest.

*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åte å ta feil på. Bare full pott er et GO.*

Ingenting av dette er smart. Det er et flagg, en liten funksjon, en server som gjør sitt eget regnestykke, og en vane med å prøve å knekke sitt eget arbeid før det går ut. Til sammen er det forskjellen på å legge til en funksjon som rører penger og å ligge våken av den.
