Add DATEV accounting adapter boundary
This commit is contained in:
parent
fcec177ded
commit
90b7e97533
8 changed files with 583 additions and 13 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
87
docs/accounting-adapter-boundary.md
Normal file
87
docs/accounting-adapter-boundary.md
Normal file
|
|
@ -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.
|
||||
19
src/fin_hub/accounting/__init__.py
Normal file
19
src/fin_hub/accounting/__init__.py
Normal file
|
|
@ -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",
|
||||
]
|
||||
255
src/fin_hub/accounting/adapters.py
Normal file
255
src/fin_hub/accounting/adapters.py
Normal file
|
|
@ -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,
|
||||
)
|
||||
103
tests/test_accounting_adapters.py
Normal file
103
tests/test_accounting_adapters.py
Normal file
|
|
@ -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"
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
94
workplans/FIN-WP-0005-datev-accounting-adapter-operations.md
Normal file
94
workplans/FIN-WP-0005-datev-accounting-adapter-operations.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue