wiki/c-work/4-done/2026-04-trustee-account-balances-import.md
ValueOn AG 24190f532a fixes
2026-04-26 08:36:33 +02:00

238 lines
16 KiB
Markdown

<!-- status: done -->
<!-- started: 2026-04-25 -->
<!-- completed: 2026-04-25 -->
<!-- component: gateway -->
# Trustee: Echte Schlusssalden aus Buchhaltungssystem importieren
## Beschreibung und Kontext
**Bug-Report Kunde (PROD-Mandant `BuHa SoHa`, Connector RMA):** Fragt man Nyla
nach dem Banksaldo per 31.12.2025 (Konto 1020), antwortet sie mit
`CHF 79'939.86`. Der echte Schlusssaldo ist `CHF 48'507.41`. Auch alle daraus
abgeleiteten Auswertungen (Geldflussrechnung, Umsatz-Diagramme, Plausichecks)
sind dadurch falsch.
**Root cause:** Die Tabelle `TrusteeDataAccountBalance` wird nicht aus dem
Buchhaltungssystem importiert, sondern in `accountingDataSync._persistBalances`
lokal aus den Journalzeilen aggregiert -- mit drei eigenstaendigen Fehlern:
1. `closingBalance = debit - credit` **nur** aus den Buchungen der jeweiligen
Periode (Monat oder Jahres-Bucket). Das ist die Periodenbewegung, kein
Schlusssaldo. Schon innerhalb desselben Jahres summieren sich die Vormonate
nicht auf den Stichtag.
2. `openingBalance` ist hart auf `0.0` kodiert -- der Saldovortrag aus den
Vorjahren / vor dem Importfenster fehlt komplett.
3. Auch der Jahres-Bucket `(accNo, year, 0)` laeuft durch dieselbe Formel,
d.h. der "Jahres-Schlusssaldo" ist eigentlich nur die Jahres-Bewegung.
**Architekturloch:** `BaseAccountingConnector` kennt schlicht keine
`getAccountBalances`-Methode. Der Defekt trifft alle drei aktuell registrierten
Connectoren (RMA / Bexio / Abacus).
**Warum jetzt:** Das Trustee-Feature steht im aktiven Roll-out beim Pilot-Kunden
(`BuHa SoHa`, Customer Trustee Demo). Falsche Saldenzahlen unterminieren das
Vertrauen in die ganze Plattform und blockieren saemtliche AI-Auswertungen, die
auf Salden aufbauen (Geldfluss, Plausichecks, Bilanz-/ER-Vergleiche).
**Risiko bei Verzicht:** Jede Frage an den Agenten zu Salden / Bilanzpositionen
liefert subtile Falschwerte (kein offensichtlicher Crash, sondern stillschweigend
falsche Zahlen). Auditor-Risiko.
## Fokus und kritische Details
- **RMA hat einen dedizierten Saldo-Endpunkt** (`GET /gl/saldo` mit `accno`,
`from`, `to`, `bookkeeping_main_curr`, `exclude_yearend_bookings`). Liefert
pro Konto den echten, durch RMA berechneten Schlusssaldo unter
Beruecksichtigung von Vorjahres-Vortraegen und Jahresabschluss-Buchungen.
**Quelle der Wahrheit ist immer der Connector**, nicht eine lokale
Berechnung -- Letztere kann Vortraege und Jahresabschluss-Offsets nicht
rekonstruieren, wenn das Importfenster nur einen Teil der Historie enthaelt.
- **Bexio + Abacus haben keinen entsprechenden Endpunkt** in dem Detailgrad.
Bexio liefert `GET /3.0/accounting/journal` (alle Journal-Eintraege mit
`debit_account_id`, `credit_account_id`, `amount`, `date`); Abacus
`GET GeneralJournalEntries` (OData V4). Dort muessen wir lokal
**kumulativ** rechnen und den **Vorjahres-Vortrag** explizit aus den
Journal-Daten ableiten (Eroeffnungsbuchungen am Geschaeftsjahres-Anfang
bzw. Abschluss-Buchungen am Vorjahres-Ende).
- **Datenmodell `TrusteeDataAccountBalance` bleibt unveraendert** -- die
Felder `openingBalance`, `debitTotal`, `creditTotal`, `closingBalance` sind
bereits vorhanden. Der Bug ist rein in der Befuellung; kein DB-Migration
noetig (`_bulkClear` + `_bulkCreate` ueberschreibt jeden Sync ohnehin).
- **Konsumenten** der Tabelle: `mainTrustee.DATA_OBJECTS`,
`routeFeatureTrustee` (data-tables UI), `methodTrustee/actions/queryData`
(entity=`balances`, mode=`raw`/`aggregate`), AI-Agent ueber
`/api/automation2/catalog`. Keine Schema-Aenderung, also kein Frontend-Impact.
- **Persistenz-Performance**: Wir reden von 200--500 Konten x 13 Perioden
(Annual + 12 Monate) = max ~6500 Rows pro Sync. Der bestehende
`_bulkCreate`-Pfad ist dafuer dimensioniert (vgl. accountingDataSync.py
Header-Kommentar zur Asyncio-Architektur).
- **Connector-Fallback-Vertrag**: Wenn `getAccountBalances` `[]`
zurueckgibt **und** keine Exception wirft, gilt das als "Connector kann
das nicht / hat nichts geliefert" -- in dem Fall faellt der Sync auf
die korrigierte lokale Aggregation zurueck. Wirft der Connector eine
Exception, wird sie in `summary["errors"]` aufgenommen und der Sync laeuft
weiter (gleiches Pattern wie bei den uebrigen Phasen).
- **i18n**: Keine neuen User-facing Strings.
- **Naming**: Funktionen `_`-Prefix fuer intern (`_buildBalanceUrl`,
`_aggregateBalancesFromLines`); camelCase ueberall (Python + TS).
- **Logging**: Keine Emojis (vgl. `.cursor/rules/python-coding.mdc`).
## Ziel und Nicht-Ziele
- **Ziel**: `TrusteeDataAccountBalance` enthaelt **echte** Salden aus dem
Buchhaltungssystem (RMA, soweit verfuegbar) bzw. eine korrekt **kumulierte**
lokale Berechnung (Bexio, Abacus). `closingBalance` = Stichtagsbestand,
`openingBalance` = Stand zu Periodenbeginn, inkl. Vortraege.
- **Ziel**: Erweiterung von `BaseAccountingConnector` um optionale Methode
`getAccountBalances(...)`. Default-Implementierung gibt `[]` zurueck (keine
externe Quelle) -- Sync faellt auf den korrigierten Fallback zurueck.
- **Ziel**: RMA-Connector implementiert echte Saldenabfrage; Bexio + Abacus
bekommen entweder eine voll funktionsfaehige `getAccountBalances`-
Implementierung oder, falls nicht moeglich, einen detaillierten Code-
Kommentar mit Endpunkt-Recherche und Skizze des fehlenden Stuecks
(User-Direktive: "besser gleich umsetzen bei allen 3").
- **Ziel**: Korrekter Fallback in `_persistBalances`: Wenn der Connector
nichts liefert, **kumuliere** Journal-Lines pro Konto chronologisch und
fuelle `openingBalance` aus den Vorjahresbuchungen (oder `0`, wenn das
Importfenster den Account-Lifecycle abdeckt).
- **Ziel**: Unit-Test pro Connector + Integrationstest, der den BuHa-SoHa-
Fall reproduziert (Konto mit Vortrag, mehreren Monaten Bewegungen,
korrekter Schlusssaldo Ende Dezember).
- **Explizit NICHT**: Schema- oder DB-Migration. Felder existieren bereits;
jeder Sync schreibt die Tabelle ohnehin neu (`_bulkClear`).
- **Explizit NICHT**: Aenderung am Frontend (data-tables-Seite). Die
Tabelle bekommt nur korrekte Werte -- die Spalten sind bereits da.
- **Explizit NICHT**: Aenderung am AI-Agent / `queryData`-Action. Die Action
liefert weiter `entity="balances"` aus derselben Tabelle.
- **Explizit NICHT**: Saldo-Logik ausserhalb von Trustee (Sanctions-Plattform
etc.).
## Betroffene Module
- **Gateway**:
- `gateway/modules/features/trustee/accounting/accountingConnectorBase.py`
-- neue Datenklasse `AccountingPeriodBalance`, neue optionale Methode
`getAccountBalances`.
- `gateway/modules/features/trustee/accounting/connectors/accountingConnectorRma.py`
-- `getAccountBalances` via `GET /gl/saldo`. Annual-Bucket + 12 Monats-
Buckets, jeweils kumulativ bis Periodenende. Opening-Balance = Saldo per
Periodenstart - 1 Tag (zweiter API-Call) oder via vorheriger Periode.
- `gateway/modules/features/trustee/accounting/connectors/accountingConnectorBexio.py`
-- `getAccountBalances` aggregiert lokal aus `GET /3.0/accounting/journal`
(filtert per `from`/`to`-Param) -- Bexio bietet keine Saldoliste.
- `gateway/modules/features/trustee/accounting/connectors/accountingConnectorAbacus.py`
-- `getAccountBalances` aggregiert lokal aus `GET GeneralJournalEntries`
(OData-Filter `JournalDate ge ... and le ...`). Mit Schemahinweis im Code,
falls Abacus-Instanz die Entity `AccountBalances` (offiziell vorhanden,
aber Verfuegbarkeit instanzabhaengig) ausliefern kann.
- `gateway/modules/features/trustee/accounting/accountingDataSync.py`
-- Phase 4 (`_persistBalances`) umbauen: zuerst Connector fragen; bei
leerer / fehlerhafter Antwort den **korrigierten** lokalen Fallback nutzen.
- `gateway/modules/workflows/methods/methodTrustee/actions/refreshAccountingData.py`
-- nur Doc-String anpassen (verweist auf neuen Datenfluss).
- **Frontend**: keine Aenderung.
- **DB-Migration**: nein.
- **Andere Komponenten**: keine.
## Entscheidungen
| Datum | Entscheidung | Begruendung |
|-------|-------------|------------|
| 2026-04-25 | Connector-Werte haben Vorrang, lokale Aggregation ist Fallback. | Vorjahres-Vortraege und Jahresabschluss-Offsets kann nur das Buchhaltungssystem zuverlaessig liefern; lokale Aggregation aus dem Importfenster bleibt naehrungsweise. |
| 2026-04-25 | Per Konto + Periode 13 Buckets (Annual + 12 Monate). Jeder Bucket = Schlusssaldo per Periodenende. | Konsistent mit bestehendem Datenmodell und der `BuHa SoHa`-Tabelle (Monat 12 = Dezember-Stichtag). Annual-Bucket = Schlusssaldo per Geschaeftsjahres-Ende. |
| 2026-04-25 | RMA: Echte Saldenabfrage via `/gl/saldo` pro Periode (Annual + 12 Monate). 13 sequenzielle GETs pro Sync. | RMA-API bietet pro Aufruf nur einen Stichtag; mehrere Aufrufe sind unkritisch (Sync laeuft im Hintergrund, schon heute 30+s). |
| 2026-04-25 | Bexio: Aggregation aus `/3.0/accounting/journal` (kein dedizierter Saldo-Endpunkt vorhanden). | Bexio Journal-API liefert alle benoetigten Felder; lokale Kumulation ist hier exakt, weil der Bexio-Mandant typischerweise im Importfenster komplett liegt. |
| 2026-04-25 | Abacus: Aggregation aus `GeneralJournalEntries`. Falls Instanz `AccountBalances`-Entity hat, kann sie spaeter bevorzugt werden (Code-Kommentar fuer Folge-Iteration). | OData-Schema variiert pro Abacus-Instanz; sichere Default-Strategie ist Aggregation. |
| 2026-04-25 | `AccountingPeriodBalance`-Pydantic-Modell (statt dict) fuer Connector-Output. | Konsistent mit `AccountingChart`, `AccountingBooking`; Typsicherheit + Pydantic-Validierung. |
| 2026-04-25 | `getAccountBalances` ist OPTIONAL (Default: `[]`). | Erlaubt graduelles Rollout pro Connector ohne Breaking Change; bestehender Fallback bleibt verfuegbar. |
| 2026-04-25 | Lokaler Fallback rechnet jetzt korrekt kumulativ pro Konto, sortiert Journal-Lines nach Buchungsdatum, und propagiert den Saldo Monat-fuer-Monat. `openingBalance` der ersten Periode = `0` (besser nichts als ein erfundener Wert). | Vortrags-Berechnung ohne Connector-Daten ist nicht zuverlaessig; explizites `openingBalance=0` + Kommentar im UI/Doku ist ehrlicher. |
## Umsetzungs-Checkliste
### Backend
- [x] `AccountingPeriodBalance` (Pydantic) in `accountingConnectorBase.py`:
Felder `accountNumber`, `periodYear`, `periodMonth`, `openingBalance`,
`debitTotal`, `creditTotal`, `closingBalance`, `currency`, `asOfDate`.
- [x] `BaseAccountingConnector.getAccountBalances(config, year, accountNumbers=None)`
mit Default-Implementierung `return []`. Doc-String dokumentiert
Vertrag (Annual-Bucket = `periodMonth=0`).
- [x] RMA: `getAccountBalances` via `GET /gl/saldo`. Pro Konto-Filter `accno`
(z.B. `xxxx`) und Datum `to=YYYY-MM-DD`, einmal pro Periode (Annual +
12 Monate). Monatlicher `openingBalance` = Schlusssaldo Vormonat.
Fehler / leere Antwort -> Konto skippen (nicht Sync abbrechen).
- [x] Bexio: `getAccountBalances` ueber `_loadRawAccounts` (Account-Map)
+ `GET /3.0/accounting/journal?from=YYYY-MM-DD&to=YYYY-MM-DD`. Lokale
Kumulation (debit - credit) pro Konto pro Monat. Jahres-Bucket separat.
- [x] Abacus: `getAccountBalances` ueber `GET GeneralJournalEntries`
mit OData-Filter; gleiche Aggregations-Logik. Code-Kommentar mit
Hinweis auf optionale `AccountBalances`-Entity (instanzabhaengig).
- [x] `accountingDataSync._persistBalances` umbauen:
1. `await connector.getAccountBalances(plainConfig, year=...)` aufrufen
(Jahre aus dateFrom/dateTo ableiten -- kann ueber mehrere Jahre gehen).
2. Bei nicht-leerer Antwort: direkt persistieren.
3. Sonst Fallback `_aggregateBalancesFromLines(...)` -- korrekt kumulativ.
- [x] Logging: pro Phase eindeutige Log-Zeile mit Connector-Name + Source
(`buha` / `local-fallback`) + Zeilenzahl.
### Tests
- [x] `gateway/tests/unit/features/trustee/test_accountingConnectorRma_balances.py`:
`getAccountBalances` mit gemocktem `/gl/saldo` (17 Tests: BuHa-SoHa-Szenario,
ER-Reset, Parser, Helpers).
- [x] `gateway/tests/unit/features/trustee/test_accountingConnectorBexio_balances.py`:
lokale Aggregation aus gemockter Journal-Liste (8 Tests: BS cumulative,
carry-over, ER-Reset).
- [x] `gateway/tests/unit/features/trustee/test_accountingConnectorAbacus_balances.py`:
OData-Aggregation (6 Tests: BS carry-over, ER-Reset).
- [x] `gateway/tests/unit/features/trustee/test_accountingDataSync_balances.py`:
End-to-End mit FakeDb (11 Tests: Connector-Path verbatim persist,
Local-Fallback kumulative BS/ER-Berechnung, _resolveBalanceYears,
_isIncomeStatementAccount).
### Wiki
- [x] Plan: dieses Dokument von `1-plan/` -> `4-done/` verschoben.
- [x] `wiki/c-work/_CHANGELOG.md`: 2 Zeilen (fix + test).
- [x] `wiki/b-reference/gateway/features/trustee.md`: Abschnitt
"Saldenliste (`TrusteeDataAccountBalance`)" mit Datenquelle + Fallback
ergaenzt; Tests-Tabelle erweitert; Link auf dieses Dokument.
- [x] `lastReviewed: 2026-04-25` + `verifiedAgainst: ... + Account-Balance-Import`
aktualisiert.
## Akzeptanzkriterien
| # | Kriterium (Given-When-Then) | Prio |
|---|---------------------------|------|
| 1 | Given Mandant `BuHa SoHa` mit RMA-Connector und Konto 1020 (echter Schlusssaldo per 31.12.2025 = `48'507.41`), When der Sync laeuft, Then enthaelt `TrusteeDataAccountBalance` fuer `accountNumber='1020'`, `periodYear=2025`, `periodMonth=12` einen `closingBalance = 48'507.41` (Toleranz +/- 0.01 CHF). | must |
| 2 | Given derselbe Sync, When man die Annual-Zeile abfragt (`periodMonth=0`), Then `closingBalance` = Schlusssaldo per Geschaeftsjahres-Ende (= `48'507.41` bei Kalenderjahr-Mandant). | must |
| 3 | Given derselbe Sync, When man die Zeile fuer `periodMonth=11` abfragt, Then `closingBalance` = Stand per 30.11.2025 (kumulativ, NICHT November-Bewegung). | must |
| 4 | Given Bexio-Connector mit lokal aggregiertem Saldo, When alle Journal-Eintraege im Importfenster liegen und kein Vortrag existiert, Then `closingBalance` einer Periode = Summe aller debits - Summe aller credits bis Periodenende fuer dieses Konto. | must |
| 5 | Given Connector wirft Exception in `getAccountBalances`, When der Sync laeuft, Then erscheint die Fehlermeldung in `summary["errors"]`, der Sync laeuft weiter und der Fallback befuellt die Tabelle aus den Journalzeilen. | must |
| 6 | Given Frage an AI-Agent "Wie hoch war der Saldo Konto 1020 per 31.12.2025?", When `queryData(entity="balances", mode="raw")` aufgerufen wird, Then enthaelt das Resultat den Datensatz mit `closingBalance=48507.41`. | must |
| 7 | Given Sync-Lauf, When er fertig ist, Then enthaelt das Log eine Zeile `Persisted N balances for <id> in Xs (source=buha\|local-fallback)`. | should |
## Testplan
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|----|----|-----|--------------|-----------|--------|
| T1 | 1, 2, 3 | unit | ja | gateway/tests/unit/features/trustee/test_accountingConnectorRma_balances.py | 17/17 passed |
| T2 | 4 | unit | ja | gateway/tests/unit/features/trustee/test_accountingConnectorBexio_balances.py | 8/8 passed |
| T3 | 4 | unit | ja | gateway/tests/unit/features/trustee/test_accountingConnectorAbacus_balances.py | 6/6 passed |
| T4 | 1-3, 5 | integration | ja | gateway/tests/unit/features/trustee/test_accountingDataSync_balances.py (FakeDb) | 11/11 passed |
| T5 | 1, 2, 6 | manual | nein | PROD-Mandant `BuHa SoHa`, vorher/nachher Vergleich | pending (nach Deployment) |
## Links
- Bug-Report (Kunden-E-Mail vom 2026-04-25)
- RMA API Doc Saldo: <https://runmyaccountsag.github.io/runmyaccounts-rest-api/Resources/resources.html#general-ledger>
- Bexio Journal API: <https://docs.bexio.com/#tag/Accounting/operation/listJournal>
- Abacus REST API: <https://downloads.abacus.ch/fileadmin/ablage/abaconnect/htmlfiles/docs/restapi/abacus_rest_api.html>
## Abschluss
- [x] `wiki/b-reference/gateway/features/trustee.md` aktualisiert (Quelle der
Saldenliste).
- [x] `wiki/TOPICS.md` -- kein neuer Eintrag noetig (faellt unter Trustee).
- [x] Dieses Dokument in `wiki/c-work/4-done/` verschoben.