Construimos el motor de compliance de Invocie para manejar clearance MENA, Peppol UE y post-audit global desde un único codebase. La arquitectura tardó tres iteraciones en estabilizarse. Estas son las decisiones de diseño que sobrevivieron.
1. Strategy Pattern al nivel de país, no de región
Región es demasiado grueso. Arabia Saudí (clearance, ZATCA, 15 %) y EAU (interoperable, FTA, 5 %) son ambos "MENA" pero arquitectónicamente muy distintos. Despachamos por código de país ISO primero, región después, con un 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 en todas partes, jamás floats
Usamos BigInt con escalado de punto fijo (10000x para precisión de 4 decimales) en caminos críticos de rendimiento, y decimal.js donde el SDK es más amistoso. El único tipo prohibido para valores monetarios es el number de JavaScript — pillamos tres bugs de redondeo en pre-prod que se rastrearon a un Number() despistado.
3. Async por defecto, sync por excepción
La ruta HTTP que crea una factura nunca se bloquea en la API gubernamental. Escribe en BBDD, encola un job y devuelve 201. El worker maneja la submisión a gobierno con retries (backoff exponencial, máx 6 intentos, jitter). Esto aísla la API de cara al usuario de la latencia del gobierno y mantiene los retries seguros incluso cuando ZATCA tiene una mala tarde.
4. Separa el modelo canónico del formato de wire
Nuestro tipo Invoice interno es el mismo independientemente del país. El buildArtifacts() de cada strategy lo convierte en el formato wire específico del país (TLV QR, UBL 2.1 XML, o solo un JSON por defecto). El modelo canónico nunca sabe de XML. El XML nunca sale del strategy. Eso convierte añadir IRP de India más adelante en un cambio de un solo archivo.
5. El logging es parte del contrato
Cada interacción con gobierno escribe una fila ComplianceLog con encadenado prev_hash → hash. Cuando vengan los auditores — y vendrán — no quieres estar reconstruyendo qué pasó desde los logs de la app. La fila en la BBDD es el recibo.