Finanzmanager/docs/banking.md
fds2610-bot f388d04c05 Phasen 4 bis 7: Cockpit, Online-Banking, Wertpapiere, Nebenmodule
Damit ist der geplante Funktionsumfang beisammen.

Phase 4 abgeschlossen
- Konfigurierbare Cockpit-Kacheln; abgewählte werden nicht berechnet
- Einnahmen/Ausgaben je Monat als Balkenpaare mit Saldolinie, aufgesetzt auf
  den Summenbericht statt auf eine eigene Abfrage
- Meine Ausgaben als Ring auf der obersten Kategorieebene
- Offene Vorgänge aus allen Modulen

Phase 5 – Online-Banking
- nemiah/php-fints als Fork auf Codeberg, psr/log direkt eingebaut: die
  Anwendung hängt an keinem fremden Update-Zyklus
- PIN AES-256-GCM-verschlüsselt; Schlüssel aus APP_KEY oder aus
  data/schluessel/app_key, den der Entrypoint einmalig erzeugt
- TAN-Dialog über mehrere Requests (FinTs::persist plus serialisierte Aktion
  in der Sitzung) – ein wartender Prozess wäre mit php-fpm der falsche Weg
- Abgleich statt Direktbuchung: verbinden, übernehmen oder verwerfen
- Regeln mit vier Vergleichsarten, Betragsgrenzen und Split-Schema

Phase 6 – Wertpapiere
- Depotbuchung und Geldbuchung als zwei Seiten eines Geschäfts; betrag ist
  der Bruttowert, Nebenkosten stehen daneben
- Einstand nach gleitendem Durchschnitt, realisierte Gewinne, Erträge
- Kursabruf über Leeway oder FMP, täglicher Lauf per Cron
- QIF-Import für !Type:Invst

Dabei behoben: Der Import buchte den Geldfluss jedes Wertpapiergeschäfts als
Abgang – auch bei Verkäufen und Dividenden; und er erkannte das
Verrechnungskonto nicht, wenn dem Konto im L-Feld eine Kategorie vorangeht
(bei Verkäufen also immer).

Phase 7 – Nebenmodule und Zahlungsverkehr
- Buchungsvorlagen inklusive QIF-Import, Splits werden mitskaliert
- Regelmäßige Buchungen mit Fälligkeitslauf, Finanzkalender, Erinnerungen
- Adressen mit IBAN-Prüfung, Verträge mit Kündigungsfrist, Sparziele
- Kredite mit gerechnetem Tilgungsplan
- Überweisungen über FinTS; die Buchung entsteht erst nach Annahme durch die
  Bank. Die pain.001-Nachricht erzeugt SepaXml selbst

Nebenbei: .dockerignore ergänzt (ohne sie brach der Build an data/mariadb ab),
PlatzhalterController entfernt, zwei Inline-Handler beseitigt, die die
Content-Security-Policy ohnehin blockierte.

888 PHP-Prüfungen und 45 E2E-Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 06:59:24 +02:00

145 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
### 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.
Verfahren und TAN-Medium werden beim ersten Abruf gewählt und am Zugang gespeichert, damit
die Bank nicht jedes Mal danach fragt.
### Konten
Beim ersten Abruf legt die Anwendung für jedes Konto, das die Bank meldet, eine Zeile in
`bank_konten` an. Trägt ein eigenes Konto dieselbe IBAN, wird es gleich zugeordnet; sonst
geschieht das im Formular des Zugangs. **Ohne Zuordnung lassen sich die Umsätze nicht
buchen** – das Konto fehlt dann schlicht.
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.
## Abgleich statt Direktbuchung
Abgerufene Umsätze landen in `bank_umsaetze` und werden **nicht sofort gebucht**. Wer seine
Zahlungen selbst erfasst, hat sie beim Abruf meist schon im Kontoblatt; ohne Zwischenschritt
stünde alles doppelt da.
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 IBAN, Datum, Betrag, Verwendungszweck und Empfänger.
**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
offen – sie sind Arbeit, die der Nutzer sehen soll.
## 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 ein festes
**Split-Schema** in Prozent. Die letzte Splitzeile bekommt den Rest, damit die Summe auf
den Cent zum Buchungsbetrag passt.
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, 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.
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.