Zes geldfouten bij multi-valuta kostendeling
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.
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.
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.
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.
| Defect | Waar | Wat een JPY-groep kreeg | Oplossing |
|---|---|---|---|
Een Balance struct zonder valuta erop | platform/simplify | een yencrediteur gekoppeld aan een roepiedebiteur | simplify verwijderd; settle partitioneert op valuta |
| De groepsstandaard gestempeld op elke afrekening | afrekenservice | een schuld van ¥300,000 autoriseerde een INR-journaal | een afrekening vermeldt de valuta die hij vereffent |
outstanding > 0 in de overbetalingsbewaking | afrekenservice | een limiet van nul werd helemaal geen limiet | *int64: nil betekent onbekend, nul betekent nul |
| Delen door 100 met een roepieteken | gateway formatMinor | een schuld van ¥6,000 afgedrukt als -₹60.00 | symbool en decimalen uit de valuta |
| Balansen per valuta samengevouwen tot één regel | /v1/claims | de valuta die de map als laatste schreef | één regel per valuta, overeenkomend met GET /balances |
| Boekingen opgeteld per lid, valuta weggevallen | end-to-end grootboekcontrole | een dubbel kapotte groep gemeld als sluitend | groeperen 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.
Populaire artikelen
- Het append-only grootboek dat saldi exact houdt8 min leestijd
- Waarom een bewerking de verdeling opnieuw moet vaststellen8 min leestijd
- Groepskosten afrekenen met minder overboekingen5 min leestijd
- Een restaurantrekening eerlijk splitten9 min leestijd
- Huur eerlijk verdelen met huisgenoten6 min leestijd