Seks pengefeil i utgiftsdeling med flere valutaer
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.
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.
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.
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.
| Feil | Hvor | Hva en JPY-gruppe fikk | Rettelse |
|---|---|---|---|
En Balance-struct uten valuta på seg | platform/simplify | en yen-kreditor paret med en rupi-debitor | simplify slettet; settle partisjonerer etter valuta |
| Gruppens standard stemplet på hvert oppgjør | oppgjørstjeneste | en gjeld på ¥300,000 autoriserte en INR-journal | et oppgjør oppgir valutaen det gjør opp |
outstanding > 0 i overbetalingsvakten | oppgjørstjeneste | et tak på null ble til ingen tak i det hele tatt | *int64: nil betyr ukjent, null betyr null |
| Del på 100 med et rupitegn | gateway formatMinor | en gjeld på ¥6,000 skrevet ut som -₹60.00 | symbol og desimaler fra valutaen |
| Saldoer per valuta flatet ut til én rad | /v1/claims | den valutaen kartet skrev sist | én oppføring per valuta, i samsvar med GET /balances |
| Posteringer summert per medlem, valuta droppet | ende-til-ende hovedbokverifikator | en dobbelt ødelagt gruppe rapportert som balansert | grupper 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.
Populære innlegg
- Append-only-hovedbok for eksakte delte saldoer8 min lesing
- Hvorfor redigering må gjenta hele fordelingen8 min lesing
- Gjør opp: rydd felles utgifter på færre overføringer5 min lesing
- Slik deler du en regning når én rett ikke ble delt9 min lesing
- Slik deler du husleien rettferdig5 min lesing