---
title: "Jak wdrażamy funkcje dotykające pieniędzy, nie zmieniając nikomu ceny przez przypadek"
description: "Błąd w cenie to taki błąd, którego nie da się cofnąć, gdy karta już została obciążona. Oto dyscyplina, dzięki której dodajemy do rezerwacji rabaty, ceny per osoba i promocje, a żadna cena nie drgnie, dopóki sami tego nie zechcemy."
url: "https://bookatu.com/pl-pl/blog/shipping-money-features-safely"
language: "pl"
section: "Blog"
category: "Technologia"
author: "Zespół inżynierów Bookatu"
published: "2026-07-01"
updated: "2026-07-01"
tags: ["feature flags", "bezpieczny kod finansowy", "projektowanie silnika cen", "walidacja po stronie serwera", "granica zaufania"]
---

# Jak wdrażamy funkcje dotykające pieniędzy, nie zmieniając nikomu ceny przez przypadek

Błąd w cenie to taki błąd, którego nie da się cofnąć, gdy karta już została obciążona. Oto dyscyplina, dzięki której dodajemy do rezerwacji rabaty, ceny per osoba i promocje, a żadna cena nie drgnie, dopóki sami tego nie zechcemy.

> **W skrócie**
>
> - Wszystko, co może ruszyć cenę, wychodzi za flagą domyślnie wyłączoną. Kod jest na produkcji, ale nic nie robi, dopóki go nie włączymy.
> - Ostateczna cena jest liczona ponownie na serwerze przy płatności. Liczbie, która przyszła z przeglądarki, nigdy nie ufamy.
> - Matematyka rabatu mieszka w jednej małej czystej funkcji z własnymi testami, więc da się o niej myśleć w oderwaniu od reszty.
> - Zanim funkcja cenowa ruszy, zestaw wrogich testów próbuje ją zmusić do zawyżenia, zaniżenia albo zejścia poniżej zera.

Większość błędów kosztuje Cię przeprosiny. Błąd w cenie kosztuje zwrot, chargeback i klienta, który już nie wierzy liczbie na ekranie. Stawki są bardzo nierówne, więc proces wokół wszystkiego, co dotyka pieniędzy, jest u nas świadomie wolniejszy niż reszta pracy. Tak to wygląda, na przykładzie świeżej funkcji: promocji na całą kategorię, dzięki której salon przecenia wszystkie zabiegi na twarz naraz.

*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

*Cena każdej rezerwacji powstaje w tej samej ustalonej kolejności, a cała suma jest przeliczana na serwerze od nowa.*

## Domyślnie wyłączone, na produkcji, nic nie robi

Pierwsza linia funkcji to flaga. Czyta zmienną środowiskową i domyślnie jest wyłączona. To znaczy, że możemy scalić i wdrożyć kod długo przed tym, zanim cokolwiek zrobi, a każda cena zostaje co do bajtu taka sama jak dzień wcześniej. Nie ma huczneg premiery, o którą trzeba się denerwować, bo start to osobny, nudny krok, który robimy, gdy jesteśmy pewni.

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

*Start to osobny, nudny krok. Kod trafia na produkcję długo przedtem, zanim cokolwiek robi.*

## Jedna funkcja, jedno zadanie

Arytmetyka rabatu mieszka w jednej czystej funkcji. Bierze grosze i procent, zwraca grosze. Nie ma bazy danych, nie ma żądania, nie ma zegara, więc każdy jej skrajny przypadek testuje się trywialnie w izolacji: promocja zerowa, promocja normalna, ograniczenie u góry, wartość ujemna, która nigdy nie powinna wystąpić. Kiedy matematyka to jedna mała funkcja, którą mieścisz w głowie, przestajesz przy niej zgadywać.

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

## Zawsze przeliczaj na serwerze

Przeglądarka pokazuje cenę, żeby klient wiedział, ile zapłaci, ale ta liczba jest wyłącznie wyświetlaczem. Kiedy przychodzi rezerwacja, serwer wczytuje prawdziwą usługę, ustala cenę danej osoby z zespołu, nakłada aktywną promocję na kategorię i dopiero wtedy liczy rabat, wszystko z danych, które sam pobrał. Jeśli ktoś po drodze podmienił żądanie, nic to nie zmienia, bo cena, którą obciążamy kartę, nigdy nie była ceną wysłaną przez przeglądarkę. To najważniejszy nawyk w kodzie od pieniędzy: granicą zaufania jest serwer i to serwer liczy sumę.

*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

*Liczba, którą pokazuje przeglądarka, to tylko wyświetlacz. Serwer pobiera własne dane i liczy sumę jeszcze raz.*

## Spróbuj to złamać, zanim zrobi to klient

Zielone testy mówią Ci, że przypadki, o których pomyślałeś, działają. Nie mówią nic o tym, który przegapiłeś. Dlatego zanim flaga cenowa zostanie włączona, robimy wrogie przejście, którego jedynym zadaniem jest znaleźć sposób na pomylenie pieniędzy. Osobne testy biorą to z różnych stron: jeden szuka jakiejkolwiek ścieżki, która zawyża rachunek klienta, drugi takiej, która okrada firmę, trzeci sprawdza, czy przy wyłączonej fladze funkcja naprawdę jest martwa, a czwarty czepia się zaokrągleń i dryfu na granicach. Promocja nałożona na kupon przy zaliczce to dokładnie miejsce, w którym chowa się ułamek grosza, więc tam patrzymy najuważniej.

*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

*Czterech sceptyków, każdy z innym sposobem na pomyłkę. Tylko czysty przebieg to GO.*

Nic z tego nie jest sprytne. To flaga, mała funkcja, serwer, który sam liczy, i nawyk psucia własnej pracy, zanim trafi do ludzi. Razem wzięte, robią różnicę między dodaniem funkcji dotykającej pieniędzy a nieprzespanymi nocami z jej powodu.
