Skip to main content

ADR-019: Integration Adapter Registry

# ADR-019 — Integration Adapter Registry

**Status:** Accepted
**Date:** 2026-05-11
**Author:** Petter Graff (CodeCraft — Architecture Lead)
**Decision-maker:** CEO Alem Bašić
**Mehanik clearance:** /tmp/mehanik-cleared-100362
**MC Task:** #100362 (Phase 0' ADR Consolidation)
**Cross-references:**

- ADR-015 (CountryPlugin — plugin selects adapters for its market; plugin version compatibility)
- ADR-016 (EInvoiceAdapter — one of the 7 adapter categories; lifecycle states formalised here)
- ADR-023 (routing — market resolved at edge before adapter dispatch)
- `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` (reference impl)
- `apps/api/src/main/kotlin/no/alai/bilko/einvoice/EInvoiceTypes.kt` (AdapterLifecycleState on disk)
- Plan v3 §6 Phase 0' Task 0'4 — `~/system/specs/bilko-multi-market-architecture-plan-v3-2026-05-11.md`

---

## 1. Context

### 1.1 Problem: Seven Integration Surfaces, No Governance

Bilko integrates with external systems across seven functional domains. As of 2026-05-11,
only one adapter exists (`StorecoveHrFiskEInvoiceAdapter`). Without a registry and governance
model, adding the second adapter (SEF for RS) and every subsequent adapter will produce:

1. Inconsistent error handling — platform-native exceptions leaking across boundaries
2. No feature-flag mechanism — a broken SEF adapter takes down all RS users
3. Secret sprawl — `STORECOVE_API_KEY` as an env var pattern, but no taxonomy when
   there are 7 adapters × 4 markets × 3 environments = up to 84 secrets
4. No observability standard — each adapter invents its own logging and metrics
5. No lifecycle discipline — adapters deployed to production without sandbox verification

### 1.2 Reference Implementation Patterns

`StorecoveHrFiskEInvoiceAdapter.kt` already demonstrates all the patterns this ADR
formalises. This ADR makes those patterns enforceable for all future adapters:

| Pattern                                          | StorecoveHrFiskEInvoiceAdapter          | ADR-019 makes it |
| ------------------------------------------------ | --------------------------------------- | ---------------- |
| PII redaction before logging                     | Lines 24–59 (`sanitizeForLog`)          | Mandatory        |
| `AdapterException` only (no platform exceptions) | Lines 469–516 (`StorecoveErrorMapper`)  | Mandatory        |
| Per-adapter Prometheus metrics                   | Lines 537–540 (`StorecoveMetrics`)      | Mandatory        |
| Lifecycle state field                            | Lines 547–548 (`lifecycleState = STUB`) | Mandatory        |
| Idempotency key on submit                        | Lines 591–600 (D5 comment)              | Mandatory        |
| Credentials NOT required for serialize()         | Lines 567–571                           | Mandatory        |
| Startup credential validation flag               | Lines 83–138                            | Recommended      |

---

## 2. Decision

### 2.1 Seven Adapter Categories

Every external integration belongs to exactly one of the following categories.
Each category is a Kotlin interface in `apps/api/src/main/kotlin/no/alai/bilko/adapter/`.

| Category | Interface                | Purpose                                                                    | Markets                                                      |
| -------- | ------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------ |
| 1        | `EInvoiceAdapter`        | E-invoice serialization + fiscal platform submission                       | HR (Storecove), RS (SEF), BA-FED (CPF), BA-RS (UINO)         |
| 2        | `CompanyRegistryAdapter` | Company data lookup (name, address, tax status) from government registries | HR (FINA), RS (APR), BA (stub)                               |
| 3        | `BankStatementAdapter`   | Bank statement import (MT940, CAMT.053, PSD2 AISP)                         | All markets — via Tok Open Banking platform                  |
| 4        | `ExchangeRateAdapter`    | FX rate feed (daily/live)                                                  | All markets (ECB primary, HNB for HR, NBS for RS)            |
| 5        | `TaxFilingAdapter`       | Electronic VAT/CIT return submission to tax authority                      | HR (ePorezna), RS (ePorezi), BA (TBD)                        |
| 6        | `FiscalDeviceAdapter`    | Fiscal receipt device or cloud fiscal service                              | HR (Fiskalizacija cloud cert), RS (LPFR chip card), BA (TBD) |
| 7        | `QESSigningAdapter`      | Qualified Electronic Signature for invoice signing                         | HR (FINA QES), RS (stub), BA (stub)                          |

**Current implementation status:**

- `EInvoiceAdapter`: `StorecoveHrFiskEInvoiceAdapter` (HR, STUB lifecycle)
- All other categories: NOT YET IMPLEMENTED

### 2.2 Common Interface Contract

Every adapter interface extends a common `BilkoAdapter` base:

```kotlin
package no.alai.bilko.adapter

import no.alai.bilko.country.TaxJurisdiction
import no.alai.bilko.einvoice.AdapterLifecycleState

/**
 * Base contract for all Bilko integration adapters.
 *
 * Every adapter implementation MUST:
 * 1. Expose [jurisdiction] and [lifecycleState] as first-class properties.
 * 2. Throw only [AdapterException] — NEVER platform-native exceptions.
 * 3. Pass all log writes through [sanitizeForLog] (defined per-adapter for PII fields).
 * 4. Record Prometheus metrics on every external call (see §2.6).
 * 5. Not require credentials for read-only / serialization operations.
 */
interface BilkoAdapter {
    val jurisdiction: TaxJurisdiction
    val lifecycleState: AdapterLifecycleState
    val adapterVersion: String  // Semantic version string, e.g. "1.0.0"
}
```

### 2.3 AdapterException — Canonical Error Contract

All adapters throw `AdapterException` and nothing else. This exception type is the
single crossing point from adapter space to core service space.

```kotlin
package no.alai.bilko.adapter

import no.alai.bilko.country.TaxJurisdiction

/**
 * Canonical adapter error. The ONLY exception type that crosses the adapter boundary.
 *
 * INVARIANT: Core services catch AdapterException only. They MUST NOT catch
 * platform-native exceptions (Ktor ResponseException, HttpRequestTimeoutException,
 * java.net.SocketTimeoutException, etc.). Map those to AdapterException in the adapter.
 *
 * [retryable]: if true, caller may retry with exponential backoff.
 * [rawPayload]: sanitized (PII-redacted) raw response body for audit. NEVER raw.
 */
data class AdapterException(
    val code: AdapterErrorCode,
    val market: TaxJurisdiction,
    val retryable: Boolean,
    val rawPayload: String,
    override val message: String = code.name,
    override val cause: Throwable? = null,
) : RuntimeException(message, cause)

/**
 * Canonical error codes — adapter-independent.
 *
 * Adapters map platform-specific HTTP status codes and error bodies to these codes.
 * See StorecoveErrorMapper (lines 469–516) for the HR reference mapping.
 */
enum class AdapterErrorCode {
    // Validation errors — not retryable
    VALIDATION_SCHEMA_ERROR,       // Invalid document structure (HTTP 400/422)
    VALIDATION_BUSINESS_RULE,      // Business rule violation (e.g., invalid OIB, non-EUR currency)
    VALIDATION_DUPLICATE_DOCUMENT, // Idempotency conflict (HTTP 409)

    // Authentication/authorisation — not retryable
    AUTH_INVALID_CREDENTIALS,      // API key invalid / token expired / certificate rejected

    // Platform errors — retryable
    PLATFORM_RATE_LIMITED,         // HTTP 429 — back off and retry
    PLATFORM_MAINTENANCE,          // HTTP 503 — platform in scheduled maintenance
    PLATFORM_INTERNAL_ERROR,       // HTTP 5xx — transient platform error

    // Network errors — retryable
    NETWORK_TIMEOUT,               // Connection or read timeout
    NETWORK_UNREACHABLE,           // DNS resolution failure or TCP refused

    // Implementation status — not retryable
    NOT_IMPLEMENTED,               // Adapter is in STUB lifecycle state
    UNKNOWN,                       // Unmapped error; always log rawPayload for triage
}
```

**Mapping rule for new adapters:** Every HTTP status code the platform can return MUST
have a mapping to an `AdapterErrorCode`. Use `UNKNOWN` only as a catch-all, never as
the primary mapping for a known status code. See `StorecoveErrorMapper` (lines 469–516
in `StorecoveHrFiskEInvoiceAdapter.kt`) as the reference pattern.

### 2.4 AdapterConfig — DB-Level Feature Flag

Every adapter is gated by an `AdapterConfig` row. An adapter MUST NOT execute any
network call if its `AdapterConfig.enabled = false`. This allows disabling a broken
adapter without redeployment.

```sql
-- V20__adapter_config.sql (Phase 1H — during Phase 2A window)

CREATE TABLE adapter_config (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    market       VARCHAR(8) NOT NULL,
                 -- TaxJurisdiction enum value: 'HR', 'RS', 'BA_FED', 'BA_RS'
    adapter_type VARCHAR(32) NOT NULL,
                 -- Matches the 7 categories: 'EINVOICE', 'COMPANY_REGISTRY',
                 -- 'BANK_STATEMENT', 'EXCHANGE_RATE', 'TAX_FILING',
                 -- 'FISCAL_DEVICE', 'QES_SIGNING'
    enabled      BOOLEAN NOT NULL DEFAULT FALSE,
    reason       TEXT,
                 -- Why disabled, e.g. "MC #8675 pending — Storecove account not activated"
                 -- Required when enabled=false.
    updated_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_by   TEXT NOT NULL DEFAULT 'system',  -- MC task ID or admin user
    CONSTRAINT pk_adapter_config UNIQUE (market, adapter_type)
);

-- Seed: all adapters start disabled
INSERT INTO adapter_config (market, adapter_type, enabled, reason, updated_by)
VALUES
    ('HR', 'EINVOICE',         false, 'MC #8675 — Storecove account pending', 'MC-100362'),
    ('HR', 'COMPANY_REGISTRY', false, 'Not implemented — Phase 1S', 'MC-100362'),
    ('HR', 'BANK_STATEMENT',   false, 'Tok AISP integration pending', 'MC-100362'),
    ('HR', 'EXCHANGE_RATE',    false, 'ECB feed not configured', 'MC-100362'),
    ('HR', 'TAX_FILING',       false, 'ePorezna integration Phase 2 scope', 'MC-100362'),
    ('HR', 'FISCAL_DEVICE',    false, 'Fiskalizacija cert not configured', 'MC-100362'),
    ('HR', 'QES_SIGNING',      false, 'FINA QES Phase 2 scope', 'MC-100362'),
    ('RS', 'EINVOICE',         false, 'SEF adapter Phase 1S scope', 'MC-100362'),
    ('BA_FED', 'EINVOICE',     false, 'CPF platform TBD ~2027', 'MC-100362'),
    ('BA_RS', 'EINVOICE',      false, 'UINO platform TBD', 'MC-100362');
-- (Remaining BA/RS adapter rows follow same pattern)

-- Admin can enable without redeploy:
-- UPDATE adapter_config SET enabled = true, reason = NULL, updated_by = 'MC-8675-DONE'
-- WHERE market = 'HR' AND adapter_type = 'EINVOICE';
```

**Kotlin enforcement pattern:**

```kotlin
// In the adapter registry (apps/api/src/main/kotlin/no/alai/bilko/adapter/AdapterRegistry.kt)
fun requireEnabled(market: TaxJurisdiction, adapterType: String) {
    val config = adapterConfigRepository.find(market, adapterType)
        ?: throw AdapterException(
            code = AdapterErrorCode.NOT_IMPLEMENTED,
            market = market,
            retryable = false,
            rawPayload = "",
            message = "No AdapterConfig row for ($market, $adapterType) — run Flyway V20"
        )
    if (!config.enabled) {
        throw AdapterException(
            code = AdapterErrorCode.NOT_IMPLEMENTED,
            market = market,
            retryable = false,
            rawPayload = "",
            message = "Adapter ($market, $adapterType) is disabled: ${config.reason}"
        )
    }
}
```

### 2.5 Lifecycle States and Transition Criteria

Formalised from `AdapterLifecycleState` enum in `EInvoiceTypes.kt` lines 22–26,
and from ADR-016 §2.3. Applies to ALL adapter categories.

```
STUB ──────────────────► SANDBOX_VERIFIED ──────────────────► PRODUCTION
```

**STUB** (initial state for all adapters):

- Compiles and registers successfully
- All network methods throw `AdapterException(code=NOT_IMPLEMENTED)`
- `serialize()` / read-only operations may work (e.g., HR serialize works in STUB)
- `AdapterConfig.enabled` is `false`

**SANDBOX_VERIFIED** transition criteria (all must be true):

- Minimum 5 distinct happy-path test cases pass against the real sandbox platform
  (not mocked — real submission IDs, real response payloads)
- All test case submission IDs are archived in BookStack as evidence
- Error mapping verified: at least HTTP 400, 401, 409, 429, 503, 5xx all produce
  correct `AdapterErrorCode` values (not `UNKNOWN`)
- Proveo sign-off with evidence file path in MC task
- `AdapterConfig.enabled` can be set to `true` after this point

**PRODUCTION** transition criteria (all must be true):

- `SANDBOX_VERIFIED` already achieved
- Securion review of adapter error handling, PII sanitization, and idempotency key
  implementation — no critical findings
- 30 consecutive days on stage Cloud Run with:
  - Zero `PLATFORM_INTERNAL_ERROR` alerts
  - Zero cross-market routing errors
  - `bilko_integration_request_total{status="error"}` < 1% of total requests
- CEO approval for production activation
- `AdapterConfig.enabled = true` in production DB (separate row from stage DB)

### 2.6 Secret Taxonomy

Runtime secrets follow the pattern `Bilko/{env}/{market}/{secret-name}`.

**Env first, not market first.** This ensures that all production secrets are under
`Bilko/production/` and can be granted/revoked as a unit for environment promotion.

```
Bilko/
  production/
    HR/
      STORECOVE_API_KEY
      STORECOVE_LEGAL_ENTITY_ID
      FINA_QES_CERTIFICATE         (Phase 2)
      EPOREZNA_CLIENT_SECRET       (Phase 2)
    RS/
      SEF_API_KEY                  (Phase 1S)
      LPFR_DEVICE_CERT             (Phase 2)
    BA_FED/
      CPF_API_KEY                  (Phase 1B — pending platform launch)
    BA_RS/
      UINO_API_KEY                 (Phase 1B — pending platform launch)
  stage/
    HR/
      STORECOVE_API_KEY
      STORECOVE_LEGAL_ENTITY_ID
    RS/
      SEF_API_KEY
    ...
  local/
    HR/
      STORECOVE_API_KEY            (developer sandbox credentials only)
    ...
```

**Secret resolution hierarchy:**

1. Runtime: GCP Secret Manager (current) — accessed via `SecretResolver` interface
2. Break-glass: Vaultwarden (`vault.basicconsulting.no`) — human access only, NOT runtime source
3. Local dev: `.env.local` file (`.gitignore`'d) — NEVER committed

```kotlin
// apps/api/src/main/kotlin/no/alai/bilko/adapter/SecretResolver.kt

/**
 * Abstracts secret retrieval behind a testable interface.
 *
 * Production implementation: GCP Secret Manager.
 * Test implementation: environment variables / in-memory map.
 *
 * Secret path convention: Bilko/{env}/{market}/{secret-name}
 */
interface SecretResolver {
    /**
     * Resolves a secret value by its canonical path.
     *
     * @param path e.g. "Bilko/production/HR/STORECOVE_API_KEY"
     * @return Secret value, or null if not found.
     * @throws AdapterException(AUTH_INVALID_CREDENTIALS) if path exists but value is empty/blank.
     */
    fun resolve(path: String): String?

    /**
     * Convenience method: builds canonical path and resolves.
     * @param env "production" | "stage" | "local"
     * @param market TaxJurisdiction enum value as string
     * @param secretName The specific secret name
     */
    fun resolve(env: String, market: String, secretName: String): String? =
        resolve("Bilko/$env/$market/$secretName")
}
```

**Vaultwarden is NOT the runtime secret source.** Vaultwarden is the human break-glass
vault for emergency access. Do not write Kotlin code that reads from Vaultwarden at
runtime. GCP Secret Manager is the runtime source.

### 2.7 Observability Mandate

Every adapter MUST emit the following for every network call:

**Structured log line (one per call):**

```
level=INFO market=HR integration=EINVOICE env=production org_id=<uuid>
  action=submit status=SUCCESS duration_ms=234 submission_id=<guid>
```

Required fields: `market`, `integration`, `env`, `org_id`. Optional but recommended:
`duration_ms`, `submission_id`, `attempt` (for retries).

**NEVER log:**

- OIB, PIB, JIB (tax IDs)
- IBAN
- `document_data` (invoice XML body)
- `api_key`, `api_secret`

Use `sanitizeForLog()` (pattern from `StorecoveHrFiskEInvoiceAdapter.kt` lines 24–59)
before any log write that touches a response body.

**Prometheus metrics (one counter per adapter):**

```kotlin
// apps/api/src/main/kotlin/no/alai/bilko/adapter/AdapterMetrics.kt

/**
 * Prometheus counter for all adapter network calls.
 *
 * Labels: market, integration, status (SUCCESS | ERROR | NOT_IMPLEMENTED | TIMEOUT)
 *
 * Example PromQL for HR e-invoice error rate:
 *   rate(bilko_integration_request_total{market="HR",integration="EINVOICE",status="ERROR"}[5m])
 *   /
 *   rate(bilko_integration_request_total{market="HR",integration="EINVOICE"}[5m])
 */
// bilko_integration_request_total{market, integration, status}
// bilko_integration_request_duration_seconds{market, integration, status}
```

Per-(market, integration) alert rule:

- Error rate > 10% over 5 minutes: PAGE (PagerDuty or Slack alert)
- Error rate > 25% over 1 minute: CRITICAL (adapter auto-disabled via `AdapterConfig`)

### 2.8 Adapter Versioning

Each adapter declares `val adapterVersion: String` (semantic version, e.g., `"1.0.0"`).
The corresponding `CountryPlugin` implementation declares the minimum adapter version
it requires:

```kotlin
// In PluginHR:
companion object {
    const val MIN_EINVOICE_ADAPTER_VERSION = "1.0.0"
}

// Startup check in DI.kt:
val adapter = StorecoveHrFiskEInvoiceAdapter()
require(semVer(adapter.adapterVersion) >= semVer(PluginHR.MIN_EINVOICE_ADAPTER_VERSION)) {
    "PluginHR requires EInvoiceAdapter >= ${PluginHR.MIN_EINVOICE_ADAPTER_VERSION}, got ${adapter.adapterVersion}"
}
```

Adapters are versioned independently of the `CountryPlugin`. Breaking changes to
an adapter interface (e.g., new required parameter in `submit()`) require a major
version bump and a coordinated plugin + adapter update.

### 2.9 Idempotency Requirements

**All submit-type methods in all adapters MUST include an idempotency key.**

The idempotency key format is adapter-specific, but the value MUST be derived
deterministically from the invoice or entity content — never a random UUID.

| Adapter              | Method     | Idempotency key derivation                                           |
| -------------------- | ---------- | -------------------------------------------------------------------- |
| EInvoiceAdapter (HR) | `submit()` | `SHA-256(invoice.id + invoice.invoiceNumber)` — matches Storecove D5 |
| EInvoiceAdapter (RS) | `submit()` | `SHA-256(invoice.id + invoice.invoiceNumber)` (same pattern)         |
| TaxFilingAdapter     | `submit()` | `SHA-256(filing.periodStart + filing.periodEnd + org.taxId)`         |
| QESSigningAdapter    | `sign()`   | `SHA-256(document.contentHash + signer.taxId)`                       |

**Rationale:** A network timeout after the platform receives the request but before
the response arrives will cause the client to retry. Without idempotency, this creates
a duplicate document. Storecove returns HTTP 409 on duplicate `document_id` (D2 in
`StorecovePayloadBuilder.wrap()` lines 420–436) — the pattern must be replicated.

---

## 3. Implementation Path

| Phase    | Task                                                         | Deliverable                                                     | Status              |
| -------- | ------------------------------------------------------------ | --------------------------------------------------------------- | ------------------- |
| Phase 0' | This ADR written to disk                                     | `ADR-019-INTEGRATION-ADAPTER-REGISTRY.md`                       | DONE                |
| Phase 1H | `AdapterException` + `AdapterErrorCode` formalized           | `adapter/AdapterException.kt` (already exists — verify package) | Verify existing     |
| Phase 1H | `AdapterConfig` Flyway migration (V20)                       | `V20__adapter_config.sql`                                       | BLOCKED BY Phase 2A |
| Phase 1H | `SecretResolver` interface + GCP impl                        | `adapter/SecretResolver.kt` + `GcpSecretResolver.kt`            | Phase 1H.4+         |
| Phase 1H | `AdapterRegistry` + `requireEnabled()` check                 | `adapter/AdapterRegistry.kt`                                    | Phase 1H.4+         |
| Phase 1H | Prometheus metrics wired in `StorecoveHrFiskEInvoiceAdapter` | `StorecoveMetrics.kt` (skeleton exists)                         | Phase 1H.2+         |
| Phase 1S | SEF RS EInvoiceAdapter                                       | `country/rs/SefRsEInvoiceAdapter.kt`                            | Post-HR GA          |
| Phase 1B | CPF BA-FED + UINO BA-RS stubs                                | `country/ba/Cpf*`, `country/ba/Uino*`                           | Post-RS GA          |

---

## 4. Consequences

### 4.1 Positive

- **Zero platform exception leakage.** `AdapterException` as the only crossing type means
  core services have one error handler for all 7 × 4 = 28 potential adapter instances.
- **Hot disable without redeploy.** `AdapterConfig.enabled = false` disables a broken
  adapter in < 1 minute (DB write). No restart required. Incident response time drops
  from minutes (redeploy) to seconds (DB update).
- **Observability from day one.** Every adapter emits standardized metrics. Error rate
  alerts fire before users report issues.
- **Secret hygiene.** Env-first taxonomy (`Bilko/{env}/{market}/{secret}`) makes
  environment promotion (stage → production) a structured operation, not an ad-hoc
  copy. Break-glass access is separated from runtime access.

### 4.2 Negative

- **Adapter scaffolding cost.** Each new adapter requires implementing the full
  interface contract, `AdapterConfig` rows, `SecretResolver` wiring, and Prometheus
  metrics. Estimate: 1–2 days for a new adapter in STUB state.
- **AdapterConfig is a deployment dependency.** The application fails at startup if
  `adapter_config` rows do not exist. Flyway V20 must run before the application
  version that adds `requireEnabled()` checks.

### 4.3 Risks

- **`AdapterConfig` DB unavailable.** If the database is unreachable, `requireEnabled()`
  fails, blocking all adapters. Mitigation: cache `AdapterConfig` in-memory at startup
  with a TTL of 5 minutes. Use cached state if DB is unreachable.
- **Metrics cardinality.** High `org_id` cardinality in metrics labels would cause
  Prometheus memory issues. The observability mandate specifies `org_id` in log lines,
  NOT in Prometheus labels. Labels are `market`, `integration`, `status` only — bounded
  cardinality.

---

## 5. References

| Reference                                        | Path                                                                                  | Lines   |
| ------------------------------------------------ | ------------------------------------------------------------------------------------- | ------- |
| `AdapterLifecycleState` enum (on disk)           | `apps/api/src/main/kotlin/no/alai/bilko/einvoice/EInvoiceTypes.kt`                    | 22–26   |
| `StorecoveErrorMapper` (error mapping reference) | `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` | 469–516 |
| `StorecoveMetrics` (metrics reference)           | `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` | 537–540 |
| PII sanitization reference                       | `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` | 24–59   |
| Idempotency key (D5) reference                   | `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` | 94–98   |
| `StorecovePayloadBuilder` (D2 document_id)       | `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` | 420–436 |
| ADR-016 §2.3 (lifecycle states — EInvoice)       | `docs/architecture/ADR-016-EINVOICE-ADAPTER.md`                                       | §2.3    |
| ADR-015 §2.4 (DI registration pattern)           | `docs/architecture/ADR-015-FOUR-JURISDICTION-PLUGIN.md`                               | §2.4    |
| Plan v3 §6 Task 0'4 acceptance criteria          | `~/system/specs/bilko-multi-market-architecture-plan-v3-2026-05-11.md`                | 279–290 |

---

## 6. Approval

**Status:** Accepted
**Unblocks:**

- Phase 1H: `AdapterConfig` Flyway V20 migration
- Phase 1H: `SecretResolver` GCP implementation
- Phase 1H: Prometheus metrics wiring in `StorecoveHrFiskEInvoiceAdapter`
- Phase 1S: SEF RS adapter scaffolding (knows the contract to implement against)
- Phase 1B: CPF/UINO BA adapter stubs

| Role                             | Sign                                  | Date       |
| -------------------------------- | ------------------------------------- | ---------- |
| Architecture Lead (Petter Graff) | Signed                                | 2026-05-11 |
| CEO (Alem Bašić)                 | Not required for registry pattern ADR | —          |

---

## 7. Document History

| Date       | Author       | Change                                            |
| ---------- | ------------ | ------------------------------------------------- |
| 2026-05-11 | Petter Graff | Initial — Phase 0' ADR consolidation (MC #100362) |