Bilko ADR-016: E-Invoice Adapter
Author: ALAI, 2026
# ADR-016: E-Invoice Adapter & UBL 2.1 Canonical Model
**Status:** Accepted
**Date:** 2026-04-21
**Author:** ALAI, 2026
**Related:** ADR-015 (Four-Jurisdiction Plugin), ADR-019 (Integration Adapter Registry)
---
## Context
Each of Bilko's four tax jurisdictions mandates **electronic invoicing** via government-operated fiscal platforms:
| Jurisdiction | Platform | Format | Mandatory Since | Inbound Support |
| -------------------- | --------------------- | ---------------------- | --------------- | --------------- |
| RS (Serbia) | SEF (efaktura.gov.rs) | UBL 2.1 + SEF envelope | 2023 | Yes (B2B) |
| HR (Croatia) | HR-FISK / FINA eRacun | UBL 2.1 + FINA XML | Jan 2026 | Yes (B2B) |
| BA-FED (Bosnia FBiH) | CPF | Unspecified (stub) | TBD 2027 | Unknown |
| BA-RS (Bosnia RS) | UINO | Unspecified (stub) | TBD | Unknown |
### Problem Statement
Bilko's **invoice domain** must:
1. Generate invoices in a **canonical format** independent of any single jurisdiction
2. **Serialize** to jurisdiction-specific XML/JSON for submission
3. **Submit** to fiscal platforms with retry/error handling
4. **Poll** for status updates (async platforms)
5. **Parse** inbound invoices received from suppliers (B2B procurement)
**Without:**
- Embedding SEF/FINA-specific logic in the core `Invoice` model
- Duplicating invoice validation across 4 jurisdictions
- Tight coupling to any single platform's API changes
### Design Decision Drivers
**Vendor patterns adopted:**
- **SAP B1 Electronic Filing Manager (EFM):** Pluggable adapter layer per jurisdiction + canonical UBL 2.1 core
- **EN 16931 European e-invoicing standard:** Defines minimal invoice semantic model
- **UBL 2.1 (ISO/IEC 19845):** OASIS Universal Business Language — XML serialization
**Key insight from SAP:** The _canonical model_ should reflect **accounting semantics**, not platform wire formats. Adapters translate from canonical → platform-specific.
---
## Decision
### 1. Canonical Invoice Model — EN 16931 Subset
Bilko's internal invoice representation is a **Kotlin data class** implementing the **EN 16931 Core Invoice Model** (mandatory BT fields + optional BG groups).
**File:** `CanonicalInvoice.kt` (see `~/ALAI/products/Bilko/scratch-api/src/main/kotlin/no/alai/bilko/einvoice/CanonicalInvoice.kt`)
**Key design rules:**
1. All monetary amounts: `BigDecimal` with 4 decimal places (ADR-002)
2. All dates: `kotlinx.datetime.LocalDate`
3. VAT categories: UN/ECE 5305 codes (`S`, `Z`, `E`, `AE`, `O`, `K`, `G`)
4. Invoice types: UNTDID 1001 codes (380 = commercial invoice, 381 = credit note, 383 = debit note)
5. Currency codes: ISO 4217 (RSD, EUR, BAM)
6. Country codes: ISO 3166-1 alpha-2 (RS, HR, BA)
**Mandatory fields (EN 16931 BT numbering in KDoc):**
- BT-1: Invoice number
- BT-2: Issue date
- BT-3: Invoice type code
- BT-5: Currency code
- BT-9: Due date
- BG-4: Supplier party (name, tax ID, address)
- BG-7: Buyer party (name, tax ID, address)
- BG-23: VAT breakdown per category
- BG-25: Invoice lines (minimum 1 line)
**Extension point:**
```kotlin
val adapterMetadata: Map<String, String> = emptyMap()
```
For jurisdiction-specific fields not covered by EN 16931 (e.g., `sef.requestId`, `hr.fiscalCode`). Namespaced by adapter.
### 2. EInvoiceAdapter Interface
All jurisdiction-specific e-invoice integrations **must** implement this contract.
**File:** `EInvoiceAdapter.kt` (see `~/ALAI/products/Bilko/scratch-api/src/main/kotlin/no/alai/bilko/einvoice/EInvoiceAdapter.kt`)
**Four methods (ADR-019 lifecycle contract):**
```kotlin
interface EInvoiceAdapter {
val jurisdiction: TaxJurisdiction
val lifecycleState: AdapterLifecycleState // STUB | SANDBOX_VERIFIED | PRODUCTION_CERTIFIED
@Throws(AdapterException::class)
fun serialize(invoice: CanonicalInvoice): ByteArray
@Throws(AdapterException::class)
fun submit(serializedInvoice: ByteArray, invoice: CanonicalInvoice): SubmitResult
@Throws(AdapterException::class)
fun pollStatus(submissionId: String, invoice: CanonicalInvoice): EInvoiceStatus
@Throws(AdapterException::class)
fun parseIncoming(rawPayload: ByteArray): CanonicalInvoice
}
```
**Lifecycle states (ADR-019):**
- `STUB`: Not implemented — all methods throw `AdapterException(NOT_IMPLEMENTED)`
- `SANDBOX_VERIFIED`: Tested against fiscal platform sandbox (e.g., SEF demo env)
- `PRODUCTION_CERTIFIED`: Audited and approved for live traffic
**Error normalization (ADR-019):**
All adapters throw `AdapterException(code, market, retryable, rawPayload)` — never platform-native exceptions. Canonical error codes: `NETWORK_TIMEOUT`, `AUTH_TOKEN_EXPIRED`, `VALIDATION_SCHEMA_ERROR`, `PLATFORM_MAINTENANCE`, etc.
### 3. Adapter Implementations — Phase 1 Priorities
| Jurisdiction | Adapter Class | Lifecycle State (2026-04-22) | Notes |
| ------------ | ----------------------- | ---------------------------- | -------------------------------------------------- |
| RS | `SEFEInvoiceAdapter` | SANDBOX_VERIFIED | Outbound + inbound implemented (MC #8682) |
| HR | `HRFISKEInvoiceAdapter` | STUB | Blocked on FINA certificate (CEO decision pending) |
| BA-FED | `CPFEInvoiceAdapter` | STUB | CPF spec not published |
| BA-RS | `UINOEInvoiceAdapter` | STUB | UINO spec unavailable |
**SEF Serbia (RS) — Reference Implementation:**
- Serialization: UBL 2.1 XML + SEF JSON envelope
- Submission: HTTPS POST to `https://efaktura.mfin.gov.rs/api/publicApi/invoice`
- Authentication: X.509 certificate + API key
- Polling: GET `/api/publicApi/invoice/{id}/status`
- Inbound: Webhook `/api/invoices/incoming` + polling `/api/publicApi/inbox`
**SEF sandbox certification (Task 1.5):**
5 invoice types tested in sandbox:
1. B2B outbound invoice
2. B2G outbound invoice (to government buyer)
3. Credit note
4. Cancelled invoice
5. Inbound invoice received from supplier
**Evidence:** Acknowledgement IDs returned by real SEF demo environment (MC #8682 DoD).
### 4. Canonical → Platform Serialization Flow
```mermaid
sequenceDiagram
participant Core as Invoice Service
participant Plugin as CountryPlugin (RS)
participant Adapter as SEFEInvoiceAdapter
participant SEF as SEF Platform
Core->>Plugin: generateEInvoiceXml(invoice)
Plugin->>Adapter: serialize(invoice)
Adapter->>Adapter: Map CanonicalInvoice → UBL 2.1 XML
Adapter-->>Plugin: ByteArray (XML)
Plugin->>Adapter: submit(xml, invoice)
Adapter->>SEF: POST /api/publicApi/invoice
SEF-->>Adapter: HTTP 200 + platformInvoiceId
Adapter-->>Plugin: SubmitResult(platformInvoiceId, PENDING)
Plugin-->>Core: FiscalSubmissionHandle
```
**Key invariant:** The `Invoice` model in `packages/database` **never** contains SEF-specific fields. All jurisdiction logic flows through the plugin and adapter layers.
---
## Consequences
### Positive
1. **Platform independence:** When SEF changes XML schema (happened 2024), only `SEFEInvoiceAdapter` changes. Core untouched.
2. **Testability:** Each adapter has isolated unit tests. Mock platforms for regression.
3. **Incremental rollout:** HR/BA adapters can remain stubs until fiscal platforms are ready.
4. **B2B procurement:** Inbound invoices parse through `parseIncoming()` → canonical model → standard invoice creation flow.
### Negative
1. **Mapping complexity:** UBL 2.1 has 200+ optional fields. CanonicalInvoice supports ~40. Adapter must decide what to include.
2. **Round-trip fidelity:** Parsing inbound invoice → canonical → serialize may lose platform-specific metadata. Use `adapterMetadata` map.
3. **Certification burden:** Sandbox testing required per adapter before production (Phase 1 Task 1.5).
### Risks
1. **SEF/FINA breaking changes:** Government platforms have no SLA, no deprecation policy. **Mitigation:** Adapter feature flags (ADR-019 Task 4.5) — disable broken adapter without redeploy.
2. **CPF/UINO spec delays:** BiH platforms have no published tech specs. **Mitigation:** Stub adapters document `NOT_IMPLEMENTED`; GA proceeds without BiH e-invoicing.
3. **UBL 2.1 extensions:** Some platforms extend UBL with custom namespaces. **Mitigation:** `adapterMetadata` carries extension data; serialization handles custom namespaces.
---
## Implementation Notes
### Validation Strategy
**EN 16931 validation (core):**
- Invoice must have ≥1 line
- `sum(lines.lineTotal)` == `invoice.totalAmountExclVat`
- `totalAmountExclVat + totalVatAmount` == `totalAmountInclVat`
- All amounts ≥ 0 (credit notes use negative `quantity`, not negative `unitPrice`)
**Jurisdiction validation (adapter):**
- SEF Serbia: Buyer VAT ID must match PIB format (9 digits)
- HR Croatia: Supplier must be FINA-registered (OIB validation)
- Validation errors throw `AdapterException(VALIDATION_BUSINESS_RULE, retryable=false)`
### Async Submission Pattern (Transactional Outbox)
SEF and HR-FISK are **asynchronous**. Recommended pattern:
```kotlin
// 1. Persist invoice to DB (transaction T1)
// 2. Write to {market}_outbox table (still in T1)
// 3. Commit T1
// 4. Background worker polls outbox, calls adapter.submit()
// 5. Update outbox row with platformInvoiceId
// 6. Background worker polls adapter.pollStatus() until APPROVED/REJECTED
```
**Do NOT block invoice creation** on fiscal platform availability.
### Sandbox Environments
| Platform | Sandbox URL | Credential Type |
| ------------ | --------------------------------- | ------------------------- |
| SEF (RS) | https://demo-efaktura.mfin.gov.rs | Test X.509 cert + API key |
| HR-FISK (HR) | https://demo.fiskalizacija.hr | Test FINA certificate |
| CPF (BA-FED) | N/A | Not available |
| UINO (BA-RS) | N/A | Not available |
**Sandbox credential convention (ADR-019):**
- Secret store path: `Bilko/sandbox/{market}/{secret-name}`
- Example: `Bilko/sandbox/RS/sef-api-key`
---
## References
- **EN 16931 spec:** https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/Compliance+with+eInvoicing+standard
- **UBL 2.1 spec:** http://docs.oasis-open.org/ubl/UBL-2.1.html
- **SEF Serbia docs:** https://www.efaktura.gov.rs/en
- **HR-FISK Croatia:** https://www.porezna-uprava.hr/HR_Fiskalizacija/Stranice/Naslovna.aspx
- **Implementation:** `~/ALAI/products/Bilko/scratch-api/src/main/kotlin/no/alai/bilko/einvoice/`
- **Master plan:** `~/system/specs/bilko-multi-market-architecture-plan.md` (Phase 1 Task 1.4, Task 1.5)
- **Related ADRs:**
- ADR-015: Four-Jurisdiction Plugin Architecture
- ADR-019: Integration Adapter Registry
---
## Approval
**Approved:** 2026-04-21 by CEO Alem Basic
**Execution:** Phase 0 Task 0.2, Phase 1 Task 1.4 (completed 2026-04-22, MC #8682)
No comments to display
No comments to display