diff --git a/.custodian-brief.md b/.custodian-brief.md index 9c294ee..2657ad2 100644 --- a/.custodian-brief.md +++ b/.custodian-brief.md @@ -2,7 +2,7 @@ # Custodian Brief — fin-hub **Domain:** financials -**Last synced:** 2026-08-11 12:39 UTC +**Last synced:** 2026-08-11 12:54 UTC **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* ## Active Workstreams @@ -15,12 +15,6 @@ Progress: 6/9 done | workplan_id: `67b6de6c-4820-4478-9789-f50260204c27` - · T04 — Expose budgets, commitments, and viability constraints `3620fc2a` - · T06 — Generalize and operate the contract `a1309d51` -### Client attribution and billing basis -Progress: 5/6 done | workplan_id: `ebc1d2de-ae11-4cde-b860-047922fc74b9` - -**Open tasks:** -- ! Select the external invoicing system `d30b606f` - --- ## MCP Orientation (when available) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index d34fd38..b65a2c9 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -10,9 +10,10 @@ | --- | --- | --- | --- | --- | | workplan | FIN-WP-0000 | finished | — | workplans/FIN-WP-0000-repo-integration.md | | workplan | FIN-WP-0001 | finished | — | workplans/FIN-WP-0001-runway-operations-lane.md | -| workplan | FIN-WP-0002 | active | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md | +| workplan | FIN-WP-0002 | finished | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md | | workplan | FIN-WP-0003 | proposed | — | workplans/FIN-WP-0003-fabric-authority-boundary.md | | workplan | FIN-WP-0004 | active | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md | +| workplan | FIN-WP-0005 | proposed | — | workplans/FIN-WP-0005-datev-accounting-adapter-operations.md | | task | FIN-WP-0000-T01 | done | — | workplans/FIN-WP-0000-repo-integration.md | | task | FIN-WP-0000-T02 | done | — | workplans/FIN-WP-0000-repo-integration.md | | task | FIN-WP-0000-T03 | done | — | workplans/FIN-WP-0000-repo-integration.md | @@ -28,7 +29,7 @@ | task | FIN-WP-0002-T02 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md | | task | FIN-WP-0002-T03 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md | | task | FIN-WP-0002-T04 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md | -| task | FIN-WP-0002-T05 | wait | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md | +| task | FIN-WP-0002-T05 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md | | task | FIN-WP-0003-T01 | todo | — | workplans/FIN-WP-0003-fabric-authority-boundary.md | | task | FIN-WP-0003-T02 | todo | — | workplans/FIN-WP-0003-fabric-authority-boundary.md | | task | FIN-WP-0003-T03 | todo | — | workplans/FIN-WP-0003-fabric-authority-boundary.md | @@ -41,3 +42,7 @@ | task | FIN-WP-0004-T07 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md | | task | FIN-WP-0004-T08 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md | | task | FIN-WP-0004-T09 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md | +| task | FIN-WP-0005-T01 | todo | — | workplans/FIN-WP-0005-datev-accounting-adapter-operations.md | +| task | FIN-WP-0005-T02 | wait | — | workplans/FIN-WP-0005-datev-accounting-adapter-operations.md | +| task | FIN-WP-0005-T03 | wait | — | workplans/FIN-WP-0005-datev-accounting-adapter-operations.md | +| task | FIN-WP-0005-T04 | wait | — | workplans/FIN-WP-0005-datev-accounting-adapter-operations.md | diff --git a/docs/accounting-adapter-boundary.md b/docs/accounting-adapter-boundary.md new file mode 100644 index 0000000..fff04eb --- /dev/null +++ b/docs/accounting-adapter-boundary.md @@ -0,0 +1,87 @@ +# External accounting adapter boundary + +Decision date: 2026-08-11 + +## Decision + +DATEV Unternehmen online (DUO) is the default and authoritative external +GoBD bookkeeping and document-retention system. fin-hub remains the source of +a reporting-only billing basis; it is not a bookkeeping system and does not +issue invoices or handle payments. + +Qonto is the first complementary candidate when a needed function is not +provided conveniently by DUO. In particular, Qonto may provide the customer +invoice draft/delivery workflow and the managed transfer of bank data, +documents, and structured invoice data to DUO. Selecting Qonto for one of +these capabilities does not make it the bookkeeping authority. + +Other GoBD-capable systems remain possible through the same adapter contract. +No provider is selected implicitly because another provider lacks a feature. +The caller must request a capability and explicitly select a non-default +adapter. + +## Data and authority flow + +```text +fin-hub billing-basis v0.1 + | + | side-effect-free, version-pinned adapter + v +provider draft / exception report + | + | authorized external workflow and legal/tax completion + v +DATEV Unternehmen online (bookkeeping and document authority) +``` + +The preferred operational route is Qonto's managed DATEV integration when the +invoice workflow already occurs in Qonto and it preserves all required fields. +A direct DATEV integration is the fallback for non-Qonto sources or missing +Qonto capabilities. Provider activation, credentials, tax configuration, +customer master data, invoice numbering, delivery, and retention validation +are deliberately outside the current adapter. + +## Contract + +`fin_hub.accounting` defines: + +- a manifest with stable adapter ID, semantic adapter version, provider role, + exact input schema version, and declared capabilities; +- a side-effect-free `prepare` operation producing an incomplete provider + draft with the source billing-basis ID as its idempotency reference; +- an explicit schema-migration graph. An adapter rejects an unsupported source + schema, including a new major version, unless a reviewed migration is + registered; +- capability checks that fail closed. The registry never silently falls back + from DATEV to Qonto or another provider. + +Provider payloads retain the billing-basis reference, client/engagement +reference, service period, currency, amount, price reference, and source +evidence IDs where supported. They explicitly mark legal and tax completion +as required and contain no invoice number, issue date, or payment state. + +## Change and migration controls + +Before a provider or adapter upgrade is promoted: + +1. Pin and review the new provider API/export and adapter versions. +2. Add an explicit billing-basis schema migration if the exact input version + changes; never reinterpret fields in place. +3. Run contract and golden-payload tests, then dual-run old and new adapters + against the same billing-basis IDs. +4. Reconcile record counts, amounts, currency, exception sets, and evidence + references in a non-production tenant. +5. Obtain bookkeeping/tax-owner acceptance before cutover, retain the prior + adapter for rollback, and record the cutover decision. + +## Custody and retention + +- fin-hub retains reporting inputs, derivations, corrections, allocations, and + export provenance according to its financial-evidence policy. +- DATEV/Steuerbüro owns bookkeeping records, document retention, tax treatment, + legal invoice completion, and correction/cancellation procedures. +- Qonto owns bank records and any Qonto-created invoice/document workflow; its + DATEV transfer must be monitored and reconciled rather than treated as proof + of successful bookkeeping. +- Secrets and provider credentials are never stored in billing-basis artifacts, + workplans, logs, or adapter configuration committed to Git. diff --git a/src/fin_hub/accounting/__init__.py b/src/fin_hub/accounting/__init__.py new file mode 100644 index 0000000..9c0dde4 --- /dev/null +++ b/src/fin_hub/accounting/__init__.py @@ -0,0 +1,19 @@ +"""Provider-neutral handoff of reporting data to external accounting systems.""" + +from fin_hub.accounting.adapters import ( + AccountingCapability, + AdapterRegistry, + DatevDuoAdapter, + QontoComplementAdapter, + SchemaMigrations, + default_registry, +) + +__all__ = [ + "AccountingCapability", + "AdapterRegistry", + "DatevDuoAdapter", + "QontoComplementAdapter", + "SchemaMigrations", + "default_registry", +] diff --git a/src/fin_hub/accounting/adapters.py b/src/fin_hub/accounting/adapters.py new file mode 100644 index 0000000..bc7ec66 --- /dev/null +++ b/src/fin_hub/accounting/adapters.py @@ -0,0 +1,255 @@ +"""Versioned, side-effect-free adapters for external accounting handoff. + +Adapters prepare provider-specific drafts. They deliberately do not authenticate, +transmit, issue invoices, assign invoice numbers, or perform bookkeeping. +""" + +from __future__ import annotations + +from collections import deque +from dataclasses import asdict, dataclass +from enum import StrEnum +from typing import Callable, Mapping, Protocol + +from fin_hub.services.billing import BillingBasisExport + + +class AccountingCapability(StrEnum): + BILLING_BASIS_HANDOFF = "billing_basis_handoff" + INVOICE_DRAFT_WORKFLOW = "invoice_draft_workflow" + STRUCTURED_INVOICE_TRANSFER = "structured_invoice_transfer" + DOCUMENT_ARCHIVE = "document_archive" + BANK_DATA_SYNC = "bank_data_sync" + + +@dataclass(frozen=True) +class AdapterManifest: + adapter_id: str + provider: str + role: str + adapter_version: str + input_schema_version: str + capabilities: frozenset[AccountingCapability] + + +@dataclass(frozen=True) +class PreparedTransfer: + schema_version: str + artifact_type: str + adapter_id: str + adapter_version: str + provider: str + provider_role: str + source_schema_version: str + records: tuple[Mapping[str, object], ...] + exceptions: tuple[Mapping[str, object], ...] + disclaimer: str + + def as_dict(self) -> dict: + return asdict(self) + + +class AccountingAdapter(Protocol): + manifest: AdapterManifest + + def prepare(self, export: BillingBasisExport) -> PreparedTransfer: ... + + +Migration = Callable[[BillingBasisExport], BillingBasisExport] + + +class SchemaMigrations: + """Explicit migration graph; no implicit major-version compatibility.""" + + def __init__(self) -> None: + self._steps: dict[tuple[str, str], Migration] = {} + + def register(self, source: str, target: str, migration: Migration) -> None: + if source == target: + raise ValueError("a schema migration must change the version") + key = (source, target) + if key in self._steps: + raise ValueError(f"migration already registered: {source} -> {target}") + self._steps[key] = migration + + def migrate(self, export: BillingBasisExport, target: str) -> BillingBasisExport: + if export.schema_version == target: + return export + + queue = deque([(export.schema_version, ())]) + visited = {export.schema_version} + path: tuple[tuple[str, str], ...] | None = None + while queue: + version, steps = queue.popleft() + for source, destination in sorted(self._steps): + if source != version or destination in visited: + continue + candidate = (*steps, (source, destination)) + if destination == target: + path = candidate + queue.clear() + break + visited.add(destination) + queue.append((destination, candidate)) + if path is None: + raise ValueError( + "no explicit billing-basis schema migration from " + f"{export.schema_version} to {target}" + ) + + migrated = export + for source, destination in path: + migrated = self._steps[(source, destination)](migrated) + if migrated.schema_version != destination: + raise ValueError( + f"migration {source} -> {destination} produced " + f"schema {migrated.schema_version}" + ) + return migrated + + +def _prepared( + manifest: AdapterManifest, + export: BillingBasisExport, + records: tuple[Mapping[str, object], ...], +) -> PreparedTransfer: + return PreparedTransfer( + schema_version="0.1", + artifact_type="accounting_handoff_draft", + adapter_id=manifest.adapter_id, + adapter_version=manifest.adapter_version, + provider=manifest.provider, + provider_role=manifest.role, + source_schema_version=export.schema_version, + records=records, + exceptions=tuple(exception.as_dict() for exception in export.exceptions), + disclaimer=( + f"{export.disclaimer} Provider draft only; requires authorized completion " + "of customer, tax, invoice-number, issue-date, delivery, and retention data." + ), + ) + + +class DatevDuoAdapter: + """Default handoff boundary for DATEV Unternehmen online.""" + + manifest = AdapterManifest( + adapter_id="datev-duo-v1", + provider="DATEV Unternehmen online", + role="authoritative_bookkeeping_and_document_archive", + adapter_version="1.0.0", + input_schema_version="0.1", + capabilities=frozenset( + { + AccountingCapability.BILLING_BASIS_HANDOFF, + AccountingCapability.STRUCTURED_INVOICE_TRANSFER, + AccountingCapability.DOCUMENT_ARCHIVE, + } + ), + ) + + def prepare(self, export: BillingBasisExport) -> PreparedTransfer: + records = tuple( + { + "external_reference": record.billing_basis_id, + "customer_reference": record.client_id, + "engagement_reference": record.cost_attribution_key, + "performance_period": record.period_month, + "currency": record.currency, + "net_billing_basis": record.revenue, + "price_reference": record.price_id, + "source_evidence_ids": ( + *record.financial_fact_ids, + *record.allocation_ids, + ), + "requires_legal_and_tax_completion": True, + } + for record in export.records + ) + return _prepared(self.manifest, export, records) + + +class QontoComplementAdapter: + """Optional Qonto invoice-workflow draft; DATEV remains authoritative.""" + + manifest = AdapterManifest( + adapter_id="qonto-invoice-draft-v1", + provider="Qonto", + role="complementary_invoice_workflow_and_datev_transport", + adapter_version="1.0.0", + input_schema_version="0.1", + capabilities=frozenset( + { + AccountingCapability.BILLING_BASIS_HANDOFF, + AccountingCapability.INVOICE_DRAFT_WORKFLOW, + AccountingCapability.STRUCTURED_INVOICE_TRANSFER, + AccountingCapability.BANK_DATA_SYNC, + } + ), + ) + + def prepare(self, export: BillingBasisExport) -> PreparedTransfer: + records = tuple( + { + "external_reference": record.billing_basis_id, + "customer_reference": record.client_id, + "description": record.cost_attribution_key, + "service_period": record.period_month, + "currency": record.currency, + "net_draft_amount": record.revenue, + "requires_legal_and_tax_completion": True, + "bookkeeping_destination": "DATEV Unternehmen online", + } + for record in export.records + ) + return _prepared(self.manifest, export, records) + + +class AdapterRegistry: + def __init__( + self, + adapters: tuple[AccountingAdapter, ...], + *, + default_adapter_id: str, + migrations: SchemaMigrations | None = None, + ) -> None: + self._adapters = {adapter.manifest.adapter_id: adapter for adapter in adapters} + if len(self._adapters) != len(adapters): + raise ValueError("adapter IDs must be unique") + if default_adapter_id not in self._adapters: + raise ValueError(f"unknown default adapter: {default_adapter_id}") + self.default_adapter_id = default_adapter_id + self.migrations = migrations or SchemaMigrations() + + def manifests(self) -> tuple[AdapterManifest, ...]: + return tuple( + adapter.manifest for _, adapter in sorted(self._adapters.items()) + ) + + def prepare( + self, + export: BillingBasisExport, + *, + adapter_id: str | None = None, + required_capabilities: frozenset[AccountingCapability] = frozenset(), + ) -> PreparedTransfer: + selected_id = adapter_id or self.default_adapter_id + try: + adapter = self._adapters[selected_id] + except KeyError as error: + raise ValueError(f"unknown accounting adapter: {selected_id}") from error + missing = required_capabilities - adapter.manifest.capabilities + if missing: + names = ", ".join(sorted(missing)) + raise ValueError(f"adapter {selected_id} lacks capabilities: {names}") + compatible = self.migrations.migrate( + export, adapter.manifest.input_schema_version + ) + return adapter.prepare(compatible) + + +def default_registry() -> AdapterRegistry: + return AdapterRegistry( + (DatevDuoAdapter(), QontoComplementAdapter()), + default_adapter_id=DatevDuoAdapter.manifest.adapter_id, + ) diff --git a/tests/test_accounting_adapters.py b/tests/test_accounting_adapters.py new file mode 100644 index 0000000..86dde93 --- /dev/null +++ b/tests/test_accounting_adapters.py @@ -0,0 +1,103 @@ +from dataclasses import replace +from decimal import Decimal + +import pytest + +from fin_hub.accounting import ( + AccountingCapability, + DatevDuoAdapter, + SchemaMigrations, + default_registry, +) +from fin_hub.services.billing import BillingBasisExport, BillingBasisRecord + + +def _export(schema_version: str = "0.1") -> BillingBasisExport: + record = BillingBasisRecord( + billing_basis_id="billing-basis:stable", + cost_attribution_key="client:acme|app:portal|instance:prod-01", + client_id="acme", + application_id="portal", + app_instance_id="prod-01", + period_month="2026-07", + currency="EUR", + price_id="price:agreement-v1", + price_revision_of=None, + price_source="agreement-v1", + revenue=Decimal("200.00"), + direct_cost=Decimal("20.00"), + allocated_cost=Decimal("80.00"), + total_cost=Decimal("100.00"), + margin=Decimal("100.00"), + financial_fact_ids=("fact:1",), + financial_fact_corrections=(), + allocation_ids=("allocation:1",), + allocation_revisions=(), + provenance=("agreement-v1",), + ) + return BillingBasisExport( + schema_version=schema_version, + artifact_type="billing_basis_report", + records=(record,), + exceptions=(), + disclaimer="Reporting basis only; not an invoice.", + ) + + +def test_datev_duo_is_default_and_prepares_only_an_incomplete_draft(): + transfer = default_registry().prepare(_export()) + + assert transfer.adapter_id == "datev-duo-v1" + assert transfer.provider == "DATEV Unternehmen online" + assert transfer.records[0]["external_reference"] == "billing-basis:stable" + assert transfer.records[0]["net_billing_basis"] == Decimal("200.00") + assert transfer.records[0]["requires_legal_and_tax_completion"] is True + assert "Provider draft only" in transfer.disclaimer + assert not any( + key in transfer.records[0] + for key in ("invoice_number", "issue_date", "payment_status") + ) + + +def test_qonto_requires_explicit_selection_and_keeps_datev_as_destination(): + transfer = default_registry().prepare( + _export(), + adapter_id="qonto-invoice-draft-v1", + required_capabilities=frozenset( + {AccountingCapability.INVOICE_DRAFT_WORKFLOW} + ), + ) + + assert transfer.provider == "Qonto" + assert transfer.records[0]["bookkeeping_destination"] == ( + "DATEV Unternehmen online" + ) + + +def test_capability_mismatch_never_silently_falls_back_to_qonto(): + with pytest.raises(ValueError, match="lacks capabilities: invoice_draft_workflow"): + default_registry().prepare( + _export(), + required_capabilities=frozenset( + {AccountingCapability.INVOICE_DRAFT_WORKFLOW} + ), + ) + + +def test_breaking_schema_requires_an_explicit_migration(): + with pytest.raises(ValueError, match="no explicit billing-basis schema migration"): + default_registry().prepare(_export("1.0")) + + +def test_registered_schema_migration_must_produce_declared_version(): + migrations = SchemaMigrations() + migrations.register("1.0", "0.1", lambda export: replace(export, schema_version="0.1")) + from fin_hub.accounting.adapters import AdapterRegistry + + registry = AdapterRegistry( + (DatevDuoAdapter(),), + default_adapter_id="datev-duo-v1", + migrations=migrations, + ) + + assert registry.prepare(_export("1.0")).source_schema_version == "0.1" diff --git a/workplans/FIN-WP-0002-client-attribution-and-billing-basis.md b/workplans/FIN-WP-0002-client-attribution-and-billing-basis.md index 9e8ae66..3e99d15 100644 --- a/workplans/FIN-WP-0002-client-attribution-and-billing-basis.md +++ b/workplans/FIN-WP-0002-client-attribution-and-billing-basis.md @@ -4,11 +4,11 @@ type: workplan title: "Client attribution and billing basis" domain: financials repo: fin-hub -status: active +status: finished owner: codex topic_slug: financials created: "2026-08-10" -updated: "2026-08-10" +updated: "2026-08-11" related: - FIN-WP-0001 - FIN-WP-0004 @@ -167,7 +167,7 @@ only disclaimer and has no invoice, bookkeeping, or payment behavior. ```task id: FIN-WP-0002-T05 -status: wait +status: done priority: high state_hub_task_id: "d30b606f-43c1-4610-875d-c1fe578c412c" ``` @@ -178,6 +178,19 @@ retention responsibilities. Candidate products from the gap assessment are lexoffice and sevDesk, but selection is an operator/business decision and is not required to settle T00–T03. +Completed 2026-08-11: the operator selected DATEV Unternehmen online as the +authoritative external GoBD bookkeeping and document-retention system. Qonto +is the preferred complementary invoice workflow and managed DATEV transport +when DUO lacks or does not conveniently expose a required function. Added a +provider-neutral, capability-declared adapter contract with DATEV as the +default, explicit Qonto selection, exact input-schema compatibility, and +fail-closed migrations. The adapters create incomplete, side-effect-free +handoff drafts only; legal/tax completion, issuance, delivery, payments, and +bookkeeping remain authorized external actions. The integration, custody, +retention, and upgrade controls are recorded in +`docs/accounting-adapter-boundary.md`. Live provider activation is tracked by +`FIN-WP-0005`. + ## Acceptance - [x] Repository scope explicitly allows the reporting and export boundary. @@ -188,4 +201,4 @@ not required to settle T00–T03. - [x] Shared-infrastructure allocations reconcile to authoritative booked costs with assumptions and unattributed residuals visible. - [x] The billing-basis export is idempotent and contains no payment execution. -- [ ] A human records the external invoicing/bookkeeping system decision. +- [x] A human records the external invoicing/bookkeeping system decision. diff --git a/workplans/FIN-WP-0005-datev-accounting-adapter-operations.md b/workplans/FIN-WP-0005-datev-accounting-adapter-operations.md new file mode 100644 index 0000000..6942a00 --- /dev/null +++ b/workplans/FIN-WP-0005-datev-accounting-adapter-operations.md @@ -0,0 +1,94 @@ +--- +id: FIN-WP-0005 +type: workplan +title: "DATEV accounting adapter operations" +domain: financials +repo: fin-hub +status: proposed +owner: codex +topic_slug: financials +created: "2026-08-11" +updated: "2026-08-11" +related: + - FIN-WP-0002 +state_hub_workstream_id: "1c081ee5-cf39-4868-ac17-48aada6e102b" +--- + +# FIN-WP-0005 — DATEV accounting adapter operations + +## Goal + +Operationalize the provider-neutral accounting handoff selected in +`FIN-WP-0002`: DATEV Unternehmen online is the bookkeeping and document +authority, with Qonto as the preferred complementary invoice workflow and +managed DATEV transport where its capabilities are sufficient. + +This workplan does not authorize fin-hub to issue invoices, assign legal +invoice numbers, execute payments, or perform bookkeeping. + +## Confirm operational capabilities and ownership + +```task +id: FIN-WP-0005-T01 +status: todo +priority: high +state_hub_task_id: "55cb6ec1-2bd6-4f33-8a4b-7791193c4c09" +``` + +With the operator and Steuerbüro, confirm the enabled Qonto plan, DATEV tenant +and services, customer/tax master-data owner, invoice workflow, document +retention policy, correction procedure, and required structured fields. +Record which capabilities use Qonto's managed DATEV services and which require +a direct DATEV integration. Route credentials only after this selection. + +## Implement the selected transport + +```task +id: FIN-WP-0005-T02 +status: wait +priority: high +state_hub_task_id: "e2400fb4-ff8e-4336-9702-60d0c2e254b3" +``` + +After T01, implement the authorized transport behind the existing adapter +contract. Prefer the managed Qonto-to-DATEV route when it preserves the full +required record; otherwise use the approved DATEV API/export surface. Keep +authentication and network side effects outside pure payload preparation. + +## Validate legal and bookkeeping completion + +```task +id: FIN-WP-0005-T03 +status: wait +priority: high +state_hub_task_id: "16865fc6-5e6e-46cd-9ed1-109fa0eeb8e7" +``` + +In a non-production tenant, demonstrate customer and tax completion, legal +invoice numbering and issuance by the authorized external system, document +arrival in DUO, bookkeeping visibility, exception handling, and correction or +cancellation flow. Do not treat transport acceptance as bookkeeping proof. + +## Certify upgrades and rollback + +```task +id: FIN-WP-0005-T04 +status: wait +priority: medium +state_hub_task_id: "e372997a-a5a2-4c02-a6f4-50ea34add067" +``` + +Golden-test and dual-run the selected adapter against the same stable billing +basis IDs. Reconcile counts, amounts, currencies, exceptions, and evidence +references; record provider and adapter versions, cutover approval, monitoring, +and rollback procedure. + +## Acceptance + +- [ ] The Steuerbüro and operator have accepted the responsibility matrix and + selected transport. +- [ ] Provider credentials are routed through their proper custodian and are + absent from Git, artifacts, and logs. +- [ ] A non-production end-to-end transfer is reconciled in DATEV. +- [ ] Issuance, payments, and bookkeeping remain outside fin-hub. +- [ ] Upgrade, breaking-migration, monitoring, and rollback controls are tested.