dimesum

Home / Blog / Geld

Geld

Zes geldfouten bij multi-valuta kostendeling

· 9 min leestijd ·

Een audit op 2026-08-21 vond zes valutafouten waar onze tests groen doorheen waren gekomen, en de oorzaak was één zin in een document die stilletjes niet meer klopte.

Eén verouderde zin in een document kostte zes geldfouten. Een multi-valuta audit van Dimesum op 2026-08-21 vond ze in code waar onze testsuites groen doorheen waren gekomen, waaronder een claimpagina die een schuld van ¥6,000 weergaf als -₹60.00. De zin luidde "groups are INR-pinned". Waar tot W7, onwaar vanaf het moment dat het live ging, en een week later nog steeds aanwezig in FOLLOWUPS.md.

Een verouderde zin is gevaarlijker dan geen documentatie. Een latere audit gelooft die en slaat de paden over die de zin beslaat, dus een verkeerde regel richt schade aan die stilte nooit zou kunnen aanrichten. Een leeg document stuurt je naar de bron. Een verkeerd document stuurt je ergens heel anders heen.

Een follow-up die waar werd is erger dan een openstaande

Dimesum parkeert uitgesteld werk in FOLLOWUPS.md, één regel per beslissing met de reden waarom die geparkeerd is. Notitie A17 zei dat groepen INR-pinned waren, dat een reis naar het buitenland niet kon worden vastgelegd in de valuta waarin die plaatsvond, en dat de oplossing wachtte op een beslissing van de oprichter. W7 leverde multi-valuta toch: een uitgave mag in elke valuta zijn, en balansen worden geïndexeerd op (group_id, member_id, currency) door migratie ledger/00005. Niemand schrapte de regel.

De volgende audit las A17, concludeerde dat het gebied nog niet gebouwd was, en opende nooit de afreken-, claim- of landingscode. Drie valutablinde geldpaden bleven live gaan op basis van één zin. Geen enkel lastig probleem stond in de weg. De docstring van app/amount.py bevatte dezelfde bewering, dus een lezer werd het twee keer verteld.

De regel die we invoerden

Schrap een follow-up in dezelfde commit die hem afsluit. Een regel die code beschrijft die zo niet meer werkt is een omleiding weg van het bestand dat je moest lezen, wat meer kost dan niets zeggen.

Dezelfde hardgecodeerde 100 dook op in drie verschillende bestanden

Geld is hier int64 minor units plus een ISO 4217 code, en floats raken het nooit aan. Gehele getallen zijn per constructie exact, dus de enige riskante rekenkunde die overblijft is de omrekening tussen major en minor units. De minor unit is niet altijd een honderdste. JPY heeft er helemaal geen, KWD heeft drie decimalen, en elke hardgecodeerde 100 is een gok dat de gebruiker thuisbleef.

De app/amount.py van de Python-sidecar bevatte de eerste vondst, verwijderd vóór de audit als landmijn in plaats van een actief defect. Die vermenigvuldigde elk getal dat hij uit een zin las met 100, dus een bon van ¥1,200 werd 120,000 minor units. Dat leest als ¥120,000. Bij een valuta zonder minor unit blaast de constante iemands rekening honderdvoudig op, en de functie zoekt nu in plaats daarvan de exponent voor de aangevraagde valuta op.

Een bon van 1,200 yen omgerekend met een hardgecodeerde vermenigvuldiger van 100 en met de ISO 4217 exponententabel ¥1,200 valuta: JPY VOOR minor = major x 100 constante in de module 120000 minor units leest als ¥120,000 NA minor = major x 10^exp JPY exponent = 0 1200 minor units leest als ¥1,200
De exponentenval in één beeld. Een constante vermenigvuldiger van 100 klopt voor INR en zit er een factor 100 naast voor JPY, dat geen minor unit heeft; dezelfde constante zit er een factor tien naast voor KWD, dat drie decimalen heeft.

De audit vond dezelfde constante op de pagina waar iemand beslist of hij een balans accepteert. formatMinor, achter de claimlanding op /m/{token}, deelde door 100 en plakte er een roepieteken voor. Een schuld van ¥6,000 werd afgedrukt als -₹60.00: verkeerd symbool, een honderdste van het bedrag. Zijn JSON-tegenhanger /v1/claims faalde tegelijk, door de per-valutaregels van een lid tot één samen te vouwen en de valuta te behouden die de map als laatste schreef.

Het derde bestand is internal/ingestion/store.go, en W8 vond het bij het uitbreiden van duplicaatdetectie, niet de audit. De tolerantie van "plus of min één roepie" was de constante 100, één roepie geteld in paise, een marge van plus of min ¥100 terwijl JPY helemaal geen minor unit heeft. Alle drie lezen nu de exponent.

Kale gehele getallen vergelijken liet een yen op een roepie lijken

De dedupe-laag van Dimesum controleert een nieuwe vastlegging tegen recente uitgaven, zodat een geïmporteerde bon niet dubbel kan afrekenen op een bestelling die iemand met de hand heeft ingetikt. De query naast die tolerantiemarge had helemaal geen valutafilter. Dus ¥1,000 kwam overeen met ₹1,000, en een import bood aan om een uitgave te vervangen waar hij niets mee te maken had. Groepen waren al multi-valuta sinds ledger/00005, dus het geval was bereikbaar in plaats van theoretisch.

Kale minor units zijn niet vergelijkbaar tussen valuta's, en toleranties evenmin. De projectie filtert nu op valuta, en leidt zijn marge af van één major unit van de vergeleken valuta.

Twee engines beantwoordden "wie betaalt wie", en de tweede was valutablind

Het dure defect is structureel, niet rekenkundig. Het afreken-schrijfpad plande over internal/platform/simplify, een tweede min-cash-flow engine waarvan de Balance struct geen valutaveld had. Nul-som per valuta maakt ook de platte som nul, dus een schuld van ¥300,000 en een schuld van ₹500 hieven elkaar op tot niets en geen enkele bewaking weigerde het plan. Het plan koppelde vervolgens een yencrediteur aan een roepiedebiteur.

Het schrijven maakte het erger. De service stempelde Currency: g.DefaultCurrency op elke afrekening, dus een schuld van ¥300,000 autoriseerde een INR-journaal, en de yenschuld kon helemaal niet worden vereffend.

Dezelfde vier balansen gerouteerd door de valutablinde simplify-engine en door de valutabewuste settle-engine BALANSEN Asha +¥300,000 Bhavna -¥300,000 Chetan +₹500 Dev -₹500 simplify: Balance{MemberID, Minor} geen valuta op de struct, dus alle vier salderen plat 300000 + (-300000) + 500 + (-500) = 0 plan: Bhavna betaalt Chetan een yendebiteur gestuurd naar een roepiecrediteur settle: Balance{MemberID, money.Amount} partitioneer op valuta, route dan binnen elke JPY: Bhavna betaalt Asha ¥300,000 INR: Dev betaalt Chetan ₹500 de afrekening vermeldt de valuta die hij vereffent simplify is verwijderd, niet gerepareerd
Vier balansen, twee engines. Nul-som per valuta impliceert een platte som van nul, dus een valutablinde engine ziet een sluitende groep en routeert vol vertrouwen een betaling tussen twee mensen die elkaar niets schuldig zijn.

simplify is verwijderd en /settle-plan is uitgefaseerd. internal/platform/settle is nu de enige engine: die verdeelt valuta's in converteerbaar en niet-converteerbaar, rekent nettobalansen één keer om, en routeert elke niet-converteerbare valuta in zijn eigen denominatie. Een afrekening vermeldt de valuta die hij vereffent, en dat veld is verplicht zodra een groep er meer dan één heeft.

Een limiet van nul werd gelezen als geen limiet

De overbetalingsbewaking weigert een betaling die groter is dan de schuld die hij vereffent. De bewaking las outstanding > 0 && amount > outstanding, dus een limiet van nul sloeg de vergelijking helemaal over. Iemand die een betaling vastlegt tegen een schuld die niet bestaat is precies het geval waarvoor de regel bestaat, en het was het geval dat er zonder problemen doorheen glipte.

De oplossing is een type, geen conditie. OutstandingMinor is nu een *int64, zodat "niemand heeft dit berekend" niet op dezelfde manier kan worden geschreven als "het antwoord is nul". Nil slaat de controle over en betekent werkelijk onbekend; elke aanroeper met grootboektoegang geeft een echt getal door.

Waarom een groene suite niets bewees

Elke fixture in deze paden gebruikte INR. Een valutablinde vergelijking is onzichtbaar onder een test met één valuta, want met één valuta valt er niets te verwarren. De suites waren niet zwak, ze waren smal, en de audit die ze had kunnen verbreden was door A17 weggestuurd.

De end-to-end grootboekcontrole deelde de blindheid, en dat is het deel dat het waard is te onthouden. De controle telde boekingen per lid op zonder te groeperen op valuta, dus sloeg loos alarm bij een gezonde groep met twee valuta's en telde op tot nul bij een groep die dubbel kapot was. Een vals-negatief is de gevaarlijke richting. Een controle die op dezelfde aanname als de code is gebouwd, is het altijd eens met de code.

Elk defect dat de multi-valuta audit van 2026-08-21 vond, wat een yengroep kreeg, en wat er live ging.
DefectWaarWat een JPY-groep kreegOplossing
Een Balance struct zonder valuta eropplatform/simplifyeen yencrediteur gekoppeld aan een roepiedebiteursimplify verwijderd; settle partitioneert op valuta
De groepsstandaard gestempeld op elke afrekeningafrekenserviceeen schuld van ¥300,000 autoriseerde een INR-journaaleen afrekening vermeldt de valuta die hij vereffent
outstanding > 0 in de overbetalingsbewakingafrekenserviceeen limiet van nul werd helemaal geen limiet*int64: nil betekent onbekend, nul betekent nul
Delen door 100 met een roepietekengateway formatMinoreen schuld van ¥6,000 afgedrukt als -₹60.00symbool en decimalen uit de valuta
Balansen per valuta samengevouwen tot één regel/v1/claimsde valuta die de map als laatste schreeféén regel per valuta, overeenkomend met GET /balances
Boekingen opgeteld per lid, valuta weggevallenend-to-end grootboekcontroleeen dubbel kapotte groep gemeld als sluitendgroeperen op lid en valuta

Grep je geldcode op de letterlijke 100

Zoek erin naar die constante, en naar elke vergelijking die twee bedragen naast elkaar zet zonder een valuta ernaast. Repareer dan het goedkopere ding, dat de volgende zes voorkomt: schrap een follow-up in dezelfde commit die hem afsluit. En verwijder een dubbele engine in plaats van hem te repareren. Twee antwoorden op "wie betaalt wie" is precies hoe een van beide fout blijft.

Veelgestelde vragen

Wat is multi-valuta kostendeling?

Multi-valuta kostendeling legt elke uitgave vast in de valuta waarin die plaatsvond en houdt een aparte balans per valuta bij in plaats van alles naar één valuta om te rekenen. Dimesum indexeert balansen op groep, lid en valuta, zodat een yenschuld en een roepieschuld nooit samensmelten tot één getal. Omrekening is een weergave voor een afrekenplan, nooit een opgeslagen bedrag.

Waarom is een hardgecodeerde 100 gevaarlijk in geldcode?

Een hardgecodeerde 100 gaat ervan uit dat elke valuta twee decimalen heeft, en meerdere hebben dat niet. JPY heeft geen minor unit, dus vermenigvuldigen met 100 maakt van een bon van ¥1,200 een bedrag van 120,000 minor units, een honderdvoudige inflatie in plaats van een afrondingsfout. KWD heeft drie decimalen, dus dezelfde constante zit er een factor tien naast. Lees in plaats daarvan de ISO 4217 exponent.

Hoe voorkom je dat twee afrekenengines het oneens zijn?

Verwijder een van de twee engines in plaats van ze op elkaar af te stemmen, want een tweede antwoord op wie wie betaalt is precies hoe het eerste fout blijft. Dimesum draaide simplify en settle naast elkaar totdat een audit ontdekte dat de eerste geen valuta op zijn balanstype had, waardoor een plan een yencrediteur aan een roepiedebiteur kon koppelen. simplify werd verwijderd en /settle-plan uitgefaseerd in plaats van gepatcht.

Waarom bleef de testsuite groen bij zes geldfouten?

De testsuite bleef groen omdat elke fixture in de betrokken paden één enkele valuta gebruikte, en een valutablinde vergelijking kan niet falen als er maar één valuta is. De end-to-end grootboekcontrole ging uit van dezelfde aanname: die telde boekingen per lid op zonder te groeperen op valuta, dus meldde die een dubbel kapotte groep als sluitend. Een controle die op de eigen aanname van de code is gebouwd, is het altijd eens met de code.

Wat doe je met een follow-up notitie zodra de functie live gaat?

Schrap een follow-up notitie in dezelfde commit die het werk afsluit dat de notitie beschrijft. Een follow-up die stilletjes waar is geworden is erger dan een openstaande, want die stuurt een latere lezer weg van de code die de notitie ooit beschreef. Dimesum liet een notitie staan dat groepen INR-pinned waren tot een week nadat multi-valuta live ging, en de volgende audit sloeg op basis daarvan drie geldpaden over.