dimesum

Forsiden / Blogg / Penger

Penger

Seks pengefeil i utgiftsdeling med flere valutaer

· 8 min lesing ·

En revisjon 2026-08-21 fant seks valutafeil testene våre hadde vært grønne gjennom, og årsaken var én setning i et dokument som stille hadde sluttet å være sann.

Én foreldet setning i et dokument kostet seks pengefeil. En flervaluta-revisjon av Dimesum den 2026-08-21 fant dem i kode som testpakkene våre hadde vært grønne gjennom, blant annet en kravside som gjenga en gjeld på ¥6,000 som -₹60.00. Setningen var "grupper er låst til INR". Sann fram til W7, usann i samme øyeblikk den ble lansert, og fortsatt liggende i FOLLOWUPS.md en uke senere.

En foreldet setning er farligere enn ingen dokumentasjon. En senere revisjon tror på den og hopper over stiene den dekker, så en feil linje gjør skade som taushet aldri kunne. Et tomt dokument sender deg til kilden. Et feil dokument sender deg et helt annet sted.

En oppfølging som ble sann er verre enn en åpen

Dimesum parkerer utsatt arbeid i FOLLOWUPS.md, én rad per beslutning med grunnen til at den ble parkert. Oppføring A17 sa at grupper var låst til INR, at en utenlandsreise ikke kunne registreres i valutaen den skjedde i, og at rettelsen ventet på en beslutning fra grunnleggeren. W7 lanserte flervaluta likevel: en utgift kan være i hvilken som helst valuta, og saldoer nøkles på (group_id, member_id, currency) av migreringen ledger/00005. Ingen strøk raden.

Den neste revisjonen leste A17, konkluderte med at området ikke var bygget, og åpnet aldri oppgjørs-, krav- eller landingskoden. Tre valutablinde pengestier fortsatte å bli lansert på styrken av én setning. Ingen vanskelige problemer sto i veien. Docstringen i app/amount.py bar det samme utsagnet, så en leser fikk beskjed to ganger.

Regelen vi tok i bruk

Stryk en oppfølging i samme commit som lukker den. En rad som beskriver kode som sluttet å virke slik, er en omdirigering vekk fra filen du trengte å lese, noe som koster mer enn å si ingenting.

Den samme hardkodede 100 dukket opp i tre forskjellige filer

Penger her er int64 mindre enheter pluss en ISO 4217-kode, og flyttall rører dem aldri. Heltall er eksakte av konstruksjon, så den eneste risikable aritmetikken som er igjen er konverteringen mellom hovedenheter og mindre enheter. Den mindre enheten er ikke alltid en hundredel. JPY har ingen i det hele tatt, KWD har tre desimaler, og hver hardkodede 100 er et veddemål på at brukeren holdt seg hjemme.

Python-sidevognens app/amount.py hadde det første funnet, hentet ut før revisjonen som en landmine snarere enn en aktiv feil. Den multipliserte tallet den leste ut av en setning med 100, så en kvittering på ¥1,200 ble til 120,000 mindre enheter. Det leses som ¥120,000. På en valuta uten mindre enhet hundredobler konstanten noens regning, og funksjonen slår nå i stedet opp eksponenten for den forespurte valutaen.

En kvittering på 1,200 yen konvertert med en hardkodet multiplikator på 100 og med ISO 4217-eksponenttabellen ¥1,200 valuta: JPY FØR minor = major x 100 konstant i modulen 120000 mindre enheter leses som ¥120,000 ETTER minor = major x 10^exp JPY-eksponent = 0 1200 mindre enheter leses som ¥1,200
Eksponentfellen i ett bilde. En konstant multiplikator på 100 er riktig for INR og feil med en faktor på 100 for JPY, som ikke har noen mindre enhet; den samme konstanten bommer med ti for KWD, som har tre desimaler.

Revisjonen fant den samme konstanten på siden der en person avgjør om en saldo skal godtas. formatMinor, bak kravlandingen på /m/{token}, delte på 100 og limte et rupitegn foran. En gjeld på ¥6,000 ble skrevet ut som -₹60.00: feil symbol, en hundredel av beløpet. JSON-søsknet /v1/claims feilet ved siden av den, og flatet ut et medlems rader per valuta til én og beholdt den valutaen kartet skrev sist.

Den tredje filen er internal/ingestion/store.go, og W8 fant den mens den utvidet duplikatdeteksjon, ikke revisjonen. Toleransen på "pluss eller minus én rupi" var konstanten 100, én rupi talt i paise, et bånd på pluss eller minus ¥100 der JPY ikke har noen mindre enhet i det hele tatt. Alle tre leser eksponenten nå.

Å sammenligne nakne heltall fikk en yen til å se ut som en rupi

Dimesums dedupe-lag sjekker en ny registrering mot nylige utgifter så en importert kvittering ikke kan dobbeltbelaste en bestilling noen skrev inn for hånd. Spørringen ved siden av det toleransebåndet hadde ingen valutafilter i det hele tatt. Så ¥1,000 matchet ₹1,000, og en import tilbød å erstatte en utgift den ikke hadde noe med å gjøre. Grupper hadde vært flervaluta siden ledger/00005, så tilfellet var oppnåelig snarere enn teoretisk.

Nakne mindre enheter er ikke sammenlignbare på tvers av valutaer, og det er heller ikke toleranser. Projeksjonen filtrerer på valuta nå, og utleder båndet sitt fra én hovedenhet av valutaen som sammenlignes.

To motorer svarte på "hvem betaler hvem", og den andre var valutablind

Den dyre feilen er strukturell, ikke aritmetisk. Skrivestien for oppgjør planla over internal/platform/simplify, en annen min-cash-flow-motor hvis Balance-struct ikke hadde noe valutafelt. Nullsum per valuta gjør den flate summen null også, så en gjeld på ¥300,000 og en gjeld på ₹500 opphevet hverandre til ingenting og ingen vakt avviste planen. Planen paret så en yen-kreditor med en rupi-debitor.

Skrivingen gjorde det verre. Tjenesten stemplet Currency: g.DefaultCurrency på hvert oppgjør, så en gjeld på ¥300,000 autoriserte en INR-journal, og yen-gjelden kunne ikke gjøres opp i det hele tatt.

De samme fire saldoene rutet av den valutablinde simplify-motoren og av den valutabevisste settle-motoren SALDOER Asha +¥300,000 Bhavna -¥300,000 Chetan +₹500 Dev -₹500 simplify: Balance{MemberID, Minor} ingen valuta på structen, så alle fire netter flatt 300000 + (-300000) + 500 + (-500) = 0 plan: Bhavna betaler Chetan en yen-debitor sendt til en rupi-kreditor settle: Balance{MemberID, money.Amount} partisjoner etter valuta, rut deretter inne i hver JPY: Bhavna betaler Asha ¥300,000 INR: Dev betaler Chetan ₹500 oppgjøret oppgir valutaen det gjør opp simplify er slettet, ikke rettet
Fire saldoer, to motorer. Nullsum per valuta innebærer en flat sum på null, så en valutablind motor ser en balansert gruppe og ruter selvsikkert en betaling mellom to personer som ikke skylder hverandre noe.

simplify er slettet og /settle-plan er tatt ut av bruk. internal/platform/settle er den eneste motoren nå: den partisjonerer valutaer i konverterbare og ikke-konverterbare, konverterer nettosaldoer én gang, og ruter hver ikke-konverterbare valuta i sin egen valørenhet. Et oppgjør oppgir valutaen det gjør opp, og det feltet er påkrevd så snart en gruppe har mer enn én.

Et tak på null ble lest som ingen tak

Overbetalingsvakten avviser en betaling som er større enn gjelden den gjør opp. Vakten leste outstanding > 0 && amount > outstanding, så et tak på null hoppet over sammenligningen helt. Noen som registrerer en betaling mot en gjeld som ikke finnes er det ene tilfellet regelen finnes for, og det var tilfellet som seilte igjennom.

Rettelsen er en type, ikke en betingelse. OutstandingMinor er nå en *int64, så "ingen beregnet dette" kan ikke skrives på samme måte som "svaret er null". Nil hopper over sjekken og betyr virkelig ukjent; enhver kaller med tilgang til hovedboken sender et ekte tall.

Hvorfor en grønn testpakke ikke beviste noe

Hvert fixture i disse stiene brukte INR. En valutablind sammenligning er usynlig under en test med én valuta, fordi med én valuta er det ingenting å forveksle. Testpakkene var ikke svake, de var smale, og revisjonen som ville ha utvidet dem hadde blitt sendt vekk av A17.

Den ende-til-ende hovedbokverifikatoren delte blindheten, og det er den delen verdt å merke seg. Verifikatoren summerte posteringer per medlem uten å gruppere etter valuta, så den ropte ulv på en sunn gruppe med to valutaer og summerte til null over en gruppe som var ødelagt to ganger. En falsk negativ er den farlige retningen. En kontrollør bygget på den samme antakelsen som koden vil alltid være enig med koden.

Hver feil flervaluta-revisjonen 2026-08-21 fant, hva en yen-gruppe fikk, og hva som ble lansert.
FeilHvorHva en JPY-gruppe fikkRettelse
En Balance-struct uten valuta på segplatform/simplifyen yen-kreditor paret med en rupi-debitorsimplify slettet; settle partisjonerer etter valuta
Gruppens standard stemplet på hvert oppgjøroppgjørstjenesteen gjeld på ¥300,000 autoriserte en INR-journalet oppgjør oppgir valutaen det gjør opp
outstanding > 0 i overbetalingsvaktenoppgjørstjenesteet tak på null ble til ingen tak i det hele tatt*int64: nil betyr ukjent, null betyr null
Del på 100 med et rupitegngateway formatMinoren gjeld på ¥6,000 skrevet ut som -₹60.00symbol og desimaler fra valutaen
Saldoer per valuta flatet ut til én rad/v1/claimsden valutaen kartet skrev sistén oppføring per valuta, i samsvar med GET /balances
Posteringer summert per medlem, valuta droppetende-til-ende hovedbokverifikatoren dobbelt ødelagt gruppe rapportert som balansertgrupper etter medlem og valuta

Grep pengekoden din etter den bokstavelige 100

Søk i den etter den konstanten, og etter hver sammenligning som setter to beløp side om side uten en valuta ved siden av dem. Rett så den billigere tingen, den som forhindrer de neste seks: stryk en oppfølging i samme commit som lukker den. Og slett en duplikatmotor i stedet for å reparere den. To svar på "hvem betaler hvem" er slik det ene av dem forblir feil.

Vanlige spørsmål

Hva er utgiftsdeling med flere valutaer?

Utgiftsdeling med flere valutaer registrerer hver utgift i valutaen den skjedde i og holder en egen saldo per valuta i stedet for å konvertere alt til én. Dimesum nøkler saldoer på gruppe, medlem og valuta, så en yen-gjeld og en rupi-gjeld aldri slås sammen til ett tall. Konvertering er en visning brukt for en oppgjørsplan, aldri et lagret beløp.

Hvorfor er en hardkodet 100 farlig i pengekode?

En hardkodet 100 antar at hver valuta har to desimaler, og flere har ikke det. JPY har ingen mindre enhet, så å multiplisere med 100 gjør en kvittering på ¥1,200 til 120,000 mindre enheter, en hundredobling snarere enn en avrundingsfeil. KWD har tre desimaler, så den samme konstanten bommer med ti. Les ISO 4217-eksponenten i stedet.

Hvordan hindrer du at to oppgjørsmotorer er uenige?

Slett den ene av de to motorene i stedet for å avstemme dem, fordi et andre svar på hvem som betaler hvem er slik det første forblir feil. Dimesum kjørte simplify og settle side om side helt til en revisjon fant at den første ikke hadde noen valuta på saldotypen sin, noe som lot en plan pare en yen-kreditor med en rupi-debitor. simplify ble fjernet og /settle-plan tatt ut av bruk i stedet for å lappes.

Hvorfor forble testpakken grønn gjennom seks pengefeil?

Testpakken forble grønn fordi hvert fixture i de berørte stiene brukte én valuta, og en valutablind sammenligning kan ikke feile når det bare er én valuta. Ende-til-ende hovedbokverifikatoren holdt den samme antakelsen: den summerte posteringer per medlem uten å gruppere etter valuta, så den rapporterte en dobbelt ødelagt gruppe som balansert. En kontrollør bygget på kodens egen antakelse er enig med koden.

Hva bør du gjøre med en oppfølgingsoppføring når funksjonen er lansert?

Stryk en oppfølgingsoppføring i samme commit som lukker arbeidet den beskriver. En oppfølging som stille har blitt sann er verre enn en åpen, fordi den peker en senere leser vekk fra koden den pleide å beskrive. Dimesum lot en oppføring bli stående om at grupper var låst til INR i en uke etter at flervaluta ble lansert, og den neste revisjonen hoppet over tre pengestier på dens ord.