Nous avons construit le moteur de compliance d'Invocie pour gérer le clearance MENA, le Peppol UE et le post-audit mondial depuis un seul codebase. L'architecture a mis trois itérations à se stabiliser. Voici les choix de design qui ont survécu.
1. Strategy Pattern au niveau pays, pas région
La région est une maille trop grosse. L'Arabie saoudite (clearance, ZATCA, 15 %) et les EAU (interoperable, FTA, 5 %) sont tous deux "MENA" mais architecturalement très différents. On dispatche par code pays ISO d'abord, par région ensuite, avec un Default en 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. Decimal partout, jamais de floats
Nous utilisons BigInt avec un scaling à virgule fixe (10000x pour 4 décimales) dans les chemins de performance, et decimal.js là où le SDK est plus accueillant. Le seul type interdit pour les valeurs monétaires est le number de JavaScript — nous avons attrapé trois bugs d'arrondi avant la mise en prod, tous remontant à un Number() égaré.
3. Async par défaut, sync par exception
La route HTTP qui crée une facture ne se bloque jamais sur l'API gouvernementale. Elle écrit en base, met en queue un job, et renvoie 201. Le worker gère la soumission au gouvernement avec retries (backoff exponentiel, max 6 tentatives, jitter). Cela isole l'API utilisateur de la latence côté gouvernement, et garde les retries sûrs même quand ZATCA passe un mauvais après-midi.
4. Séparez le modèle canonique du format wire
Notre type Invoice interne est le même indépendamment du pays. Le buildArtifacts() de chaque strategy le convertit dans le format wire spécifique au pays (TLV QR, UBL 2.1 XML, ou juste un JSON par défaut). Le modèle canonique ignore tout du XML. Le XML ne sort jamais du strategy. Ajouter l'IRP indien plus tard devient un changement à un seul fichier.
5. Le logging fait partie du contrat
Chaque interaction gouvernement écrit une ligne ComplianceLog avec chaînage prev_hash → hash. Quand les auditeurs viendront — et ils viendront — vous ne voulez pas reconstruire ce qui s'est passé depuis les logs applicatifs. La ligne en base, c'est le reçu.