dimesum

Startseite / Blog / Geld

Geld

Sechs Geldfehler bei Ausgaben in mehreren Währungen

· 8 Min. Lesezeit ·

Ein Audit vom 2026-08-21 fand sechs Währungsdefekte, durch die unsere Tests grün geblieben waren, und die Ursache war ein Satz in einer Doku, der still aufgehört hatte, wahr zu sein.

Ein einziger veralteter Satz in einer Doku kostete sechs Geldfehler. Ein Multi-Währungs-Audit von Dimesum am 2026-08-21 fand sie in Code, den unsere Testsuiten für grün gehalten hatten, darunter eine Anspruchsseite, die eine Schuld von ¥6,000 als -₹60.00 darstellte. Der Satz lautete „Gruppen sind auf INR festgelegt“. Wahr bis W7, falsch in dem Moment, in dem es ausgeliefert wurde, und eine Woche später immer noch in FOLLOWUPS.md.

Ein veralteter Satz ist gefährlicher als gar keine Dokumentation. Ein späteres Audit glaubt ihm und überspringt die Pfade, die er abdeckt, sodass eine falsche Zeile Schaden anrichtet, den Schweigen nie anrichten könnte. Eine leere Doku schickt dich zur Quelle. Eine falsche schickt dich ganz woanders hin.

Ein wahr gewordener Nachtrag ist schlimmer als ein offener

Dimesum parkt aufgeschobene Arbeit in FOLLOWUPS.md, eine Zeile pro Entscheidung mit dem Grund für das Parken. Eintrag A17 besagte, dass Gruppen auf INR festgelegt seien, dass eine Auslandsreise nicht in der Währung erfasst werden könne, in der sie stattfand, und dass die Behebung auf eine Gründerentscheidung warte. W7 lieferte Multi-Währung trotzdem aus: eine Ausgabe darf in jeder Währung sein, und Salden werden per Migration ledger/00005 mit (group_id, member_id, currency) verschlüsselt. Niemand strich die Zeile.

Das nächste Audit las A17, schloss daraus, dass der Bereich nicht gebaut war, und öffnete den Code für Ausgleich, Ansprüche oder Landing nie. Drei währungsblinde Geldpfade gingen weiter in Produktion, gestützt auf einen einzigen Satz. Kein schwieriges Problem stand im Weg. Der Docstring von app/amount.py trug dieselbe Behauptung, sodass ein Leser sie zweimal zu hören bekam.

Die Regel, die wir eingeführt haben

Streiche einen Nachtrag in demselben Commit, der ihn schließt. Eine Zeile, die Code beschreibt, der so nicht mehr funktioniert, ist eine Umleitung weg von der Datei, die du lesen musstest, was mehr kostet als gar nichts zu sagen.

Dieselbe hartkodierte 100 tauchte in drei verschiedenen Dateien auf

Geld ist hier int64-Nebeneinheiten plus ein ISO-4217-Code, und Fließkommazahlen fassen es nie an. Ganzzahlen sind von Natur aus exakt, sodass die einzige riskante Arithmetik, die bleibt, die Umrechnung zwischen Haupt- und Nebeneinheit ist. Die Nebeneinheit ist nicht immer ein Hundertstel. JPY hat gar keine, KWD hat drei Nachkommastellen, und jede hartkodierte 100 ist eine Wette darauf, dass der Nutzer zu Hause geblieben ist.

Der app/amount.py des Python-Sidecars enthielt die erste Sichtung, vor dem Audit als Landmine und nicht als aktiver Defekt entfernt. Es multiplizierte jede Zahl, die es aus einem Satz las, mit 100, sodass ein Beleg über ¥1,200 zu 120,000 Nebeneinheiten wurde. Das liest sich als ¥120,000. Bei einer Währung ohne Nebeneinheit bläht die Konstante jemandes Rechnung um das Hundertfache auf, und die Funktion schlägt jetzt stattdessen den Exponenten für die Anfragewährung nach.

Ein Beleg über 1,200 Yen, umgerechnet durch einen hartkodierten Multiplikator von 100 und durch die ISO-4217-Exponententabelle ¥1,200 Währung: JPY VORHER minor = major x 100 Konstante im Modul 120000 Nebeneinheiten liest sich als ¥120,000 NACHHER minor = major x 10^exp JPY-Exponent = 0 1200 Nebeneinheiten liest sich als ¥1,200
Die Exponentenfalle in einem Bild. Ein konstanter Multiplikator von 100 ist für INR korrekt und für JPY, das keine Nebeneinheit hat, um den Faktor 100 falsch; dieselbe Konstante liegt für KWD, das drei Nachkommastellen hat, um den Faktor zehn daneben.

Das Audit fand dieselbe Konstante auf der Seite, auf der eine Person entscheidet, ob sie einen Saldo akzeptiert. formatMinor, hinter der Anspruchsseite unter /m/{token}, teilte durch 100 und klebte ein Rupien-Zeichen davor. Eine Schuld von ¥6,000 wurde als -₹60.00 ausgegeben: falsches Symbol, ein Hundertstel des Betrags. Ihr JSON-Pendant /v1/claims versagte gleich mit, indem es die währungsweisen Zeilen eines Mitglieds zu einer einzigen zusammendrückte und diejenige Währung behielt, welche die Map zuletzt schrieb.

Die dritte Datei ist internal/ingestion/store.go, und W8 fand sie beim Erweitern der Dublettenerkennung, nicht das Audit. Ihre Toleranz von „plus minus einer Rupie“ war die Konstante 100, eine Rupie gezählt in paise, ein Band von plus minus ¥100, wo JPY gar keine Nebeneinheit hat. Alle drei lesen jetzt den Exponenten.

Der Vergleich nackter Ganzzahlen ließ einen Yen wie eine Rupie aussehen

Die Dublettenschicht von Dimesum prüft eine neue Erfassung gegen kürzliche Ausgaben, damit ein importierter Beleg eine von Hand eingetippte Bestellung nicht doppelt belasten kann. Die Abfrage neben diesem Toleranzband hatte gar keinen Währungsfilter. So passte ¥1,000 zu ₹1,000, und ein Import bot an, eine Ausgabe zu ersetzen, mit der er nichts zu tun hatte. Gruppen waren seit ledger/00005 mehrwährungsfähig, sodass der Fall erreichbar und nicht theoretisch war.

Nackte Nebeneinheiten sind über Währungen hinweg nicht vergleichbar, und Toleranzen ebenso wenig. Die Projektion filtert jetzt nach Währung und leitet ihr Band aus einer Haupteinheit der verglichenen Währung ab.

Zwei Engines beantworteten „wer zahlt wem“, und die zweite war währungsblind

Der teure Defekt ist strukturell, nicht arithmetisch. Der Schreibpfad des Ausgleichs plante über internal/platform/simplify, eine zweite Min-Cash-Flow-Engine, deren Balance-Struktur kein Währungsfeld hatte. Ein währungsweises Nullsummenspiel macht auch die flache Summe null, sodass eine Schuld von ¥300,000 und eine Schuld von ₹500 sich zu nichts aufhoben und keine Sicherung den Plan ablehnte. Der Plan paarte dann einen Yen-Gläubiger mit einem Rupien-Schuldner.

Der Schreibvorgang machte es schlimmer. Der Dienst stempelte Currency: g.DefaultCurrency auf jeden Ausgleich, sodass eine Schuld von ¥300,000 eine INR-Buchung autorisierte, und die Yen-Schuld konnte gar nicht ausgeglichen werden.

Dieselben vier Salden, geleitet durch die währungsblinde simplify-Engine und durch die währungsbewusste settle-Engine SALDEN Asha +¥300,000 Bhavna -¥300,000 Chetan +₹500 Dev -₹500 simplify: Balance{MemberID, Minor} keine Währung an der Struktur, sodass alle vier flach saldieren 300000 + (-300000) + 500 + (-500) = 0 Plan: Bhavna zahlt Chetan ein Yen-Schuldner an einen Rupien-Gläubiger geschickt settle: Balance{MemberID, money.Amount} nach Währung partitionieren, dann innerhalb jeder leiten JPY: Bhavna zahlt Asha ¥300,000 INR: Dev zahlt Chetan ₹500 der Ausgleich nennt die Währung, die er begleicht simplify wird gelöscht, nicht repariert
Vier Salden, zwei Engines. Ein währungsweises Nullsummenspiel impliziert eine flache Summe von null, sodass eine währungsblinde Engine eine ausgeglichene Gruppe sieht und selbstsicher eine Zahlung zwischen zwei Personen leitet, die einander nichts schulden.

simplify ist gelöscht und /settle-plan ist außer Dienst gestellt. internal/platform/settle ist jetzt die einzige Engine: sie partitioniert Währungen in konvertierbare und nicht konvertierbare, rechnet Nettosalden einmal um und leitet jede nicht konvertierbare Währung in ihrer eigenen Denomination. Ein Ausgleich nennt die Währung, die er begleicht, und dieses Feld ist verpflichtend, sobald eine Gruppe mehr als eine hält.

Eine Obergrenze von null las sich als keine Obergrenze

Die Überzahlungssicherung verweigert eine Zahlung, die größer ist als die Schuld, die sie begleicht. Die Sicherung las outstanding > 0 && amount > outstanding, sodass eine Obergrenze von null den Vergleich ganz übersprang. Dass jemand eine Zahlung gegen eine nicht existierende Schuld erfasst, ist genau der Fall, für den die Regel existiert, und es war der Fall, der glatt durchging.

Die Behebung ist ein Typ, keine Bedingung. OutstandingMinor ist jetzt ein *int64, sodass „niemand hat das berechnet“ nicht genauso geschrieben werden kann wie „die Antwort ist null“. Nil überspringt die Prüfung und bedeutet echt unbekannt; jeder Aufrufer mit Ledger-Zugriff übergibt eine echte Zahl.

Warum eine grüne Suite nichts bewies

Jedes Fixture in diesen Pfaden nutzte INR. Ein währungsblinder Vergleich ist unter einem Einzelwährungstest unsichtbar, denn mit einer Währung gibt es nichts zu verwechseln. Die Suiten waren nicht schwach, sie waren eng, und das Audit, das sie erweitert hätte, war von A17 weggeschickt worden.

Der End-to-End-Ledger-Prüfer teilte die Blindheit, und das ist der Teil, der es wert ist, festgehalten zu werden. Der Prüfer summierte Buchungen pro Mitglied, ohne nach Währung zu gruppieren, sodass er bei einer gesunden Zweiwährungsgruppe blinden Alarm schlug und über eine Gruppe, die zweifach kaputt war, zu null summierte. Ein falsch Negatives ist die gefährliche Richtung. Ein Prüfer, der auf derselben Annahme wie der Code gebaut ist, wird dem Code immer zustimmen.

Jeder Defekt, den das Multi-Währungs-Audit vom 2026-08-21 fand, was eine Yen-Gruppe erhielt und was ausgeliefert wurde.
DefektWoWas eine JPY-Gruppe erhieltBehebung
Eine Balance-Struktur ohne Währungplatform/simplifyein Yen-Gläubiger mit einem Rupien-Schuldner gepaartsimplify gelöscht; settle partitioniert nach Währung
Der Gruppenstandard auf jeden Ausgleich gestempeltAusgleichsdiensteine Schuld von ¥300,000 autorisierte eine INR-Buchungein Ausgleich nennt die Währung, die er begleicht
outstanding > 0 in der ÜberzahlungssicherungAusgleichsdiensteine Obergrenze von null wurde zu gar keiner Obergrenze*int64: nil bedeutet unbekannt, null bedeutet null
Durch 100 teilen mit einem Rupien-ZeichenGateway formatMinoreine Schuld von ¥6,000 als -₹60.00 ausgegebenSymbol und Nachkommastellen aus der Währung
Währungsweise Salden zu einer Zeile zusammengedrückt/v1/claimsdiejenige Währung, welche die Map zuletzt schriebein Eintrag pro Währung, passend zu GET /balances
Buchungen pro Mitglied summiert, Währung fallen gelassenEnd-to-End-Ledger-Prüfereine zweifach kaputte Gruppe als ausgeglichen gemeldetnach Mitglied und Währung gruppieren

Durchsuche deinen Geldcode nach der buchstäblichen 100

Durchsuche ihn nach dieser Konstante und nach jedem Vergleich, der zwei Beträge nebeneinander stellt, ohne eine Währung daneben. Behebe dann das billigere Ding, jenes, das die nächsten sechs verhindert: streiche einen Nachtrag in demselben Commit, der ihn schließt. Und lösche eine doppelte Engine, statt sie zu reparieren. Zwei Antworten auf „wer zahlt wem“ sind der Weg, auf dem eine von ihnen falsch bleibt.

Häufige Fragen

Was bedeutet Ausgaben splitten in mehreren Währungen?

Ausgaben splitten in mehreren Währungen erfasst jede Ausgabe in der Währung, in der sie anfiel, und hält je Währung einen eigenen Saldo, statt alles in eine umzurechnen. Dimesum verschlüsselt Salden nach Gruppe, Mitglied und Währung, sodass eine Yen-Schuld und eine Rupien-Schuld nie zu einer einzigen Zahl verschmelzen. Die Umrechnung ist eine Sicht für einen Ausgleichsplan, nie ein gespeicherter Betrag.

Warum ist eine hartkodierte 100 im Geldcode gefährlich?

Eine hartkodierte 100 nimmt an, dass jede Währung zwei Nachkommastellen hat, und mehrere haben das nicht. JPY hat keine Nebeneinheit, sodass die Multiplikation mit 100 einen Beleg über ¥1,200 in 120,000 Nebeneinheiten verwandelt, eine hundertfache Aufblähung statt eines Rundungsfehlers. KWD hat drei Nachkommastellen, sodass dieselbe Konstante um den Faktor zehn danebenliegt. Lies stattdessen den ISO-4217-Exponenten.

Wie verhindert man, dass zwei Ausgleichs-Engines sich widersprechen?

Lösche eine der beiden Engines, statt sie abzugleichen, denn eine zweite Antwort darauf, wer wem zahlt, ist der Weg, auf dem die erste falsch bleibt. Dimesum ließ simplify und settle nebeneinander laufen, bis ein Audit fand, dass die erste keine Währung an ihrem Saldotyp hatte, was einen Plan einen Yen-Gläubiger mit einem Rupien-Schuldner paaren ließ. simplify wurde entfernt und /settle-plan außer Dienst gestellt, statt geflickt.

Warum blieb die Testsuite trotz sechs Geldfehlern grün?

Die Testsuite blieb grün, weil jedes Fixture in den betroffenen Pfaden eine einzige Währung nutzte, und ein währungsblinder Vergleich kann nicht scheitern, wenn es nur eine Währung gibt. Der End-to-End-Ledger-Prüfer trug dieselbe Annahme: er summierte Buchungen pro Mitglied, ohne nach Währung zu gruppieren, sodass er eine zweifach kaputte Gruppe als ausgeglichen meldete. Ein Prüfer, der auf der eigenen Annahme des Codes gebaut ist, stimmt dem Code zu.

Was macht man mit einem Nachtrag, sobald das Feature ausgeliefert ist?

Streiche einen Nachtrag in demselben Commit, der die Arbeit abschließt, die er beschreibt. Ein Nachtrag, der still wahr geworden ist, ist schlimmer als ein offener, weil er einen späteren Leser vom Code wegweist, den er einst beschrieb. Dimesum ließ einen Eintrag stehen, der besagte, Gruppen seien auf INR festgelegt, eine Woche nachdem Multi-Währung ausgeliefert war, und das nächste Audit übersprang drei Geldpfade auf sein Wort hin.