Skip to main content

Bilko ADR-019: Integration Adapter Registry

Author: ALAI, 2026

# ADR-019: Integration Adapter Registry

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

---

## Context

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

| Integration Type | RS (Serbia)                     | HR (Croatia)            | BA-FED         | BA-RS       | Total   |
| ---------------- | ------------------------------- | ----------------------- | -------------- | ----------- | ------- |
| E-Invoice        | SEF                             | HR-FISK/FINA            | CPF (stub)     | UINO (stub) | 4       |
| Company Registry | APR                             | FINA                    | UIO            | PURS        | 4       |
| Bank Statement   | MT940/CAMT.053 (shared)         | —                       | —              | —           | 1       |
| 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,
    AUTH_INVALID_CREDENTIALS,
    AUTH_INSUFFICIENT_PERMISSIONS,

    // Validation
    VALIDATION_SCHEMA_ERROR,
    VALIDATION_BUSINESS_RULE,
    VALIDATION_DUPLICATE_DOCUMENT,

    // Platform-side
    PLATFORM_MAINTENANCE,
    PLATFORM_RATE_LIMITED,
    PLATFORM_INTERNAL_ERROR,

    // Adapter state
    NOT_IMPLEMENTED,      // Stub adapter
    ADAPTER_DISABLED,     // Feature-flagged off

    // Generic
    UNKNOWN
}
```

**Error normalization example (SEF Serbia):**

```kotlin
// Before (platform-native exception):
throw HttpException(409, "Invoice already exists")

// After (canonical exception):
throw AdapterException(
    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 Notes

### P0 Adapters (Phase 1 Task 1.6)

Implemented in `~/ALAI/products/Bilko/scratch-api/src/main/kotlin/no/alai/bilko/adapter/`:

1. **Company Registry:**
   - `APRCompanyRegistryAdapter` (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 ID validation
- `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:** Phase 1 Tasks 1.6, 1.7 (completed 2026-04-22, MC #8683); Phase 4 Tasks 4.3–4.6 (not yet started)