-- 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;