---
title: "Näin julkaisemme rahaan koskevia ominaisuuksia muuttamatta vahingossa kenenkään hintaa"
description: "Hinnoitteluvirhe on sellainen jota ei voi ottaa takaisin kun korttia on jo veloitettu. Tässä se kuri jolla lisäämme alennuksia, työntekijäkohtaisia hintoja ja tarjouksia varausvirtaan ilman että yksikään hinta liikkuu ennen kuin tarkoitamme niin."
url: "https://bookatu.com/fi-fi/blog/shipping-money-features-safely"
language: "fi"
section: "Blogi"
category: "Tekniikka"
author: "Bookatun tekniikkatiimi"
published: "2026-07-01"
updated: "2026-07-01"
tags: ["ominaisuusliput", "rahaturvallinen koodi", "hinnoittelumoottorin suunnittelu", "palvelinpuolen validointi", "luottamusraja"]
---

# Näin julkaisemme rahaan koskevia ominaisuuksia muuttamatta vahingossa kenenkään hintaa

Hinnoitteluvirhe on sellainen jota ei voi ottaa takaisin kun korttia on jo veloitettu. Tässä se kuri jolla lisäämme alennuksia, työntekijäkohtaisia hintoja ja tarjouksia varausvirtaan ilman että yksikään hinta liikkuu ennen kuin tarkoitamme niin.

> **Lyhyesti**
>
> - Kaikki mikä voi liikuttaa hintaa julkaistaan lipun takana, joka on oletuksena pois päältä. Koodi on tuotannossa mutta ei tee mitään ennen kuin kytkemme sen päälle.
> - Lopullinen hinta lasketaan uudelleen palvelimella maksuhetkellä. Selaimesta tulleeseen lukuun ei koskaan luoteta.
> - Alennuslaskenta asuu yhdessä pienessä puhtaassa funktiossa omine testeineen, joten sitä voi tarkastella erillään.
> - Ennen kuin hinnoitteluominaisuus menee tuotantoon, joukko vihamielisiä tarkistuksia yrittää saada sen veloittamaan liikaa, liian vähän tai menemään nollan alle.

Useimmat bugit maksavat sinulle anteeksipyynnön. Hinnoitteluvirhe maksaa hyvityksen, maksunpalautusriidan ja asiakkaan joka ei enää luota näytöllä olevaan lukuun. Panokset ovat epäsymmetriset, joten kaiken rahaan koskevan ympärillä oleva prosessi on tarkoituksella hitaampi kuin muu työmme. Tältä se näyttää, esimerkkinä yksi tuore ominaisuus: koko kategorian tarjous, jossa hoitola voi laittaa kaikki kasvohoidot tarjoukseen kerralla.

*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

*Jokaisen varauksen hinta rakennetaan samassa kiinteässä järjestyksessä, ja koko summa lasketaan uudelleen palvelimella.*

## Oletuksena pois, tuotannossa, tekemättä mitään

Ominaisuuden ensimmäinen rivi on lippu. Se lukee ympäristömuuttujan ja on oletuksena pois päältä. Se tarkoittaa että voimme yhdistää ja julkaista koodin kauan ennen kuin se tekee mitään, ja jokainen hinta pysyy tavu tavulta samana kuin edellisenä päivänä. Ei ole mitään suurta julkaisuhetkeä josta hermoilla, koska julkaisu on erillinen ja tylsä askel jonka otamme kun olemme luottavaisia.

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

*Julkaisu on erillinen ja tylsä askel. Koodi menee tuotantoon kauan ennen kuin se tekee mitään.*

## Yksi funktio, yksi tehtävä

Alennuksen laskutoimitus asuu yhdessä puhtaassa funktiossa. Se ottaa sisään sentit ja prosentin ja palauttaa sentit. Sillä ei ole tietokantaa, ei pyyntöä eikä kelloa, mikä tarkoittaa että sen jokainen reunatapaus on helppo testata erillään: nollatarjous, tavallinen tarjous, katkaisu yläpäässä ja negatiivinen jota ei pitäisi koskaan tapahtua. Kun laskenta on yksi pieni funktio jonka pitää päässään, sitä lakkaa arvailemasta.

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

## Laske uudelleen palvelimella, aina

Selain näyttää hinnan jotta asiakas tietää mitä maksaa, mutta se luku on aina vain näyttöä varten. Kun varaus saapuu, palvelin lataa oikean palvelun, ratkaisee työntekijäkohtaisen hinnan, soveltaa mahdollisen voimassa olevan kategoriatarjouksen ja laskee sitten alennuksen, kaikki itse hakemastaan datasta. Jos joku muokkasi pyyntöä matkalla, se ei muuta mitään, koska hinta jolla korttia veloitetaan ei koskaan ollut se hinta jonka selain lähetti. Tämä on rahakoodin tärkein yksittäinen tapa: luottamusraja on palvelin, ja palvelin tekee laskun.

*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

*Selaimen näyttämä luku on vain näyttöä varten. Palvelin hakee oman datansa ja tekee laskun uudelleen.*

## Yritä rikkoa se ennen kuin asiakas tekee sen

Läpimenevät testit kertovat että ne tapaukset joita ajattelit toimivat. Ne eivät kerro siitä tapauksesta jonka ohitit. Siksi ennen kuin hinnoittelulippu menee päälle, ajamme vihamielisen kierroksen, jonka ainoa tehtävä on löytää tapa saada rahat väärin. Erilliset tarkistukset ottavat kukin eri kulman: yksi metsästää mitä tahansa polkua joka veloittaa asiakkaalta liikaa, yksi metsästää polkua joka vie yritykseltä rahaa, yksi tarkistaa että ominaisuus on aidosti eloton kun lippu on pois päältä, ja yksi käy pyöristysten ja rajakohtien ryömimisen kimppuun. Tarjous kupongin päällä ennakkomaksun päällä on tasan se paikka johon murto-osasentti piiloutuu, joten sinne katsomme tarkimmin.

*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

*Neljä epäilijää, kukin oma tapansa mennä pieleen. Vain puhdas läpimeno on GO.*

Mikään tästä ei ole nokkelaa. Se on lippu, pieni funktio, palvelin joka tekee omat laskunsa ja tapa yrittää rikkoa oma työnsä ennen julkaisua. Yhdessä ne ovat ero sen välillä että lisää rahaan koskevan ominaisuuden ja sen että menettää sen takia yöunensa.
