Bisher entschied das erste Verfahren, das die Bank meldet – wer chipTAN nutzt, kam über eine App-Freigabe nicht weiter. Die Wahl steht jetzt auf einer eigenen Seite am Zugang. Abgefragt statt eingetippt: Die Kennungen sind bankspezifisch, und getTanModes() kommt ohne Anmeldung aus, verlangt also selbst keine TAN. Behoben: Das Speichern des Zugangsformulars setzte Verfahren und TAN-Medium zurück, weil beide Felder darin nicht vorkommen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
178 lines
8.9 KiB
Markdown
178 lines
8.9 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.
|
||
|
||
### Das Verfahren wählen
|
||
|
||
Fast jede Bank bietet mehrere Verfahren an. Welches gilt, steht am Zugang unter
|
||
*TAN-Verfahren einstellen* (`/banking/{id}/verfahren`).
|
||
|
||
**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. Der Knopf
|
||
*Verfahren bei der Bank abfragen* holt die Liste über `getTanModes()`. Das 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 abgefragte Liste liegt in der Sitzung, nicht in der Datenbank – sie ist eine
|
||
Momentaufnahme. Beim Übernehmen wird gegen sie 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 Verfahren steht bewusst nicht im Zugangsformular – dort ließe es sich nur als Zahl
|
||
eintragen. Das Formular fasst es deshalb auch 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
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|