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>
145 lines
7.1 KiB
Markdown
145 lines
7.1 KiB
Markdown
# 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.
|