Invocie

Tech

Concevoir une API e-invoicing multi-régions : leçons du terrain

Si vous bâtissez une infrastructure qui dessert à la fois des pays clearance et post-audit, vous n'avez qu'une seule chance de réussir vos abstractions. Voici ce que nous avons appris à la dure.

Invocie Team · 4 décembre 2025 · 6 min de lecture


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.


Lectures liées

Émettez des factures conformes sur tous les marchés

ZATCA, FTA, Peppol et post-audit mondial — une seule API.

Parler à notre équipe