# 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 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: - Bexio Journal API: - Abacus REST API: ## 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.