Invocie

Tech

다지역 e-invoicing API 설계: 현장에서 얻은 교훈

clearance와 post-audit 국가를 동시에 지원하는 인프라를 만든다면, 추상화를 제대로 잡을 기회는 한 번뿐입니다. 우리가 어렵게 얻은 교훈을 공유합니다.

Invocie Team · 2025년 12월 4일 · 6 분 분량


We built Invocie's compliance engine to handle MENA clearance, EU Peppol, and global post-audit out of one codebase. The architecture took three iterations to settle. These are the design choices that survived.

1. Strategy Pattern at the country level, not the region level

Region is too coarse. Saudi (clearance, ZATCA, 15%) and the UAE (interoperable, FTA, 5%) are both "MENA" but architecturally very different. We dispatch by ISO country code first, region second, with a Default 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 everywhere, never floats

We use BigInt with fixed-point scaling (10000x for 4-decimal precision) in performance paths and decimal.js where the SDK is friendlier. The only forbidden type for monetary values is JavaScript's number — we caught three rounding bugs in pre-production tracing back to a stray Number().

3. Async-by-default, sync-by-exception

The HTTP route that creates an invoice never blocks on the government API. It writes to the database, enqueues a job, and returns 201. The worker handles the gov submission with retries (exponential backoff, max 6 attempts, jitter). This insulates the user-facing API from gov-side latency, and it makes retries safe even when ZATCA has a bad afternoon.

4. Separate the canonical model from the wire format

Our internal Invoice type is the same regardless of country. Each strategy's buildArtifacts() turns it into the country-specific wire format (TLV QR, UBL 2.1 XML, or just a Default JSON). The canonical model never knows about XML. The XML never escapes the strategy. This makes adding India IRP later a one-file change.

5. Logging is part of the contract

Every gov interaction writes a ComplianceLog row with prev_hash → hash chaining. When auditors come — and they will come — you don't want to be reconstructing what happened from app logs. The DB row is the receipt.


관련 글

모든 시장에서 컴플라이언트한 청구서 발행

ZATCA, FTA, Peppol과 글로벌 사후 감사 — 단일 API.

팀에 문의