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)
HR14 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_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-nativeUNKNOWN 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.