Invocie

Tech

Desenhar uma API de faturação eletrónica multirregião: lições do campo

Se está a construir infraestrutura que serve países de clearance e de post-audit, só tem uma hipótese de acertar nas abstrações. Aqui fica o que aprendemos da forma difícil.

Invocie Team · 4 de dezembro de 2025 · 6 min de leitura


Construímos o motor de compliance da Invocie para tratar clearance MENA, Peppol UE e post-audit global a partir de uma única codebase. A arquitetura levou três iterações a estabilizar. Estas foram as decisões de design que sobreviveram.

1. Strategy Pattern ao nível do país, não da região

Região é granularidade demasiado grossa. Arábia Saudita (clearance, ZATCA, 15 %) e EAU (interoperable, FTA, 5 %) são ambos "MENA" mas arquiteturalmente muito diferentes. Despachamos primeiro pelo código de país ISO, depois pela região, com um Default como 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 em todo o lado, nunca floats

Usamos BigInt com escalado de vírgula fixa (10000x para precisão de 4 casas decimais) nos caminhos de performance e decimal.js onde o SDK é mais amigável. O único tipo proibido para valores monetários é o number do JavaScript — apanhámos três bugs de arredondamento em pré-produção, todos a apontar para um Number() descontrolado.

3. Async por defeito, sync por exceção

A rota HTTP que cria uma fatura nunca bloqueia na API governamental. Escreve na BD, mete um job na queue e devolve 201. O worker trata da submissão à autoridade com retries (backoff exponencial, máx. 6 tentativas, com jitter). Isto isola a API voltada ao utilizador da latência do lado do governo, e mantém os retries seguros mesmo quando a ZATCA tem uma má tarde.

4. Separar o modelo canónico do formato de wire

O nosso tipo Invoice interno é o mesmo independentemente do país. O buildArtifacts() de cada strategy converte-o no formato wire específico do país (TLV QR, UBL 2.1 XML, ou apenas um JSON default). O modelo canónico nunca sabe sobre XML. O XML nunca sai da strategy. Isto faz com que adicionar o IRP indiano mais tarde seja uma mudança num único ficheiro.

5. O logging faz parte do contrato

Cada interação com o governo escreve uma linha ComplianceLog com encadeamento prev_hash → hash. Quando os auditores aparecerem — e vão aparecer — não vais querer reconstruir o que aconteceu a partir dos logs da app. A linha na BD é o recibo.


Leituras relacionadas

Emita faturas conformes em todos os mercados

ZATCA, FTA, Peppol e pós-auditoria global — uma API.

Falar com a nossa equipa