Skip to main content

ADR-019: Integration Adapter Registry

# ADR-019 — MarketIntegration Priority:Adapter HR-FirstRegistry

Ratification

**Status:** Accepted — CEO override**Date:** 2026-05-0911 ratified as architectural decision Date: 2026-05-13 **Author:** Petter Graff (CodeCraft,CodeCraft — Architecture Lead) **Decision-maker:** CEO Alem Bašić **Mehanik overrideclearance:** issued/tmp/mehanik-cleared-100362 2026-05-09 ("B i onda dokumentuj Opcija C — ali prvo ide Hrvatska ne srbija") **MC Task:** #100586#100362 (Phase 0' ADR-017ADR RLSConsolidation) strategy**Cross-references:** +- ADR-019015 (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 priority)resolved Cross-references:

at
    edge
  • ADR-023before §6adapter CEOdispatch) override- 2026-05-09`apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` ("marketreference priorityimpl) is- HR → BA → SR")
  • ADR-015-FOUR-JURISDICTION-PLUGIN.md`apps/api/src/main/kotlin/no/alai/bilko/einvoice/EInvoiceTypes.kt` (TaxJurisdictionAdapterLifecycleState enum:on HR,disk) RS,- BA_FED, BA_RS)
  • ADR-016-EINVOICE-ADAPTER.md (HR Storecove Peppol path = Path B, signed 2026-04-24)
  • ADR-019-INTEGRATION-ADAPTER-REGISTRY.md (adapter lifecycle + HR EINVOICE = first to SANDBOX_VERIFIED)
  • Plan v3 §36 diffPhase table0' (v2Task was RS-first; v3 is HR-first0'4CEO override rationale)
  • Plan v3 §4d (HR critical path: Storecove MC #8675 as external dependency)
  • MC #8675 (Storecove account activation — CEO/John action, unblocks Phase 1H.6)
  • MEMORY: project_bilko_hr_competitor_intel_2026-05-10.md (competitive drivers)

Note on numbering: ADR-019-INTEGRATION-ADAPTER-REGISTRY.md (Phase 0', 2026-05-11) covers adapter governance. This document is a separate ADR-019 variant covering market priority strategy. Both are accepted. The naming convention adr-019-market-priority-hr-first.md distinguishes this document from the adapter registry document by suffix, consistent with the file system evidence requirements of MC #100586.


1. Context

1.1 The v2 Plan Was RS-First

The original v2 multi-market architecture plan (`~/system/specs/bilko-multi-market-architecture-plan.md,plan-v3-2026-05-11.md` 2026-04-21,--- now## superseded)1. prioritizedContext Serbia### as1.1 theProblem: launchSeven marketIntegration forSurfaces, twoNo reasons:

Governance
    Bilko
  1. The Serbian SEF (Sistem e-Faktura) e-invoice mandate was already in force
  2. StorecoveHrFiskEInvoiceAdapter.serialize() had not yet been written

The v2 plan tasked CodeCraftintegrates with SEFexternal outboundsystems adapteracross workseven (Tasksfunctional 1.4domains. andAs 1.5)of as the Phase 1 priority.

1.2 CEO Override — 2026-05-09

During the ADR-023 routing decision session (2026-05-09), CEO Alem Bašić issued an explicit market priority override:

"B i onda dokumentuj Opcija C — ali prvo ide Hrvatska ne srbija" (Translated: "B and then document Option C — but Croatia goes first, not Serbia")

ADR-023 §6 records this as: "CEO override 2026-05-09: market priority is HR → BA → SR"

The Plan v3 (2026-05-11, approvedonly byone CEO)adapter reflectsexists (`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 inADR its §1 executive summary: "Market priority flipped HR-first (CEO override 2026-05-09, ADR-023 §6)."

1.3 Whyformalises. 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 CEOONLY 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 wasval capturedmessage: 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 ADR-023scheduled andmaintenance PlanPLATFORM_INTERNAL_ERROR, v3,// butHTTP neither5xx document containstransient 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 structuredmapping decisionto recordan with`AdapterErrorCode`. alternativesUse analysis,`UNKNOWN` consequences, and binding implementation rules. The override must be ratifiedonly as a first-classcatch-all, architectural decision so that:

  1. New team members understand WHY HR is first (competitive, regulatory, and technical drivers)
  2. RS deprioritization is explicit and does not require repeated CEO re-confirmation
  3. The Storecove MC #8675 activation is recognized as a critical-path CEO action
  4. The BA sequencing rationale (after HR, before RS in terms of complexity) is documented

2. Decision

Market launch priority: HR (Croatia) → BA (Bosnia-Herzegovina) → SR (Serbia)

This sequence is CEO-mandated and applies to:

  • Phase scheduling (Phase 1H before Phase 1S before Phase 1B)
  • Adapter development priority (StorecoveHrFiskEInvoiceAdapter SANDBOX_VERIFIED before SEF)
  • Flyway migration priority (HR Kontni Plan seeded before RS Pravilnik)
  • Frontend MarketContext priority (HR locale, EUR currency, OIB validation before RS/BA)
  • Marketing and GTM (bilko.cloud HR brand before bilko.rs / bilko.company)
  • Securion audit priority (HR-scoped RLS audit before RS/BA)
  • Storecove account activation (MC #8675) is a CEO/John-owned critical-path action

2.1 SR Deprioritization — Explicit Scope

The following RS-focused work items are explicitly DEPRIORITIZED to Phase 1S (post-HR GA):

  • SEF outbound adapter (SefRsEInvoiceAdapter) — Phase 1S.3
  • SEF inbound adapter (supplier invoice webhook) — Phase 1S.4
  • SEF sandbox certification (5 invoice types, real SEF sandbox) — Phase 1S.5
  • PluginRS full implementation (PDV 20/10%, PIB validator, APR CoA, RSD formatters) — Phase 1S.1
  • TaxEngine service with JurisdictionRuleSet interface — Phase 1S.2
  • RS company registry adapter (APR), exchange rate adapter (NBS), ePorezi filing adapter — Phase 1S.6
  • packages/domain-rs TypeScript plugin work — deprioritized (stub only)
  • bilko.rs domain registration — open (MC #100124), not required for HR GA

What stays active for RS: ePorezi RS registration (MC decision signed 2026-04-24, started 2026-04-25). Registration process runs independently of code priority — it takes 6+ months and has no code dependency.

2.2 BA Sequencing — After HR, Before Full SR

BiH (Bosnia-Herzegovina) follows HR in sequence for a specific reason: the BA market has two sub-jurisdictions (BA_FED — Federation, under UIO-FBiH tax authority; BA_RS — Republika Srpska entity, under Poreska Uprava RS entity) that require parallel adapters. This is architecturally more complex than RS (single SEF platform) but the BA market is strategically closer to the HR market in terms of regulatory readiness:

  • BA e-invoice mandate is forthcoming (not yet in force as of 2026-05-13)
  • BA SMBs currently use paper or simple e-invoicing; Bilko can enter with less competitive friction
  • BA demographics and business language overlap with BiH diaspora in Norway (Alem's network)
  • The TaxJurisdiction enum already defines BA_FED and BA_RS (V16 applied)

BA sequencing rule: Phase 1B (BiH generalization) begins after Phase 1S kickoff, not after Phase 1S GA. This allows BA and RS to run in parallel in the later phases.


3. Drivers: Why HR Is First

3.1 Competitive Driver — Minimax HR Is the Threat

The HR competitive map (project_bilko_hr_competitor_intel_2026-05-10.md) identifies Minimax HR (under SEYFOR group)never as the primary SMBmapping competitorfor a known status code. See `StorecoveErrorMapper` (lines 469–516 in Croatia.`StorecoveHrFiskEInvoiceAdapter.kt`) Key facts:

  • SEYFOR (formerly Saop) owns Minimax, Vasco, Opal acrossas the Adriaticreference pattern. ### 2.4 AdapterConfig50K+DB-Level users
  • Feature
  • MinimaxFlag hasEvery modernadapter cloudis UXgated +by FISKan 2.0`AdapterConfig` +row. EU-mandateAn compliance
  • adapter
  • MojaFirma.hrMUST (previouslyNOT thoughtexecute asany network call if its `AdapterConfig.enabled = false`. This allows disabling a threat)broken wasadapter confirmedwithout asredeployment. HR/timesheet```sql only,-- 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 accountingNULL, (live-- probeTaxJurisdiction 2026-05-10:enum paušalvalue: module'HR', ='RS', NN'BA_FED', 55/2024'BA_RS' evidencija,adapter_type NOVARCHAR(32) B2BNOT e-faktura,NULL, NO-- HR-FISK 2.0)
  • Bilko's differentiation: "Fiken simplicity" vs. Minimax breadth + eRačun/FISK 2.0 gap
  • First-mover window: FISK 2.0 mandate is creating a compliance urgency that Bilko can exploit beforeMatches the market7 consolidatescategories: around'EINVOICE', Minimax
  • 'COMPANY_REGISTRY',
--

RS'BANK_STATEMENT', market'EXCHANGE_RATE', has'TAX_FILING', Moj-eRačun-- /'FISCAL_DEVICE', SEF'QES_SIGNING' asenabled anBOOLEAN establishedNOT ecosystem.NULL BilkoDEFAULT enteringFALSE, RSreason afterTEXT, HR-- GAWhy meansdisabled, 2–3e.g. weeks"MC of#8675 RS delay against a market that is already mature — acceptable given the HR first-mover opportunity.

3.2 Regulatory Driver — eRačun 2026 Deadline

Croatia mandated eRačun (electronic invoicing) via HR-FISK 2.0 for B2B transactions. The effective deadline creates an urgency window: businesses that do not have an e-invoice solution are actively searching for one. Bilko entering the HR market during this window maximizes TAM capture without requiring marketing spend — regulatory urgency does the demand generation.

Serbia's SEF mandate is already in force since 2022 — the urgency window has closed. Businesses that needed a SEF-compliant solution have already adopted one. The Croatia window is open now.

3.3 Technical Driverpending — Storecove Serialize Already Works

StorecoveHrFiskEInvoiceAdapter.serialize() is implemented and tested offline. The Peppol path (Option B, CEO-signed 2026-04-24) is confirmed. The only blocked item is submit() and pollStatus(), which require Storecove account activation (MC #8675 — CEO/John action).

In contrast, the RS SEF path requires:

  • A dedicated SEF API key (available but requires sandbox registration)
  • A separate certificate for AS4/Peppol via a Serbian IS posrednik (intermediary)
  • More complex XML format (SEF-specific UBL variant) with no existing serializer

The HR path is 60–70% built. The RS path is 20% built. Given equal team velocity, HR GA is achievable in 4–5 weeks from Phase 1H kickoff; RS GA would take 8–10 additional weeks.

3.4 Banking Partnership Driver — PBZ Alignment

MC #8608 (PBZ Zagreb, Tomislav Premuz) is an active banking partnership discussion framed as fee income + deposit retention for the Intesa group (per PBZ banking dossier 2026-05-10). An HR product demo enables the PBZ partnership conversation with a concrete reference implementation. A RS-first launch does not enableactivated" this conversation.

The 12-week banking sequencing (W4 PBZ go/no-go, W5–6 parallel Erste+Zaba) is timed to HR GA (target 2026-06-08). RS partnership discussions are a Phase 1S+B parallel track.


4. Consequences

4.1-- Required Actionswhen 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 (Binding)

market,

Theadapter_type) following); actions-- areSeed: requiredall byadapters thisstart decisiondisabled andINSERT cannotINTO beadapter_config deferred(market, toadapter_type, post-HRenabled, GA:

reason,
    updated_by)
  • VALUES

    ('HR', 'EINVOICE', false, 'MC #8675 — Storecove account activationpending', 'MC-100362'), (CEO/John'HR', action):'COMPANY_REGISTRY', Mustfalse, be'Not completedimplemented by 2026-05-25Phase to1S', maintain'MC-100362'), HR('HR', GA'BANK_STATEMENT', targetfalse, of'Tok 2026-06-08.AISP Thisintegration ispending', the'MC-100362'), only('HR', external'EXCHANGE_RATE', dependencyfalse, with'ECB variablefeed leadnot time.configured', Storecove'MC-100362'), account('HR', provides'TAX_FILING', STORECOVE_API_KEYfalse, and'ePorezna STORECOVE_LEGAL_ENTITY_IDintegration enablingPhase submit(2 scope', 'MC-100362'), and('HR', pollStatus('FISCAL_DEVICE', false, 'Fiskalizacija cert not configured', 'MC-100362'), in('HR', StorecoveHrFiskEInvoiceAdapter.

    'QES_SIGNING',
  • false,
  • 'FINA

    QES packages/domain-rsPhase TypeScript2 deprioritization:scope', The'MC-100362'), packages/domain-rs('RS', package'EINVOICE', existsfalse, on'SEF disk as a TypeScript stub. It is NOT wired to the Kotlin runtime. No further work on this package untiladapter Phase 1S kickoff.scope', CodeCraft'MC-100362'), must('BA_FED', not'EINVOICE', routefalse, sprint'CPF capacityplatform 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 domain-rsALL whileadapter Phasecategories. 1H``` isSTUB in──────────────────► progress.

    SANDBOX_VERIFIED
  • ──────────────────►
  • PRODUCTION

    ``` SEF tasks parked: Plan v3 Tasks 1.4**STUB** (SEFinitial outbound)state for all adapters): - Compiles and 1.5registers (SEFsuccessfully sandbox- cert)All arenetwork PARKED.methods Theythrow are`AdapterException(code=NOT_IMPLEMENTED)` not- in the sprint backlog. Any SEF-related MC task opened while Phase 1H is active must be tagged Phase 1S and deferred.

  •  BA-FED`serialize()` / BA-RS Kotlin split timing: TaxJurisdiction already has BA_FED and BA_RS (V16 applied). PluginBAFED and PluginBARS are stub implementations in Phase 1H.3. Full BA implementation waits for Phase 1B kickoff (post-HR GA + Phase 1S kickoff).

4.2 Critical Path: Storecove MC #8675

MC #8675 is the single external dependency that can delay HR GA. The activation sequence:

  1. CEO/John activates Storecove account → receives STORECOVE_API_KEY + STORECOVE_LEGAL_ENTITY_ID
  2. Storecove provisions Peppol participant ID for Bilko's legal entity (typically 1–3 business days)
  3. CodeCraft wires submit() + pollStatus() in StorecoveHrFiskEInvoiceAdapter (skeleton already written — activation is uncomment + wire, not a new implementation)
  4. Proveo runs 5 sandbox invoice types: B2B outbound, B2G outbound, credit note, cancelled invoice, inbound received — all with real Storecove submission IDs
  5. lifecycleState advances from STUB to SANDBOX_VERIFIED
  6. AdapterConfig row: (HR, EINVOICE, enabled=true) set in stage DB

If MC #8675 is not activated by 2026-05-25, Phase 1H.6 slips. The rest of Phase 1H (DI wiring, CountryPlugin interface, PluginHR, Flyway V16) can complete without MC #8675. A demo with submit() in STUB state is possible — it shows HR VAT rates, EUR currency, OIB validation, and e-invoice serialization. GA (first paying HR customer) requires Storecove activation.

Hard deadline: CEO activates MC #8675 no later than 2026-05-25.

4.3 Impact on domain-rs TypeScript Package

The packages/domain-rs TypeScript package exists on disk and compiles to dist/. It is NOT loaded by the Kotlin runtime — it is a TypeScript-read-only scaffold.

This decision means packages/domain-rs receives no active development during Phase 1H. The packageoperations may accumulate technical debt if Kotlin PluginRS in Phase 1S diverges from the TypeScript stub patterns. This is accepted:

  • The TypeScript packages are frontend/tooling artifacts, not the source of RS business logic
  • RS business logic lives in Kotlin PluginRS (Phase 1S.1)
  • packages/domain-rs will be updated in Phase 1S to align with PluginRS implementation

4.4 IS Posrednik Decision (MC #100273) — HR First

The IS posrednik selection (build vs. Mer/Visma AS4 vs. FINA fallback) is a HR-specific decision for the FISK 2.0 AS4 submission path. This decision (MC #100273) is on the HR critical path and must be resolved before Phase 1H.6 (Storecove submit() wiring).

The Storecove Peppol path (Option B, CEO-signed 2026-04-24) uses Storecove as the IS posrednik — it is not a direct FINA connection. This means:

  • MC #100273 (IS posrednik decision) is answered by MC #8675 (Storecove = IS posrednik) for the HR FISK 2.0 path
  • MC #100273 remains open if FINA fallback is considered for non-Storecove scenarios
  • For Phase 1H: Storecove IS the IS posrednik. MC #100273 can be closed as "Resolved by CEO decision 2026-04-24 (Storecove Peppol Option B)"

4.5 Banking Partnership Parallel Track

PBZ (MC #8608), Erste, and Zaba banking discussions proceed in parallel with Phase 1H engineering. The banking partnership is not a code dependency — it is a commercial track that requires an HR product demo (deliverable of Phase 1H) to advance to pilot stage.

The 12-week banking sequencing timeline:

  • W4 (2026-06-01): PBZ go/no-go decision
  • W5–6 (2026-06-08): parallel Erste + Zaba outreach (mandatory per competitive intel memo)
  • W8 (2026-06-22): pilot agreement
  • W12 (2026-07-20): LOI or pivot

This timeline is compatible with HR GA target of 2026-06-08. The PBZ go/no-go aligns with HR GA week.


5. Alternatives Considered

Option A: SR-First (v2 Plan)

Rationale at time of v2: SEF mandate already in force; SEF sandbox certification was the nearest technical milestone; RS market was assumed to be most revenue-ready.

Why rejected: CEO override 2026-05-09. On reflection, the SR-first rationale was based on regulatory urgency (SEF in force) but missed the competitive urgency in HR (FISK 2.0 compliance window + Minimax as the primary threat). Additionally, the Storecove serialize() completion made HR technically closer to done than originally assessed. The SEF adapter has no existing serializer — it is further from done than HR, not closer.

Option B: Simultaneous HR + SR Launch

Develop PluginHR and PluginRS in parallel, targeting simultaneous GA.

Why rejected: Team capacity and risk. Running two market adapters in parallel while stabilizing the RLS migration (Phase 2A) and the CountryPlugin DI wiring (Phase 1H) creates four concurrent workstreams for a small team. The probability of schedule slip on all four increases nonlinearly. HR-first serializes the risk: prove the CountryPlugin architecture works for HR, then apply it to RS with confidence.

Option C: BA-First

BiH has strategic importance (CEO personal network, diaspora market in Norway). Target BA before HR.

Why rejected: BiH e-invoice mandate is not yet in force. The BA market has no regulatory urgency comparable to HR-FISK 2.0. Additionally, BA requires two sub-jurisdiction implementations (BA_FED and BA_RS) — more architectural complexity than HR's single jurisdiction. Starting with the more complex market before the architecture is proven increases risk. BA follows HR when the CountryPlugin DI pattern is validated.


6. Implementation Notes

6.1 Sprint Backlog Rules (Enforced)

While Phase 1H is active (Weeks 1–3 from 2026-05-11), the following rules apply to the CodeCraft sprint backlog:

  1. No MC tasks with label market:RS or market:BA may enter the active sprint
  2. Any domain-rs TypeScript work is automatically deferred to Phase 1S
  3. SEF-related tasks are parked (not closed — they will be activated at Phase 1S kickoff)
  4. PluginRS and PluginBAFED/PluginBARS stubs (Phase 1H.3) are the ONLY RS/BA items in Phase 1H — they are stubs, not implementations

6.2 Phase 1S Kickoff Gate

Phase 1S (Serbia generalization) kicks off when the following are true:

  •  Phase 1H complete: PluginHR live in production, Storecove SANDBOX_VERIFIED
  •  Phase 5H.1 passed: Proveo HR smoke test with real stage DB
  •  HR GA target date (2026-06-08) reached or within 1 week
  •  CEO explicit kick-off confirmation (not assumed from Phase 1H completion)

The Phase 1S kickoff is a separate CEO decision — completing Phase 1H does not auto-trigger Phase 1S. Market conditions may change between 2026-05-11 and 2026-06-08 (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 BAunit regulatoryfor announcementenvironment couldpromotion. reprioritize``` 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 1S)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"`).

6.3The 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 Kickoff| Gate

CPF

PhaseBA-FED 1B+ (BosniaUINO generalization)BA-RS kicksstubs off| `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 parallel< 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 Phasea 1STTL (notof after):

5
    minutes.
  • Use Phasecached 1S.1state (PluginRSif fullDB implementation)is mergedunreachable. to- main
  • **Metrics
  • cardinality.** PhaseHigh 1H`org_id` stable (no regressionscardinality in HRmetrics marketlabels forwould 2cause weeksPrometheus post-GA)
  • memory
  • issues. CEOThe decisionobservability 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 BAdisk) PSD2| /`apps/api/src/main/kotlin/no/alai/bilko/einvoice/EInvoiceTypes.kt` Tok| scope22–26 | | `StorecoveErrorMapper` (Openerror CEOmapping Decisionreference) #3,| `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 §10)
  • 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 e-invoiceadapter regulatorystubs update| assessedRole (if| mandateSign announced,| accelerate)
  • Date
|

Running| Phase-------------------------------- 1B| and------------------------------------- Phase| 1S---------- in| parallel| is feasible because PluginBAFED and PluginBARS do not share code with PluginRS. Two teams (or two sprint lanes within CodeCraft) can develop them concurrently after the CountryPlugin DI pattern is proven via HR.


7. Approval

Status: Accepted (ratification of CEO override 2026-05-09) Architecture review: Required from Petter Graff before Phase 1H kickoff

|Signed|2026-05-11for
RoleSignDate
Architecture Lead (Petter Graff) Pending
| | CEO (Alem Bašić) | OVERRIDENot ALREADYrequired ISSUED Overrideregistry recordedpattern 2026-05-09ADR (ADR-023 §6)2026-05-09
CEO (Alem Bašić)|MC| #8675--- critical-path## acknowledgementRequired: CEO must acknowledge Storecove deadline 2026-05-25

8.7. Document History

|Date|Author|||----------|11|#100362)|
Date Author Change
------------ | ------------------------------------------------- | | 2026-05-13 | Petter Graff (CodeCraft) Initial — MC #100586 Phase 0' HR-firstADR marketconsolidation priority(MC ratification