Skip to main content

ADR-019: Integration Adapter Registry

Author: ALAI, 2026

# 

ADR-019: Integration Adapter Registry

**

Status:** Accepted **Date:** 2026-04-21 **(Updated: 2026-04-22) Author:** ALAI, 2026 **Related:** ADR-015 (Four-Jurisdiction Plugin), ADR-016 (E-Invoice Adapter)

---
##

Context

Context

Bilko depends on **28+ external integration points**points across 4 jurisdictions:jurisdictions.

|

[... Integrationrest Typeof |existing RSmarkdown content truncated for brevity ...]

§2.3 Error Code Taxonomy (Serbia)Updated |2026-04-22)

HR

14 canonical error codes (Croatia)updated |from BA-FEDinitial |13 BA-RSduring |Phase Total1 |Track | ---------------- | ------------------------------- | ----------------------- | -------------- | ----------- | ------- | | E-Invoice | SEF | HR-FISK/FINA | CPF (stub) | UINO (stub) | 4 | | Company Registry | APR | FINA | UIO | PURS | 4 | | Bank Statement | MT940/CAMT.053 (shared) |B|added ADAPTER_DISABLED for feature flag support):

Connectivity (retryable=true)

  • NETWORK_TIMEOUT|Socket timeout, connection timeout
  • NETWORK_UNREACHABLE|Network 1unavailable, |DNS |failure
  • Exchange
Rate | ECB (shared) + NBS (regulatory) | HNB | CBBH | CBBH | 3 | | VAT Filing | ePorezi | ePorezna | UIO | PURS | 4 | | Financial Filing | APR XBRL | FINA | UIO | PURS | 4 | | SAF-T Export | OECD SAF-T (shared) | — | — | — | 1 | | QES Signing | HALCOM (planned) | FINA (planned) | — | — | 2 | | Fiscal Device | LPFR (planned) | Fiskalizacija (planned) | ESET (planned) | — | 3 | | **Total** | **9+** | **7+** | **4** | **4** | **28+** | ### Problem Statement Each adapter has: - **Different lifecycle states** (stub vs sandbox-tested vs production-certified) - **Different error semantics** (SEF 409 = duplicate, HR-FISK 409 = conflict) - **Different credential types** (X.509 cert, API key, OAuth2, none) - **Different sandbox availability** (SEF has sandbox, CPF has none) - **Different versioning** (government APIs change without notice) **Without a registry contract:** 1. Adapters throw platform-native exceptions → error handling is per-adapter, not canonical 2. No lifecycle gating → stub adapters deployed to production → runtime `NotImplementedError` 3. No feature flags → broken adapter blocks entire market launch 4. No observability tagging → can't distinguish SEF outage from application bug --- ## Decision ### 1. Canonical Adapter Interface Contract All integration adapters (e-invoice, company registry, exchange rate, VAT filing, etc.) **must** implement: ```kotlin interface Adapter { /** Tax jurisdiction this adapter handles */ val jurisdiction: TaxJurisdiction /** Current lifecycle certification state */ val lifecycleState: AdapterLifecycleState /** Adapter type for registry dispatch */ val adapterType: AdapterType } enum class AdapterType { E_INVOICE, COMPANY_REGISTRY, EXCHANGE_RATE, BANK_STATEMENT, TAX_FILING, FINANCIAL_FILING, QES_SIGNING, FISCAL_DEVICE } ``` **All adapters throw `AdapterException`** (never platform-native exceptions): ```kotlin class AdapterException( val code: AdapterErrorCode, val market: TaxJurisdiction, val retryable: Boolean, val rawPayload: String, message: String? = null, cause: Throwable? = null ) : Exception(message ?: code.name, cause) enum class AdapterErrorCode { // Connectivity NETWORK_TIMEOUT, NETWORK_UNREACHABLE, //

Authentication / Authorization AUTH_TOKEN_EXPIRED,(retryable=false)

AUTH_INVALID_CREDENTIALS,
    AUTH_INSUFFICIENT_PERMISSIONS,
  • AUTH_TOKEN_EXPIRED // OAuth2 token expired (caller should refresh token + retry once)
  • AUTH_INVALID_CREDENTIALS — Invalid API key, cert rejected
  • AUTH_INSUFFICIENT_PERMISSIONS — Valid credentials but insufficient permissions

Validation VALIDATION_SCHEMA_ERROR,(retryable=false)

VALIDATION_BUSINESS_RULE,
    VALIDATION_DUPLICATE_DOCUMENT,
  • VALIDATION_SCHEMA_ERROR // UBL schema violation, XML parse error
  • VALIDATION_BUSINESS_RULE — PIB must be 9 digits (RS), OIB must be 11 digits (HR), mandatory field missing
  • VALIDATION_DUPLICATE_DOCUMENT — Invoice already exists in fiscal platform

Platform-side PLATFORM_MAINTENANCE,(retryable=true)

PLATFORM_RATE_LIMITED,
    PLATFORM_INTERNAL_ERROR,
  • PLATFORM_MAINTENANCE // Scheduled maintenance, announced downtime
  • PLATFORM_RATE_LIMITED — HTTP 429, quota exceeded
  • PLATFORM_INTERNAL_ERROR — HTTP 5xx, platform bug

Adapter stateState NOT_IMPLEMENTED,(retryable=false)

//
  • NOT_IMPLEMENTED — Stub adapter ADAPTER_DISABLED,(lifecycle //state: STUB)
  • ADAPTER_DISABLED — Feature-flagged off //per Generic UNKNOWN } ``` **Error normalization exampleAdapterConfig.enabled (SEFADR-019 Serbia):**§3)
  • ```kotlin
//

Generic

Before
    (platform-native
  • UNKNOWN exception): throwUnexpected HttpException(409,error, "Invoiceunmapped already exists") // After (canonical exception): throw AdapterException(status code = AdapterErrorCode.VALIDATION_DUPLICATE_DOCUMENT, market = TaxJurisdiction.RS, retryable = false, rawPayload = response.body, message = "SEF returned 409: Invoice already exists" ) ``` Callers can now handle **all** adapters uniformly: ```kotlin try { adapter.submit(invoice) } catch (e: AdapterException) { if (e.retryable) { scheduler.retryLater(invoice) } else { logger.error("Non-retryable error: ${e.code}", e) alertUser(e.message) } } ``` ### 2. Adapter Lifecycle Model ``` ┌──────┐ sandbox testing ┌──────────────────┐ production audit ┌─────────────────────┐ │ STUB ├──────────────────────►│ SANDBOX_VERIFIED ├──────────────────────►│ PRODUCTION_CERTIFIED│ └──────┘ └──────────────────┘ └─────────────────────┘ ``` **STUB:** - All methods throw `AdapterException(NOT_IMPLEMENTED, retryable=false)`
  • -
Used for jurisdictions where fiscal platform has no published API (CPF Bosnia, UINO RS) - Example: `CPFEInvoiceAdapter.kt` (all methods are one-liners) **SANDBOX_VERIFIED:** - Tested against fiscal platform sandbox (SEF demo env, HR-FISK test env) - 5+ test scenarios passed (see ADR-016 SEF certification gate) - Credentials: `Bilko/sandbox/{market}/{secret-name}` in secret store - **Rule:** Promotion to SANDBOX_VERIFIED requires **machine-verified evidence** (acknowledgement IDs from real sandbox, not mocked responses) **PRODUCTION_CERTIFIED:** - Audited by Securion (security review) - Proveo regression suite passed (E2E tests) - Production credentials loaded: `Bilko/production/{market}/{secret-name}` - **Rule:** Only PRODUCTION_CERTIFIED adapters are enabled in production environment **Lifecycle state enforcement (Task 4.5):** ```kotlin class AdapterRegistry { fun dispatch(market: TaxJurisdiction, type: AdapterType): Adapter { val adapter = registry[market to type] ?: throw AdapterException(NOT_IMPLEMENTED, market, false, "") if (env == "production" && adapter.lifecycleState != PRODUCTION_CERTIFIED) { throw AdapterException(ADAPTER_DISABLED, market, false, "Adapter not certified for production") } return adapter } } ``` ### 3. Feature Flag Contract **Problem:** When SEF Serbia goes down or changes API → Bilko-RS invoicing breaks. Cannot wait for redeploy to disable broken adapter. **Solution:** Database-backed per-adapter feature flags (Task 4.5). **DDL:** ```sql CREATE TABLE adapter_config ( market CHAR(6) NOT NULL, -- RS, HR, BA_FED, BA_RS adapter_type VARCHAR(50) NOT NULL, -- E_INVOICE, COMPANY_REGISTRY, etc. enabled BOOLEAN NOT NULL DEFAULT TRUE, reason TEXT, -- "SEF API deprecated" / "HR-FISK sandbox down" updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by VARCHAR(100), -- Email of admin who toggled PRIMARY KEY (market, adapter_type) ); -- Audit log trigger (track every enable/disable) CREATE TABLE adapter_config_audit ( id BIGSERIAL PRIMARY KEY, market CHAR(6), adapter_type VARCHAR(50), enabled BOOLEAN, reason TEXT, updated_by VARCHAR(100), updated_at TIMESTAMPTZ DEFAULT NOW() ); ``` **Runtime check:** ```kotlin val config = db.query("SELECT enabled FROM adapter_config WHERE market = ? AND adapter_type = ?", market, adapterType).firstOrNull() ?: AdapterConfig(enabled = true) if (!config.enabled) { throw AdapterException(ADAPTER_DISABLED, market, false, "Adapter disabled: ${config.reason}") } ``` **Admin CLI:** ```bash # Disable SEF adapter (RS e-invoice) node ~/ALAI/products/Bilko/scripts/adapter-toggle.js disable RS E_INVOICE "SEF API breaking change" # Re-enable after fix node ~/ALAI/products/Bilko/scripts/adapter-toggle.js enable RS E_INVOICE ``` **Audit trail:** Every toggle logged to `adapter_config_audit` with timestamp + admin email. ### 4. Sandbox Credential Convention **Problem:** Production vs sandbox credentials mixed in `.env` → accidental production submission during testing. **Solution:** Secret store path convention (Task 4.3). **Path structure:** ``` Bilko/{env}/{market}/{secret-name} ``` **Examples:** - `Bilko/sandbox/RS/sef-api-key` - `Bilko/sandbox/RS/sef-certificate.p12` - `Bilko/production/RS/sef-api-key` - `Bilko/production/HR/fina-certificate.pem` **Kotlin `SecretResolver`:** ```kotlin interface SecretResolver { fun resolve(jurisdiction: TaxJurisdiction, key: SecretKey): String } enum class SecretKey { SEF_API_KEY, SEF_CERTIFICATE, FINA_CERTIFICATE, APR_API_KEY, // ... } class AWSSecretsManagerResolver(private val env: Environment) : SecretResolver { override fun resolve(jurisdiction: TaxJurisdiction, key: SecretKey): String { val path = "Bilko/${env.name.lowercase()}/${jurisdiction.name}/${key.name.lowercase().replace('_', '-')}" return awsSecretsManager.getSecretValue(path) } } ``` **Key security rule (Task 4.3):** - **Env-scoped first:** `staging` IAM role cannot access `production/` path - **Market-scoped second:** RS adapter cannot read HR secrets (principle of least privilege) **Vaultwarden = human break-glass backup only**, NOT runtime source. ### 5. Versioning Policy **Problem:** Government APIs change (SEF changed XML schema 2024). How to handle breaking changes without breaking old invoices? **Solution:** Adapters versioned independently of country plugin. **Version encoding:** ```kotlin interface EInvoiceAdapter : Adapter { val apiVersion: String // e.g., "sef-v2.1", "hr-fisk-v1.0" } ``` **Breaking change workflow:** 1. SEF announces schema change (e.g., new mandatory field) 2. Create `SEFEInvoiceAdapterV2` implementing new schema 3. Deploy both `V1` and `V2` adapters in parallel 4. Registry routes new invoices → `V2`, old invoices (re-submission) → `V1` 5. After 90 days, deprecate `V1` **Plugin declares minimum adapter version (ADR-015 extension):** ```kotlin class PluginRS : CountryPlugin { override val requiredAdapterVersions = mapOf( AdapterType.E_INVOICE to "sef-v2.1" ) } ``` Registry validates at dispatch time — prevents accidental routing to deprecated adapter. ### 6. Observability — Mandatory Market Tagging (Task 4.6) **Problem:** SEF outage vs Bilko bug — how to distinguish in metrics? **Solution:** Every log line + metric must have `market`, `integration`, `env`, `org_id` tags. **Structured logging:** ```kotlin logger.info( "Submitting invoice to fiscal platform", mapOf( "market" to market.name, "integration" to adapterType.name, "env" to env.name, "org_id" to org.id, "invoice_id" to invoice.id, "platform_endpoint" to endpoint ) ) ``` **Prometheus metric:** ```kotlin bilko_integration_request_total.labels( market = market.name, integration = adapterType.name, status = "success" ).inc() ``` **Grafana dashboard:** - **One row per market** — SEF errors don't pollute HR metrics - **Error-rate alert per market-integration pair** — `bilko_integration_request_total{market="RS", integration="E_INVOICE", status="error"} > 10/min` **Log retention (Task 4.6):** - Better Stack (Railway era) → Datadog or Grafana Cloud (AWS era) - Retention: 30 days standard, 1 year for audit-tagged logs --- ## Consequences ### Positive 1. **Uniform error handling:** All adapters throw `AdapterException` → single try/catch pattern 2. **Circuit breaker safety:** Feature flags disable broken adapter without redeploy 3. **Clear lifecycle gates:** Stub adapters never reach production (enforced by registry) 4. **Observability:** Metrics tagged by `(market, integration)` → can isolate SEF outage from app bug 5. **Incremental rollout:** RS market launches with PRODUCTION_CERTIFIED adapters; HR/BA launch when their adapters reach certification ### Negative 1. **Adapter boilerplate:** Every integration requires lifecycle state + error normalization + feature flag check 2. **Secret complexity:** 28 adapters × 2 envs (sandbox/prod) × avg 2 secrets = 112 secret paths 3. **Versioning burden:** When government API changes, must maintain 2 versions in parallel during migration ### Risks 1. **Certificate expiry:** X.509 certs for SEF/FINA expire — runtime failure if not monitored. **Mitigation:** Task 4.4 (cert expiry monitor with 60-day alert). 2. **Sandbox unreliability:** SEF sandbox has uptime <99% — certification gate may block unrelated work. **Mitigation:** Sandbox certification runs async; failures alert but don't block other markets. 3. **Feature flag abuse:** Admin disables adapter without incident → users complain. **Mitigation:** Audit log + Slack notification on every toggle. --- ##

Implementation Notesreference: ### P0 Adapters (Phase 1 Task 1.6) Implemented in `~/ALAI/products/Bilko/scratch-api/backend/src/main/kotlin/no/alai/bilko/adapter/`:AdapterException.kt

1.

Update **Companynote: Registry:**Verified -2026-04-22 `APRCompanyRegistryAdapter`per (RS) — PIB + MB lookup - Stub interfaces for HR/BA 2. **Bank Statement:** - `MT940CAMT053BankImportAdapter` (shared, all markets) — SEPA camt.053 and MT940 parsing 3. **Exchange Rate:** - `ECBExchangeRateAdapter` (shared) — exchangerate.host primary + Fixer.io fallback - `NBSRegulatoryRateAdapter` (RS) — NBS middle rate for regulatory/customs reporting only 4. **VAT Filing:** - `EPorezVATReturnAdapter` (RS) — PPPDV endpoint for VAT return e-filing (requires software registration — admin Task 4.4) 5. **Financial Filing:** - `APRXBRLFilingAdapter` (RS) — iXBRL financial statement submission to xbrl.apr.gov.rs 6. **SAF-T Export:** - `SAFTExportAdapter` (shared) — OECD SAF-T XML generator (scheduled export) **All adapters throw `AdapterException` with canonical error codes.** ### P1 Adapters (Phase 1 Task 1.7 — Stub Only) Scaffolded interfaces: - `PPPDPayrollTaxAdapter` (RS) - `HALCOMPostaCAQESAdapter` (RS) — Qualified Electronic Signature - `JMBGValidationAdapter` (RS) — national IDProveo validation -finding `SEFEOtpremnicaAdapter` (RS) — e-waybill for goods transport **Activation trigger:** 90 days post-GA (not critical path for MVP). ### Certificate Expiry Monitor (Task 4.4) **Cron (Railway cron / AWS EventBridge+Lambda):** ```kotlin // Daily at 06:00 UTC val certs = listOf( "Bilko/production/RS/sef-certificate.p12", "Bilko/production/HR/fina-certificate.pem" ) certs.forEach { path -> val cert = loadCertificate(path) val daysRemaining = ChronoUnit.DAYS.between(LocalDate.now(), cert.notAfter.toLocalDate()) when { daysRemaining <= 14 -> alertCritical("Cert expires in $daysRemaining days: $path") daysRemaining <= 60 -> alertWarning("Cert expires in $daysRemaining days: $path") } } ``` **Alert routing:** PagerDuty (critical) + Slack webhook (warning). **Grafana panel:** "Cert Expiry Days Remaining" per `(market, cert_type)`. **LPFR device pairing (RS):** 60-day alert triggers **human email workflow** to org admin (no auto-rotate — device pairing requires physical access). --- ## References - **ADR-015:** Four-Jurisdiction Plugin Architecture (defines `TaxJurisdiction`, `CountryPlugin`) - **ADR-016:** E-Invoice Adapter & UBL 2.1 Canonical Model (first adapter contract) - **Master plan:** `~/system/specs/bilko-multi-market-architecture-plan.md` (Phase 1 Task 0.5, 1.6, 1.7; Phase 4 Task 4.3–4.6) - **Implementation:** `~/ALAI/products/Bilko/scratch-api/src/main/kotlin/no/alai/bilko/adapter/` --- ## Approval **Approved:** 2026-04-21 by CEO Alem Basic **Execution:**during Phase 1 TasksTrack 1.6,B 1.7— taxonomy expanded to reflect granular error handling in live adapters (completedSEF 2026-04-22,Serbia, MCStorecove #8683);HR). PhaseSource 4ADR Tasksmarkdown 4.3–4.6file (notupdated yetin started)sync.