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>
158 lines
8.3 KiB
SQL
158 lines
8.3 KiB
SQL
-- Phase 5: Online-Banking über FinTS.
|
||
--
|
||
-- Ein Bankzugang bündelt die Zugangsdaten einer Bank; ihm hängen die Konten an,
|
||
-- die dort geführt werden (Zuordnung IBAN → eigenes Konto). Abgerufene Umsätze
|
||
-- landen zunächst als Rohdaten in bank_umsaetze und werden erst beim Abgleich zu
|
||
-- Buchungen – so lässt sich jeder Abruf ansehen, bevor er im Kontoblatt steht.
|
||
|
||
CREATE TABLE IF NOT EXISTS bank_zugaenge (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||
benutzer_id INT UNSIGNED NOT NULL,
|
||
name VARCHAR(128) NOT NULL,
|
||
bankname VARCHAR(128) NOT NULL DEFAULT '',
|
||
blz VARCHAR(16) NOT NULL DEFAULT '',
|
||
fints_url VARCHAR(255) NOT NULL DEFAULT '',
|
||
benutzerkennung VARCHAR(128) NOT NULL DEFAULT '',
|
||
-- PIN und TAN-Medium sind AES-verschlüsselt (Services\Crypto), niemals im
|
||
-- Klartext. Der Schlüssel steht als APP_KEY in der Umgebung, nicht hier.
|
||
pin TEXT NULL,
|
||
tan_verfahren_id INT NULL,
|
||
tan_medium VARCHAR(128) NOT NULL DEFAULT '',
|
||
-- Abrufzeitraum: seit dem letzten Abruf, letzte 7/30/365 Tage.
|
||
abrufzeitraum VARCHAR(24) NOT NULL DEFAULT 'letzter_abruf',
|
||
zuletzt_am DATETIME NULL,
|
||
aktiv TINYINT(1) NOT NULL DEFAULT 1,
|
||
erstellt_am DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
geaendert_am DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY uq_bankzugang_name (benutzer_id, name),
|
||
CONSTRAINT fk_bankzugang_benutzer FOREIGN KEY (benutzer_id)
|
||
REFERENCES benutzer (id) ON DELETE CASCADE
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
-- Welches Bankkonto zu welchem eigenen Konto gehört. Eine eigene Tabelle statt
|
||
-- einer JSON-Liste am Zugang: Die Zuordnung wird bei jedem Abruf gebraucht, und
|
||
-- ein Fremdschlüssel auf konten hält sie sauber, wenn ein Konto verschwindet.
|
||
CREATE TABLE IF NOT EXISTS bank_konten (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||
zugang_id INT UNSIGNED NOT NULL,
|
||
konto_id INT UNSIGNED NULL,
|
||
iban VARCHAR(34) NOT NULL DEFAULT '',
|
||
kontonummer VARCHAR(34) NOT NULL DEFAULT '',
|
||
bezeichnung VARCHAR(128) NOT NULL DEFAULT '',
|
||
-- Abgeschaltete Zuordnungen werden beim Abruf übersprungen; das ist der
|
||
-- Kontenfilter aus PLAN.md, nur eine Zeile statt eines JSON-Feldes.
|
||
abrufen TINYINT(1) NOT NULL DEFAULT 1,
|
||
saldo DECIMAL(15,2) NULL,
|
||
saldo_am DATETIME NULL,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY uq_bankkonto (zugang_id, iban, kontonummer),
|
||
KEY idx_bankkonto_konto (konto_id),
|
||
CONSTRAINT fk_bankkonto_zugang FOREIGN KEY (zugang_id)
|
||
REFERENCES bank_zugaenge (id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_bankkonto_konto FOREIGN KEY (konto_id)
|
||
REFERENCES konten (id) ON DELETE SET NULL
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
-- Rohumsätze vor dem Abgleich. extern_id ist der Fingerabdruck des Umsatzes bei
|
||
-- der Bank; er verhindert, dass derselbe Umsatz bei überlappenden Abrufzeiträumen
|
||
-- zweimal hereinkommt.
|
||
CREATE TABLE IF NOT EXISTS bank_umsaetze (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||
benutzer_id INT UNSIGNED NOT NULL,
|
||
zugang_id INT UNSIGNED NOT NULL,
|
||
konto_id INT UNSIGNED NULL,
|
||
extern_id CHAR(40) NOT NULL,
|
||
buchungstag DATE NOT NULL,
|
||
wertstellung DATE NULL,
|
||
betrag DECIMAL(15,2) NOT NULL DEFAULT 0.00,
|
||
waehrung CHAR(3) NOT NULL DEFAULT 'EUR',
|
||
empfaenger VARCHAR(255) NOT NULL DEFAULT '',
|
||
verwendungszweck TEXT NULL,
|
||
buchungsart VARCHAR(100) NOT NULL DEFAULT '',
|
||
-- offen: wartet auf den Abgleich; uebernommen: wurde zur Buchung;
|
||
-- verworfen: bewusst liegen gelassen (etwa doppelt gelieferte Altumsätze).
|
||
status ENUM('offen','uebernommen','verworfen') NOT NULL DEFAULT 'offen',
|
||
buchung_id INT UNSIGNED NULL,
|
||
roh LONGTEXT NULL,
|
||
abgerufen_am DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY uq_umsatz_extern (benutzer_id, extern_id),
|
||
KEY idx_umsatz_status (benutzer_id, status, buchungstag),
|
||
KEY idx_umsatz_konto (konto_id, buchungstag),
|
||
CONSTRAINT fk_umsatz_benutzer FOREIGN KEY (benutzer_id)
|
||
REFERENCES benutzer (id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_umsatz_zugang FOREIGN KEY (zugang_id)
|
||
REFERENCES bank_zugaenge (id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_umsatz_konto FOREIGN KEY (konto_id)
|
||
REFERENCES konten (id) ON DELETE SET NULL,
|
||
-- Wird die Buchung gelöscht, ist der Umsatz wieder offen; er verschwindet
|
||
-- nicht mit ihr, sonst fehlte er beim nächsten Abgleich.
|
||
CONSTRAINT fk_umsatz_buchung FOREIGN KEY (buchung_id)
|
||
REFERENCES buchungen (id) ON DELETE SET NULL
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
-- Regeln für die automatische Kategorisierung. Sie greifen beim Abruf und auf
|
||
-- Wunsch nachträglich auf schon vorhandene Buchungen.
|
||
CREATE TABLE IF NOT EXISTS abgleich_regeln (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||
benutzer_id INT UNSIGNED NOT NULL,
|
||
name VARCHAR(128) NOT NULL,
|
||
-- Worauf das Muster passt: Empfänger, Verwendungszweck oder beides.
|
||
feld ENUM('empfaenger','verwendungszweck','beides') NOT NULL DEFAULT 'beides',
|
||
-- Wie verglichen wird. „regex“ ist der Notausgang für die Fälle, in denen
|
||
-- „enthält“ nicht reicht; Muster werden vor dem Speichern geprüft.
|
||
vergleich ENUM('enthaelt','beginnt','endet','regex') NOT NULL DEFAULT 'enthaelt',
|
||
muster VARCHAR(255) NOT NULL,
|
||
betrag_von DECIMAL(15,2) NULL,
|
||
betrag_bis DECIMAL(15,2) NULL,
|
||
kategorie_id INT UNSIGNED NULL,
|
||
klasse_id INT UNSIGNED NULL,
|
||
gegenkonto_id INT UNSIGNED NULL,
|
||
-- Festes Split-Schema als JSON: [{"kategorie_id":1,"anteil":0.7}, …].
|
||
splits LONGTEXT NULL,
|
||
-- Kleinere Zahl greift zuerst; bei Gleichstand entscheidet die Reihenfolge.
|
||
prioritaet INT NOT NULL DEFAULT 100,
|
||
aktiv TINYINT(1) NOT NULL DEFAULT 1,
|
||
treffer INT UNSIGNED NOT NULL DEFAULT 0,
|
||
erstellt_am DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY uq_regel_name (benutzer_id, name),
|
||
KEY idx_regel_reihenfolge (benutzer_id, aktiv, prioritaet),
|
||
CONSTRAINT fk_regel_benutzer FOREIGN KEY (benutzer_id)
|
||
REFERENCES benutzer (id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_regel_kategorie FOREIGN KEY (kategorie_id)
|
||
REFERENCES kategorien (id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_regel_klasse FOREIGN KEY (klasse_id)
|
||
REFERENCES klassen (id) ON DELETE SET NULL,
|
||
CONSTRAINT fk_regel_gegenkonto FOREIGN KEY (gegenkonto_id)
|
||
REFERENCES konten (id) ON DELETE CASCADE
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
-- Abrufprotokoll: was wann lief, wie viel dabei herauskam und woran es
|
||
-- gegebenenfalls scheiterte. Ohne das ist ein fehlgeschlagener Abruf nicht mehr
|
||
-- nachzuvollziehen – die FinTS-Meldungen der Banken sind selten selbsterklärend.
|
||
CREATE TABLE IF NOT EXISTS bank_abrufe (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||
benutzer_id INT UNSIGNED NOT NULL,
|
||
zugang_id INT UNSIGNED NOT NULL,
|
||
begonnen_am DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
beendet_am DATETIME NULL,
|
||
status ENUM('laeuft','tan_offen','erfolg','fehler','abgebrochen') NOT NULL DEFAULT 'laeuft',
|
||
neu INT UNSIGNED NOT NULL DEFAULT 0,
|
||
uebersprungen INT UNSIGNED NOT NULL DEFAULT 0,
|
||
meldung TEXT NULL,
|
||
protokoll LONGTEXT NULL,
|
||
PRIMARY KEY (id),
|
||
KEY idx_abruf_zugang (zugang_id, begonnen_am),
|
||
CONSTRAINT fk_abruf_benutzer FOREIGN KEY (benutzer_id)
|
||
REFERENCES benutzer (id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_abruf_zugang FOREIGN KEY (zugang_id)
|
||
REFERENCES bank_zugaenge (id) ON DELETE CASCADE
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
INSERT INTO einstellungen (schluessel, wert, bereich) VALUES
|
||
('banking_produkt_id', '', 'banking'),
|
||
('banking_regeln_beim_abruf', '1', 'banking'),
|
||
('banking_abgleich_tage', '5', 'banking')
|
||
ON DUPLICATE KEY UPDATE schluessel = schluessel;
|