Strona główna / Blog / Pieniądze
PieniądzeSześć błędów walutowych w podziale wydatków
Audyt z 2026-08-21 wykrył sześć wad walutowych, przez które nasze testy przechodziły na zielono, a przyczyną było jedno zdanie w dokumentacji, które po cichu przestało być prawdziwe.
Jedno nieaktualne zdanie w dokumentacji kosztowało sześć błędów walutowych. Audyt wielowalutowy Dimesum z 2026-08-21 znalazł je w kodzie, przez który nasze zestawy testów przechodziły na zielono, w tym na stronie roszczenia, która pokazywała dług ¥6,000 jako -₹60.00. Zdanie brzmiało „grupy są przypięte do INR”. Prawdziwe do W7, fałszywe w chwili wdrożenia, i wciąż obecne w FOLLOWUPS.md tydzień później.
Nieaktualne zdanie jest groźniejsze niż brak dokumentacji. Późniejszy audyt mu wierzy i pomija ścieżki, które opisuje, więc błędny wiersz wyrządza szkodę, jakiej cisza nigdy by nie zdołała. Pusty dokument odsyła cię do źródła. Błędny odsyła cię zupełnie gdzie indziej.
Zrealizowana notatka jest gorsza niż otwarta
Dimesum odkłada odroczoną pracę w FOLLOWUPS.md, po jednym wierszu na decyzję wraz z powodem odłożenia. Wpis A17 mówił, że grupy są przypięte do INR, że wyjazdu za granicę nie da się zapisać w walucie, w której nastąpił, oraz że poprawka czeka na decyzję założyciela. W7 i tak wdrożyło obsługę wielu walut: wydatek może być w dowolnej walucie, a salda są kluczowane przez (group_id, member_id, currency) w migracji ledger/00005. Nikt nie skreślił tego wiersza.
Kolejny audyt przeczytał A17, uznał, że obszar nie został zbudowany, i nigdy nie otworzył kodu rozliczeń, roszczeń ani strony docelowej. Trzy ścieżki pieniężne ślepe na walutę wciąż trafiały na produkcję na podstawie jednego zdania. Żaden trudny problem nie stał na przeszkodzie. Docstring pliku app/amount.py niósł to samo twierdzenie, więc czytelnik usłyszał je dwa razy.
Skreśl notatkę w tym samym commicie, który ją zamyka. Wiersz opisujący kod, który przestał tak działać, to przekierowanie z dala od pliku, który trzeba było przeczytać, a to kosztuje więcej niż milczenie.
Ta sama zaszyta na stałe wartość 100 pojawiła się w trzech różnych plikach
Pieniądze to tutaj jednostki podrzędne int64 plus kod ISO 4217, a liczby zmiennoprzecinkowe nigdy ich nie dotykają. Liczby całkowite są z definicji dokładne, więc jedyna ryzykowna arytmetyka, jaka zostaje, to przeliczanie między jednostkami głównymi a podrzędnymi. Jednostka podrzędna nie zawsze jest setną częścią. JPY nie ma jej wcale, KWD ma trzy miejsca po przecinku, a każda zaszyta na stałe wartość 100 to zakład, że użytkownik został w domu.
Plik app/amount.py w pobocznym module w Pythonie zawierał pierwsze wystąpienie, usunięte przed audytem jako mina, a nie jako aktywna wada. Mnożył każdą liczbę odczytaną ze zdania przez 100, więc paragon ¥1,200 stawał się 120,000 jednostkami podrzędnymi. Odczytuje się to jako ¥120,000. W walucie bez jednostki podrzędnej stała zawyża czyjś rachunek stukrotnie, a funkcja teraz zamiast tego sprawdza wykładnik dla żądanej waluty.
Audyt znalazł tę samą stałą na stronie, na której osoba decyduje, czy zaakceptować saldo. formatMinor, obsługujący stronę roszczenia pod /m/{token}, dzielił przez 100 i doklejał z przodu znak rupii. Dług ¥6,000 wyświetlał się jako -₹60.00: zły symbol, setna część kwoty. Jego odpowiednik w JSON /v1/claims zawodził razem z nim, spłaszczając wiersze członka rozbite na waluty do jednego i zachowując tę walutę, którą mapa zapisała jako ostatnią.
Trzeci plik to internal/ingestion/store.go, a znalazło go W8 podczas rozszerzania wykrywania duplikatów, nie audyt. Jego tolerancja „plus minus jedna rupia” była stałą 100, jedną rupią liczoną w paise, pasmem plus minus ¥100 tam, gdzie JPY nie ma żadnej jednostki podrzędnej. Wszystkie trzy odczytują teraz wykładnik.
Porównywanie gołych liczb całkowitych sprawiało, że jen wyglądał jak rupia
Warstwa deduplikacji Dimesum sprawdza nowy wpis względem ostatnich wydatków, aby zaimportowany paragon nie obciążył dwukrotnie zamówienia wpisanego ręcznie. Zapytanie obok tego pasma tolerancji nie miało w ogóle filtra waluty. Więc ¥1,000 pasowało do ₹1,000, a import proponował zastąpienie wydatku, z którym nie miał nic wspólnego. Grupy były wielowalutowe od ledger/00005, więc przypadek był osiągalny, a nie teoretyczny.
Gołe jednostki podrzędne nie są porównywalne między walutami, tolerancje również nie. Projekcja filtruje teraz po walucie i wyprowadza swoje pasmo z jednej jednostki głównej porównywanej waluty.
Dwa silniki odpowiadały na pytanie „kto komu płaci”, a drugi był ślepy na walutę
Kosztowna wada jest strukturalna, nie arytmetyczna. Ścieżka zapisu rozliczeń planowała na internal/platform/simplify, drugim silniku minimalnego przepływu gotówki, którego struktura Balance nie miała pola waluty. Zerowanie się salda w obrębie każdej waluty zeruje też sumę płaską, więc dług ¥300,000 i dług ₹500 znosiły się do zera i żaden strażnik nie odrzucił planu. Plan łączył wtedy wierzyciela w jenach z dłużnikiem w rupiach.
Zapis pogarszał sprawę. Usługa stemplowała Currency: g.DefaultCurrency na każdym rozliczeniu, więc dług ¥300,000 autoryzował wpis księgowy w INR, a długu w jenach nie dało się w ogóle rozliczyć.
simplify jest usunięty, a /settle-plan wycofany. internal/platform/settle to teraz jedyny silnik: dzieli waluty na wymienialne i niewymienialne, przelicza salda netto raz i kieruje każdą niewymienialną walutę w jej własnym nominale. Rozliczenie podaje walutę, którą rozlicza, a to pole jest wymagane, gdy grupa ma więcej niż jedną.
Limit równy zero odczytany jako brak limitu
Strażnik nadpłaty odrzuca płatność większą niż dług, który rozlicza. Strażnik czytał outstanding > 0 && amount > outstanding, więc limit równy zero całkowicie pomijał porównanie. Ktoś rejestrujący płatność wobec długu, który nie istnieje, to jedyny przypadek, dla którego reguła istnieje, i to właśnie ten przypadek przechodził bez przeszkód.
Poprawką jest typ, nie warunek. OutstandingMinor jest teraz *int64, więc „nikt tego nie policzył” nie da się zapisać tak samo jak „odpowiedź to zero”. Nil pomija sprawdzenie i oznacza naprawdę nieznane; każdy wywołujący z dostępem do księgi przekazuje prawdziwą liczbę.
Dlaczego zielony zestaw testów niczego nie dowodził
Każda atrapa w tych ścieżkach używała INR. Porównanie ślepe na walutę jest niewidoczne w teście jednowalutowym, ponieważ przy jednej walucie nie ma czego pomylić. Zestawy nie były słabe, były wąskie, a audyt, który by je poszerzył, został odprawiony przez A17.
Kompleksowy weryfikator księgi dzielił tę ślepotę, i to jest część warta zapamiętania. Weryfikator sumował księgowania na członka bez grupowania według waluty, więc podnosił fałszywy alarm na zdrowej grupie dwuwalutowej i sumował do zera na grupie zepsutej dwukrotnie. Fałszywy negatyw to niebezpieczny kierunek. Kontroler zbudowany na tym samym założeniu co kod zawsze zgodzi się z kodem.
| Wada | Gdzie | Co dostawała grupa JPY | Poprawka |
|---|---|---|---|
Struktura Balance bez waluty | platform/simplify | wierzyciel w jenach połączony z dłużnikiem w rupiach | simplify usunięty; settle dzieli według waluty |
| Domyślna waluta grupy stemplowana na każdym rozliczeniu | usługa rozliczeń | dług ¥300,000 autoryzował wpis księgowy w INR | rozliczenie podaje walutę, którą rozlicza |
outstanding > 0 w strażniku nadpłaty | usługa rozliczeń | limit równy zero stawał się brakiem limitu | *int64: nil oznacza nieznane, zero oznacza zero |
| Dzielenie przez 100 ze znakiem rupii | bramka formatMinor | dług ¥6,000 wyświetlany jako -₹60.00 | symbol i miejsca po przecinku z waluty |
| Salda rozbite na waluty spłaszczone do jednego wiersza | /v1/claims | ta waluta, którą mapa zapisała jako ostatnią | jeden wpis na walutę, zgodnie z GET /balances |
| Księgowania sumowane na członka, waluta pominięta | kompleksowy weryfikator księgi | grupa zepsuta dwukrotnie raportowana jako zrównoważona | grupowanie według członka i waluty |
Przeszukaj swój kod pieniężny grepem pod kątem literału 100
Przeszukaj go pod kątem tej stałej oraz pod kątem każdego porównania, które zestawia dwie kwoty obok siebie bez waluty przy nich. Potem napraw tańszą rzecz, tę, która zapobiega kolejnym sześciu: skreśl notatkę w tym samym commicie, który ją zamyka. I usuń zduplikowany silnik zamiast go naprawiać. Dwie odpowiedzi na pytanie „kto komu płaci” to sposób, w jaki jedna z nich pozostaje błędna.
Często zadawane pytania
Na czym polega wielowalutowe dzielenie wydatków?
Wielowalutowe dzielenie wydatków zapisuje każdy wydatek w walucie, w której nastąpił, i utrzymuje osobne saldo dla każdej waluty, zamiast przeliczać wszystko na jedną. Dimesum kluczuje salda według grupy, członka i waluty, więc dług w jenach i dług w rupiach nigdy nie zlewają się w jedną liczbę. Przeliczenie to widok używany do planu rozliczenia, nigdy przechowywana kwota.
Dlaczego zaszyta na stałe wartość 100 jest groźna w kodzie pieniężnym?
Zaszyta na stałe wartość 100 zakłada, że każda waluta ma dwa miejsca po przecinku, a kilka nie ma. JPY nie ma jednostki podrzędnej, więc mnożenie przez 100 zamienia paragon ¥1,200 w 120,000 jednostek podrzędnych, czyli stukrotne zawyżenie, a nie błąd zaokrąglenia. KWD ma trzy miejsca po przecinku, więc ta sama stała myli się dziesięciokrotnie. Zamiast tego odczytaj wykładnik ISO 4217.
Jak powstrzymać dwa silniki rozliczeń przed rozbieżnością?
Usuń jeden z dwóch silników zamiast je uzgadniać, ponieważ druga odpowiedź na pytanie, kto komu płaci, to sposób, w jaki pierwsza pozostaje błędna. Dimesum uruchamiał simplify i settle obok siebie, dopóki audyt nie wykrył, że pierwszy nie miał waluty w typie salda, co pozwalało planowi łączyć wierzyciela w jenach z dłużnikiem w rupiach. simplify został usunięty, a /settle-plan wycofany zamiast łatany.
Dlaczego zestaw testów pozostawał zielony mimo sześciu błędów walutowych?
Zestaw testów pozostawał zielony, ponieważ każda atrapa w dotkniętych ścieżkach używała jednej waluty, a porównanie ślepe na walutę nie może zawieść, gdy jest tylko jedna waluta. Kompleksowy weryfikator księgi trzymał to samo założenie: sumował księgowania na członka bez grupowania według waluty, więc raportował grupę zepsutą dwukrotnie jako zrównoważoną. Kontroler zbudowany na własnym założeniu kodu zgadza się z kodem.
Co zrobić z notatką o odroczonej pracy, gdy funkcja trafi na produkcję?
Skreśl notatkę w tym samym commicie, który zamyka opisaną przez nią pracę. Notatka, która po cichu się zrealizowała, jest gorsza niż otwarta, ponieważ kieruje późniejszego czytelnika z dala od kodu, który kiedyś opisywała. Dimesum zostawił wpis mówiący, że grupy są przypięte do INR, przez tydzień po wdrożeniu obsługi wielu walut, a kolejny audyt pominął trzy ścieżki pieniężne na jego podstawie.
Popularne wpisy
- Dopisywalna księga dla dokładnych sald wydatków8 min czytania
- Edycja wydatku musi na nowo określić podział8 min czytania
- Rozliczenie: wspólne wydatki w kilku przelewach4 min czytania
- Jak sprawiedliwie podzielić rachunek w restauracji8 min czytania
- Jak sprawiedliwie podzielić czynsz między współlokatorów5 min czytania