Finanzmanager/docs/banking.md
fds2610-bot a7bfbe39e0 Splitbuchungen in den Regeln für den Abgleich
Eine Regel kann jetzt eine Splitbuchung vorgeben: mehrere Kategorien mit einem
Anteil in Prozent. Greift sie, wird der Umsatz in diesem Verhältnis aufgeteilt –
bei jedem Betrag neu gerechnet, sodass eine Abrechnung über 300 € und eine über
450 € nach demselben Schlüssel gebucht werden.

Das Schema lag bereits im Modell und in der Datenbank, war über die Oberfläche
aber nicht erreichbar; die Dokumentation beschrieb ein Feld, das es nirgends zu
sehen gab.

- Unter jeder Regel klappt „Splitbuchung“ die Zeilen auf: Kategorie, Klasse,
  Notiz, Anteil. Dasselbe Formular steht bei „Neue Regel“ – beide binden
  src/Views/banking/regelsplits.php ein.
- Die Anteile gelten zueinander, nicht absolut: 60 zu 30 sind zwei Drittel zu
  einem Drittel. Bisher hätte die letzte Zeile als Rest 40 statt der
  eingetragenen 30 Prozent bekommen, und der Fehler wäre in der fertigen
  Buchung nicht mehr zu sehen gewesen.
- Im Abgleichdialog zeigt der Vorschlag die errechneten Zeilen und schickt sie
  mit. Zuvor gingen sie beim Knopf „Als neue Buchung“ verloren: Das Formular
  kannte nur Kategorie und Klasse, sodass von Hand übernommene Umsätze
  ungeteilt im Kontoblatt landeten. Der automatische Lauf war nie betroffen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:02:17 +02:00

18 KiB
Raw Blame History

Online-Banking (FinTS)

Umsätze kommen per Knopfdruck von der Bank, werden gegen die vorhandenen Buchungen abgeglichen und über Regeln weitgehend selbst kategorisiert.

Die Bibliothek: ein eigener Fork

Verwendet wird nemiah/php-fints, aber nicht von Packagist, sondern als Fork auf Codeberg (fds2610-bot/phpFinTS, eingebunden über ein VCS-Repository in der composer.json, Version fest auf 4.1.1 gepinnt).

Im Fork ist psr/log direkt eingebaut: Die acht Interface-Dateien liegen unter psr-log/Psr/Log/ und werden über einen eigenen Autoload-Eintrag geladen, der require-Eintrag ist entfallen. Damit hängt der Finanzmanager an keinem fremden Update-Zyklus – die Anwendung hat genau eine Abhängigkeit, und die steht unter eigener Kontrolle.

Beim Nachziehen eines neuen Upstream-Standes muss dieser Eingriff wiederholt werden; wie, steht in psr-log/README.md des Forks.

Der Schlüssel für die Zugangsdaten

PIN und TAN-Medium stehen AES-256-GCM-verschlüsselt in der Datenbank (Services\Crypto, übernommen aus Steuerkram). Der Schlüssel ist eine 64-stellige Hex-Zeichenkette:

  • aus der Umgebung (APP_KEY in der .env), oder
  • aus data/schluessel/app_key – die Datei erzeugt der Container-Entrypoint beim ersten Start, wenn APP_KEY leer ist.

Das Verzeichnis ist ein eigenes Bind-Mount, damit der Schlüssel einen Neubau des Images überlebt. Er gehört ins Backup: Ohne ihn sind gespeicherte Zugangsdaten nicht mehr lesbar (neu eingeben hilft, verloren ist sonst nichts).

Ablauf eines Abrufs

Der Abruf ist eine Zustandsmaschine über mehrere Requests:

anmelden → konten → umsaetze (je Konto) → fertig

Jeder Schritt kann eine TAN verlangen – bei den meisten Banken schon der Login, auch für reines Lesen. Dann sichert Services\FinTsAbruf den Dialog (FinTs::persist()) und die serialisierte Aktion in der Sitzung, zeigt die TAN-Maske und macht nach der Eingabe an derselben Stelle weiter.

Ein wartender Prozess wäre der naheliegende, aber falsche Weg: php-fpm hat dafür keinen Platz, und ein Request, der fünf Minuten auf eine TAN wartet, blockiert einen Worker.

Was in der Sitzung liegt, enthält keine PIN – die Bibliothek schließt sie beim Persistieren ausdrücklich aus. Sie wird bei jedem Request neu aus der Datenbank geholt und entschlüsselt.

Wie weit zurück: Zeitraum und Stichtag

Ab wann abgerufen wird, bestimmt Bankzugang::abrufBeginn(). Zwei Angaben spielen hinein:

  • Der Abrufzeitraum der Bankverbindung (bank_zugaenge.abrufzeitraum): seit dem letzten Abruf, letzte 7/30/365 Tage – oder Ab Stichtag mit dem Datum aus bank_zugaenge.abruf_stichtag.
  • Ein Stichtag für diesen einen Abruf, angegeben im Datumsfeld neben Umsätze abrufen. Er geht der Vorgabe vor und wird nirgends gespeichert.

Beides nebeneinander, weil beides gebraucht wird: Die Vorgabe beantwortet den Alltag („immer seit dem letzten Abruf“), der einmalige Stichtag den Sonderfall – einen Bestand ab einem festen Datum aufbauen, eine Lücke nachholen. Ohne ihn müsste man die Einstellung des Zugangs ändern und hinterher zurückstellen, mit dem üblichen Ausgang, dass man das Zurückstellen vergisst.

Der einmalige Stichtag liegt im Abrufzustand, nicht in der Datenbank: Nach einer TAN-Runde läuft der Abruf im nächsten Request weiter und muss dort denselben Zeitraum holen wie beim Start.

„Seit dem letzten Abruf“ nimmt den Tag des letzten Laufs, nicht die Uhrzeit: Banken buchen nachträglich, und ein Umsatz, der schon da war, fällt ohnehin über seinen Fingerabdruck heraus. Beim allerersten Abruf gilt ein Jahr zurück – sonst käme entweder nichts oder alles.

Ein Stichtag in der Zukunft wird auf heute gesetzt statt abgewiesen: Aus der Zukunft liefert keine Bank etwas, und ein verrutschtes Datum soll den Abruf nicht ergebnislos machen – das Protokoll vermerkt die Korrektur. Ab Stichtag ohne Datum lässt sich dagegen gar nicht erst speichern; eine Vorgabe, die nichts vorgibt, führte beim Abruf nur zu einem anderen Zeitraum als gedacht.

Wie weit die Bank tatsächlich zurückreicht, entscheidet sie selbst – 90 Tage sind üblich, mehr liefert nicht jede. Ein früherer Stichtag holt deshalb nicht zwingend mehr; die Lücke davor schließt der Anfangssaldo des Kontos oder der QIF-Import.

TAN-Verfahren

  • Klassisch (chipTAN, photoTAN): Die Maske zeigt den Hinweistext der Bank und, wenn vorhanden, die photoTAN-Grafik als Datenadresse. Der Nutzer tippt die TAN ein.
  • Entkoppelt (pushTAN-App): Die Freigabe passiert im Gerät des Nutzers. Statt zu warten, fragt die Anwendung auf Knopfdruck nach (checkDecoupledSubmission) – steht die Freigabe noch aus, bleibt die Maske stehen.

Einrichtung in drei Schritten

Fast jede Bank bietet mehrere Verfahren an, und welche das sind, weiß nur sie. Fragen lässt sie sich aber erst, wenn Bankleitzahl, FinTS-Adresse, Benutzerkennung und PIN vorliegen – deshalb zerfällt das Anlegen einer Bankverbindung in drei Schritte:

  1. Zugangsdaten (/banking/neu) – Weiter speichert sie.
  2. TAN-Verfahren (/banking/{id}/verfahren) – die Seite fragt die Bank beim Aufruf selbst und zeigt die Liste zur Wahl.
  3. Konten zuordnen (/banking/{id}/konten) – welches Bankkonto gehört zu welchem eigenen Konto?

Der zweite Schritt holt die Liste ohne Knopfdruck: Wer hier landet, will genau das, und ein Knopf, dessen Ergebnis feststeht, wäre ein Schritt zu viel. Ein erneuter Anlauf steht trotzdem bereit, wenn sich etwas geändert hat.

Am Ende des dritten Schritts – und ebenso beim vorzeitigen Verlassen über Später einstellen oder Später zuordnen – geht es zurück in die Übersicht der Bankverbindungen (BankingController::zielNachZuordnung()), nicht in die Zugangsmaske: Dort steht die neue Verbindung fertig zwischen den anderen, während die Zugangsmaske nur noch anböte, was gerade durchlaufen wurde. Einzeln aufgerufen bleibt die Zuordnung dagegen auf ihrer Seite stehen.

Später erreicht man dieselbe Seite über TAN-Verfahren einstellen am Zugang.

Abgefragt, nicht eingetippt. Die Kennungen sind bankspezifisch – 913 heißt nicht überall chipTAN QR –, und niemand soll sie in einer PDF der Bank suchen müssen. getTanModes() kommt ohne Anmeldung aus (die Bibliothek braucht dafür nur die Bankparameterdaten) und verlangt deshalb selbst keine TAN.

getTanMedia() wird nur für die Verfahren aufgerufen, die ein Medium brauchen: Jeder Aufruf ist ein eigener Dialog mit der Bank.

Die Zeitüberschreitungen sind kürzer gesetzt als in der Bibliothek (8 statt 15 Sekunden zum Verbinden, 20 statt 30 zur Antwort): Die Abfrage hängt an einem Seitenaufruf.

Das Ergebnis liegt in der Sitzung, auch ein gescheitertes – sonst liefe jeder Seitenaufruf erneut in dieselbe Zeitüberschreitung. Scheitert die Abfrage, erklärt die Seite, wo der Fehler meist steckt, und der Weg zurück zu den Zugangsdaten steht daneben. Die Wahl lässt sich überspringen; die Verbindung ist auch ohne sie benutzbar.

Beim Übernehmen wird gegen die abgefragte Liste geprüft: Eine Kennung, die die Bank nicht angeboten hat, wäre beim nächsten Abruf wirkungslos.

Ohne Wahl nimmt der erste Abruf das erste Verfahren, das die Bank meldet, und hält es fest; sonst stünde im Formular nichts. Eine ausdrückliche Wahl wird dagegen nie überschrieben: Bietet die Bank sie einmal nicht an, sagt das Protokoll das und die Einstellung bleibt stehen.

Das Zugangsformular fasst Verfahren und Medium nicht an: Ohne diesen Vorbehalt setzte jedes Speichern die Wahl zurück.

Abruf und Überweisung benutzen dieselbe Wahl (Services\FinTsVerfahren::anwenden()) – eine Überweisung soll nicht plötzlich ein anderes Verfahren verlangen als der Abruf.

Konten zuordnen

Für jedes Konto, das die Bank meldet, legt die Anwendung eine Zeile in bank_konten an. Trägt ein eigenes Konto dieselbe IBAN, wird es gleich zugeordnet; sonst geschieht das auf der Zuordnungsseite. Ohne Zuordnung lassen sich die Umsätze nicht buchen – das Konto fehlt dann schlicht.

Die Liste kommt frisch von der Bank. Anders als die Verfahrensabfrage braucht das eine Anmeldung, und die verlangt bei den meisten Banken eine TAN – deshalb steht dafür ein Knopf und keine Automatik beim Seitenaufruf. Im Einrichtungsablauf startet das Übernehmen des TAN-Verfahrens die Abfrage gleich mit; sie benutzt dieselbe Zustandsmaschine wie der Umsatzabruf, hält aber nach der Kontenabfrage an (FinTsAbruf::starten($id, 'konten')).

Fehlt ein passendes eigenes Konto, legt neues Konto anlegen es an. IBAN, Kontonummer und Bankname kommen aus dem, was die Bank gemeldet hat; einzutragen bleiben Name, Kontoart und Anfangssaldo – letzterer deckt die Zeit ab, die der Abruf nicht mehr hergibt.

Wird die Zuordnung nach einem Abruf getroffen, ziehen die schon abgerufenen offenen Umsätze mit: bank_umsaetze.bank_konto_id merkt sich, von welchem Bankkonto ein Umsatz stammt. Übernommene bleiben, wo sie sind – ihre Buchung steht bereits in einem Kontoblatt, und ein Kontowechsel würde sie dort wegziehen.

Was nicht abgerufen werden soll, wird dort abgewählt; das ist der Kontenfilter.

Der Saldo wird mitgeholt und landet in der Spalte Online-Saldo der Kontenübersicht. Verlangt die Bank dafür eine eigene TAN, entfällt er – eine zweite TAN-Runde nur für eine Zahl wäre niemandem zu erklären.

Vom Umsatz zur Buchung

Abgerufene Umsätze landen zuerst in bank_umsaetze. Ob sie von dort selbst weiterwandern, entscheidet Abgerufene Umsätze selbst buchen (banking_automatisch_buchen, Vorgabe an) in den Einstellungen.

Automatisch buchen

Eingeschaltet läuft nach jedem Abruf – und nach dem Speichern der Kontenzuordnung – Services\Bankabgleich über alle offenen Umsätze:

  • Passt eine schon erfasste Buchung, wird verbunden statt neu gebucht. Sonst stünde jede selbst erfasste Zahlung doppelt im Kontoblatt.
  • Sonst entsteht eine neue Buchung – mit der Kategorie aus der Regel, und wenn keine greift, ohne. Genau dafür ist der Schalter da: Der Umsatz soll im Kontoblatt stehen, die Kategorie wird dort nachgetragen.
  • Umsätze ohne zugeordnetes Konto bleiben offen; ohne Konto gibt es kein Kontoblatt, in das sie gehörten.

Der Lauf läuft einmal am Ende, nicht je Konto: So sieht er alle Umsätze auf einmal und kann jeden mit der Buchung verbinden, die zu ihm passt. Ein einzelner Umsatz bricht ihn nicht ab – ein abgeschlossener Monat oder ein doppelter Fingerabdruck betrifft nur ihn, er bleibt offen, und die Meldung nennt die ersten Fälle.

Wo dabei noch eine Kategorie fehlt, zeigt die Kontenübersicht: Konten mit unkategorisierten Buchungen aus dem laufenden Jahr stehen dort fett.

Der Abgleichdialog

Abgeschaltet – oder für alles, was liegen geblieben ist – bleibt es beim Abgleich von Hand. Die Vorschlagsliste (Umsätze abgleichen) kennt drei Wege:

Vorschlag Wann Wirkung
Verbinden Es gibt eine erfasste Buchung mit gleichem Konto, gleichem Betrag und nahem Datum Der Umsatz wird mit ihr verknüpft, die Buchung gilt als abgeglichen
Als neue Buchung Keine passende Buchung vorhanden Neue Buchung mit Quelle fints, Kennzeichen abgeglichen
Verwerfen Der Umsatz soll nicht ins Kontoblatt Bleibt als „verworfen“ liegen und kommt nicht wieder

Wie weit Buchungs- und Banktag auseinanderliegen dürfen, steht als Toleranz beim Abgleich in den Einstellungen (Vorgabe fünf Tage). Eine Buchung, die schon mit einem anderen Umsatz verbunden ist, wird nie ein zweites Mal vorgeschlagen.

Dass derselbe Umsatz bei überlappenden Abrufzeiträumen nur einmal hereinkommt, sichert extern_id – ein Fingerabdruck aus Bankkonto, Datum, Betrag, Verwendungszweck und Empfänger. Das Bankkonto steckt als bank_konto_id darin, nicht als IBAN: Welche Kennung eine Bank liefert, wechselt von Dialog zu Dialog – die Comdirect etwa nennt die IBAN beim Kontenabruf, beim Umsatzabruf aber nicht. Hinge der Fingerabdruck daran, käme derselbe Umsatz ein zweites Mal herein. Aus demselben Grund findet ein Bankkonto sich über seine Kontonummer wieder; eine später nachgelieferte IBAN wird am vorhandenen Eintrag nachgetragen, statt einen zweiten anzulegen.

Verliert ein Umsatz seine Buchung – jemand löscht sie im Kontoblatt –, kehrt er in die Vorschlagsliste zurück. Bliebe er auf übernommen stehen, wäre er nirgends mehr zu sehen: nicht im Kontoblatt, nicht in der Liste, und beim nächsten Abruf als schon vorhanden übersprungen. Verwaiste Umsätze aus gelöschten Konten fängt derselbe Mechanismus vor jedem Abruf und vor jedem Öffnen der Liste ab.

Alle eindeutigen übernehmen erledigt in einem Zug, was ohne Rückfrage geht: verbinden, wo eine Buchung passt, und übernehmen, wo eine Regel greift. Umsätze ohne Regel bleiben hier offen – anders als beim automatischen Lauf. Wer den Dialog öffnet, will die Zuordnung selbst treffen; eine Buchung ohne Kategorie wäre ihm nur ungefragt aufgehalst.

Nichts verschwindet von allein: Ein Umsatz, der weder übernommen noch verworfen wird, bleibt offen und steht beim nächsten Öffnen wieder da. Das Cockpit zählt die offenen unter zum Abgleich.

Regeln

Eine Regel prüft ein Muster gegen Empfänger und/oder Verwendungszweck:

  • Vergleich: enthält, beginnt mit, endet auf, regulärer Ausdruck. Groß- und Kleinschreibung spielt nie eine Rolle – Banken schreiben denselben Empfänger mal so, mal so.
  • Betragsgrenzen rechnen mit Vorzeichen: „von −60 bis −40“ trifft Lastschriften zwischen 40 und 60 Euro.
  • Wirkung: Kategorie, Klasse, Gegenkonto (macht daraus eine Umbuchung) oder eine Splitbuchung.

Splitbuchung in einer Regel

Unter jeder Regel klappt Splitbuchung die Zeilen auf: je Zeile eine Kategorie, wahlweise Klasse und Notiz, dazu ein Anteil in Prozent. Greift die Regel, entsteht keine Buchung mit einer Kategorie, sondern eine Splitbuchung mit diesen Zeilen.

Die Anteile gelten zueinander, nicht als feste Beträge – darin liegt der ganze Zweck: Dieselbe Regel trifft bei jeder Rechnungshöhe dasselbe Verhältnis. Eine Nebenkostenabrechnung über 300 € wird bei 60 zu 30 in 200 € und 100 € geteilt, eine über 450 € in 300 € und 150 €. Ob dort 60 und 30 steht oder 2 und 1, ändert nichts.

Dass die Anteile aufeinander bezogen werden und nicht absolut gelten, ist Absicht: Sonst bekäme die letzte Zeile als Rest plötzlich 40 statt der eingetragenen 30 Prozent, und der Fehler wäre in der fertigen Buchung nicht mehr zu sehen. Die letzte Zeile bekommt weiterhin den Rundungsrest, damit die Summe auf den Cent zum Buchungsbetrag passt – bei einem Drittel zu zwei Dritteln ginge sonst ein Cent verloren, und die Buchung ließe sich nicht speichern.

Eine Zeile ohne Kategorie oder ohne Anteil fällt weg; ohne jede Splitzeile bucht die Regel wie zuvor auf ihre Kategorie. Im Abgleichdialog zeigt der Vorschlag die errechneten Zeilen mit ihren Beträgen an und übernimmt sie unverändert – geändert wird eine Aufteilung im Kontoblatt, wo die ganze Buchung samt Summenprüfung vorliegt.

Die erste passende Regel gewinnt, sortiert nach Priorität (kleinere Zahl zuerst). Kein Sammeln über mehrere Regeln: Wenn zwei verschiedene Kategorien setzen wollten, wäre am Ende nicht mehr nachvollziehbar, warum die eine dasteht.

Reguläre Ausdrücke werden beim Speichern geprüft – ein kaputtes Muster brächte sonst jeden Abruf zum Stehen. Eingegeben wird ohne Trennzeichen: Miete|Nebenkosten, nicht /Miete/.

Der Zähler Treffer zeigt, welche Regeln im Alltag überhaupt greifen.

Protokoll und Diagnose

Jeder Abruf schreibt eine Zeile nach bank_abrufe mit Status, Zahlen und Verlauf – ohne das ist ein fehlgeschlagener Abruf nicht mehr zu rekonstruieren, denn FinTS-Meldungen der Banken sind selten selbsterklärend. Läufe, die länger als zwei Stunden auf eine TAN warten, gelten als abgebrochen.

Bei eingeschaltetem Debug-Log (Einstellungen → Diagnose) landen zusätzlich die FinTS-Dialogschritte in data/logs/ – über Services\DebugLogAdapter, der den PSR-3-Logger der Bibliothek an das eigene Log hängt. PIN und TAN maskieren beide Seiten.

Tests

tests/16_BankingTest.php prüft alles um den Dialog herum: Verschlüsselung (Kennung, kein Klartext, wechselnder Geheimtext, kein Verschachteln), PIN bleibt beim Speichern erhalten, Abrufzeiträume samt Stichtag (gespeicherte Vorgabe, einmalige Angabe geht ihr vor und lässt die Vorgabe stehen, Zukunft wird auf heute gesetzt, unlesbare Datumsangaben werden abgewiesen), Dublettenerkennung, Regeln nach Priorität und Feld, Splitschema mit Restausgleich, Abweisung ungültiger Ausdrücke, Abgleich gegen vorhandene Buchungen samt Toleranz sowie Übernehmen, Verwerfen und Zurücksetzen.

Zur Verfahrenswahl geprüft: Kennung, Name und verschlüsseltes Medium werden gespeichert, unbekannte Kennungen und Medien abgewiesen, ein Verfahren ohne Medienzwang bekommt keines angehängt, und das Speichern des Zugangsformulars lässt die Wahl stehen. Die Abfrage bei der Bank steckt hinter derselben Grenze wie der Dialog – geprüft wird, was mit ihrem Ergebnis geschieht.

tests/19_KontenzuordnungTest.php nimmt sich den Weg vor, den ein Umsatz nimmt, wenn die Zuordnung erst nach dem Abruf getroffen wird: Offene Umsätze ziehen mit, übernommene nicht, und nur das Bankkonto, um das es geht. Dazu das automatische Buchen – mit Regel, ohne Regel, verbinden statt doppeln, Umsätze ohne Konto bleiben liegen, abgeschaltet passiert nichts – sowie die Zählung der unkategorisierten Buchungen je Konto (Jahresgrenze, Umbuchung und Splitbuchung zählen nicht mit) und ihr Weg in die Kontenübersicht.

Der FinTS-Dialog selbst braucht eine echte Bankverbindung und lässt sich nicht nachstellen. Geprüft wurde er gegen den Fehlerpfad: Eine unerreichbare Adresse bricht sauber ab, protokolliert und meldet verständlich.