Praktický návod
Jak správně ukládat peníze a ceny do databáze
Částku ukládej přesně, měnu vždy explicitně a zaokrouhlení prováděj na předem určené hranici.
Nejdřív stručně
Peníze nejsou běžný float
Binární float neumí přesně reprezentovat mnoho desetinných částek. Drobné chyby se projeví při součtech, daních a porovnávání, proto se pro peníze nehodí.
Bezpečné jsou dvě běžné reprezentace: přesný decimal jako PostgreSQL NUMERIC, nebo celé číslo v nejmenší používané jednotce. V obou případech musí být součástí hodnoty také měna a pravidlo zaokrouhlení.
Připrav si
Co musíš rozhodnout předem
Datový typ je až důsledek obchodních pravidel. Nejdřív definuj přesnost, měny a okamžik zaokrouhlení.
- Seznam podporovaných měn a počet desetinných míst používaný pro účtování, ceny a mezivýpočty.
- Pravidlo zaokrouhlení pro jednotkovou cenu, daň, řádek dokladu a celkovou částku.
- Rozhodnutí, zda se historický doklad ukládá jako neměnný snapshot místo přepočtu z aktuálního katalogu.
- Rozsah nejvyšší částky a výpočtů, podle kterého zvolíš precision a scale nebo rozsah BIGINT.
Kroky 1 až 3
Zaveď přesný peněžní model
Vyber jednu reprezentaci pro daný kontext a nemíchej v něm desetinné řetězce, floaty a haléře bez převodní hranice.
1. Vyber NUMERIC nebo nejmenší jednotky
- NUMERIC(p, s) ukládá desetinnou hodnotu přesně. Precision a scale zvol podle největší částky a požadovaných mezivýpočtů, ne podle náhodného příkladu.
- BIGINT v nejmenších jednotkách zjednoduší sčítání a porovnávání. Ne všechny měny ale používají dvě desetinná místa, proto převod nehardcoduj jako krát sto.
- Typ PostgreSQL money nepoužívej jako univerzální doménový model. Jeho vstup a výstup závisí na locale a samotný typ nenese kód měny.
- Pro katalogovou cenu s výpočty DPH bývá NUMERIC praktické; pro uzavřené platební částky může být vhodný integer minor amount. Rozhodnutí zdokumentuj.
amount NUMERIC(19, 4) NOT NULL, currency CHAR(3) NOT NULL Oficiální PostgreSQL dokumentace k číselným typům 2. Přenášej částky bez floatu
- Z API a formuláře přijmi desetinnou částku jako validovaný řetězec. V PHP ji neposílej přes float před uložením do NUMERIC.
- Pro výpočty použij decimal nebo money knihovnu s explicitním rounding mode. Řetězec 19.90 musí zůstat přesnou hodnotou 19.90.
- Částku a ISO kód měny drž pohromadě ve value objectu. Nedovol sčítat EUR a CZK bez explicitního směnného kurzu.
- Do databáze neposílej formátované hodnoty s čárkou, měnovým symbolem nebo oddělovačem tisíců. Formátování patří až do výstupu.
new MoneyAmount('19.90', 'CZK') Oficiální PHP dokumentace k přesnosti floatu 3. Urči jedinou hranici zaokrouhlení
- Pojmenuj rounding mode a místo, kde se použije. Zaokrouhlovat po každé mezihodnotě a potom znovu na konci dává jiné výsledky.
- U faktury ulož jednotkovou cenu, množství, sazbu, základ, daň, celkovou částku a měnu tak, jak byly při vystavení schválené.
- Databázové constrainty použij pro formát měny a platný rozsah. Záporná částka může být správná pro dobropis, proto zákaz odvoď z významu sloupce.
- Stejné příklady a rounding mode sdílej mezi backendem, účetním exportem a testy. Frontendový náhled nesmí být jediným výpočtem.
rounding: half-up; scale: 2; apply: invoice line tax Oficiální PHP dokumentace k zaokrouhlení Krok 4
Otestuj přesnost i hranice
Testuj hodnoty, na kterých se reprezentace a zaokrouhlení opravdu lámou, ne jen celé koruny.
-
Sečti problematické desetiny
Ověř přesný výsledek 0.10 + 0.20 a ukládání nul na konci podle zvoleného scale.
SELECT 0.10::numeric + 0.20::numeric; -
Otestuj poloviny a záporné částky
Přidej příklady přesně na hranici zaokrouhlení, dobropis, nulu a maximální povolenou hodnotu.
php bin/phpunit --filter Money -
Porovnej celý doklad
Součet řádků, daní a celku musí odpovídat schválenému účetnímu příkladu a zůstat stejný po načtení z databáze.
Když to zlobí
Nejčastější chyby
19.90 se po cestě změnilo na nepřesný float
Najdi převod na float v requestu, DTO nebo serializaci. Decimal přenášej jako řetězec nebo money value object až k databázovému driveru.
Součet řádků nesedí s celkem faktury
Tým zaokrouhluje na jiné hranici nebo jiným režimem. Definuj, zda se zaokrouhluje řádek, daň nebo až celek, a ulož výsledný snapshot.
Částka v integeru je pro některou měnu stokrát vedle
Převod předpokládá dvě desetinná místa. Použij metadata konkrétní měny a měj explicitní hranici mezi major a minor units.
Databázový typ money se formátuje jinak na serveru
Je závislý na locale. Pro přenositelný model použij NUMERIC nebo BIGINT a samostatný kód měny.
Hotovo
Částky jsou přesné a jejich význam úplný.
Databáze teď ukládá přesnou hodnotu, měnu a obchodně domluvené zaokrouhlení. Stejný model používej od vstupu přes výpočet až po neměnný doklad.