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.