Wir haben Invocies Compliance-Engine so gebaut, dass sie MENA-Clearance, EU-Peppol und globales Post-audit aus einer Codebase bedient. Die Architektur brauchte drei Iterationen, bis sie sich gesetzt hat. Das sind die Designentscheidungen, die überlebt haben.
1. Strategy Pattern auf Länderebene, nicht Regionsebene
Region ist zu grob. Saudi-Arabien (Clearance, ZATCA, 15 %) und die VAE (interoperable, FTA, 5 %) sind beide "MENA", aber architektonisch sehr unterschiedlich. Wir dispatchen zuerst nach ISO-Ländercode, dann nach Region, mit einem Default als Fallback.
function pickStrategy(t: { country_code: string; tax_region: string }) {
if (MENA.includes(t.country_code)) return new MENAStrategy();
if (EU.includes(t.country_code)) return new EUStrategy();
if (t.tax_region === "MENA") return new MENAStrategy();
if (t.tax_region === "EU") return new EUStrategy();
return new DefaultStrategy();
}2. Überall Decimal, niemals Floats
Auf performancekritischen Pfaden nutzen wir BigInt mit Festkomma-Skalierung (10000x für 4 Dezimalstellen), an anderer Stelle decimal.js, wo das SDK bequemer ist. Der einzige verbotene Typ für Geldbeträge ist JavaScripts number — wir haben drei Rundungsbugs vor der Produktion gefangen, alle ließen sich auf ein verirrtes Number() zurückführen.
3. Async by default, sync nur als Ausnahme
Die HTTP-Route, die eine Rechnung erzeugt, blockiert nie auf der Behörden-API. Sie schreibt in die DB, queued einen Job und liefert 201 zurück. Der Worker übernimmt die Übermittlung an die Behörde mit Retries (Exponential-Backoff, max. 6 Versuche, mit Jitter). Das isoliert die User-facing-API von Latenz auf Behördenseite und macht Retries auch dann sicher, wenn ZATCA einen schlechten Nachmittag hat.
4. Trennen Sie das kanonische Modell vom Wire-Format
Unser interner Invoice-Typ ist landesunabhängig derselbe. Das buildArtifacts() jeder Strategy macht daraus das landesspezifische Wire-Format (TLV-QR, UBL-2.1-XML oder einfach Default-JSON). Das kanonische Modell weiß nie etwas von XML. Das XML verlässt nie die Strategy. So wird das spätere Hinzufügen des indischen IRP zu einer Ein-Datei-Änderung.
5. Logging ist Teil des Vertrags
Jede Behörden-Interaktion schreibt eine ComplianceLog-Zeile mit prev_hash → hash-Verkettung. Wenn die Prüfer kommen — und sie kommen — wollen Sie nicht aus App-Logs rekonstruieren, was passiert ist. Die DB-Zeile ist die Quittung.