From 2b93d385138f87092dab07c9ff5ee5f22327cbfa Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Tue, 21 Jul 2026 18:43:41 +0200 Subject: [PATCH 1/5] Feature: Kontostand nach Direktabruf auf Bank-Verbindungskachel --- Changes.md | 9 +++ docs/funktionen.md | 1 + migrations/087_bank_sources_last_balance.sql | 2 + src/Models/BankSource.php | 15 ++++- src/Services/FinTsFetchService.php | 17 +++++- src/Services/PayPalNvpFetchService.php | 6 +- src/Views/bank/fints.php | 13 +++++ src/version.txt | 2 +- tests/57_BankSourceLastBalanceTest.php | 61 ++++++++++++++++++++ 9 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 migrations/087_bank_sources_last_balance.sql create mode 100644 tests/57_BankSourceLastBalanceTest.php diff --git a/Changes.md b/Changes.md index a838a7d..f4c882a 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,15 @@ --- +## 2026-07-21 – Feature: Kontostand nach Direktabruf auf der Bank-Verbindungskachel + +- **`migrations/087_bank_sources_last_balance.sql`:** Neue Spalte `last_balance` in `bank_sources`. +- **`src/Models/BankSource.php`:** `updateLastFetched()` nimmt optional den beim Abruf ermittelten Kontostand entgegen und persistiert ihn; ohne neuen Wert bleibt der zuletzt bekannte Stand erhalten (kein Verlust bei Fehlern während des Abrufs). +- **`src/Services/FinTsFetchService.php`:** Summiert nach dem Abruf pro Konto den Schlusssaldo (`getEndBalance()`, Fallback `getStartBalance()`) des letzten Statements und übergibt ihn an `updateLastFetched()`. +- **`src/Services/PayPalNvpFetchService.php`:** Der bereits vorhandene `GetBalance`-Aufruf in `testCredentials()` liefert den aktuellen Kontostand (`AMT`), der nun mitgespeichert wird. +- **`src/Views/bank/fints.php`:** Kachel zeigt bei vorhandenem Kontostand rechts neben dem Abrufdatum den Betrag an (gleiche Höhe wie das Datum), mit dem Label „Kontostand" rechtsbündig darüber. Kachelgröße unverändert. +- **`tests/57_BankSourceLastBalanceTest.php`:** Neuer Test prüft, dass `last_balance` beim Abruf gesetzt wird und bei einem Abruf ohne ermittelten Kontostand erhalten bleibt. + ## 2026-07-16 – Fix: Stored XSS über KI-Kontierungsvorschlag (supplier_name) in Beleg-Detailansicht (Issue #129) - **Mittel-Hoch:** `supplier_name` aus dem per LLM erzeugten Kontierungsvorschlag (`KontierungsvorschlagService`) wird nur `trim()`t und ungeprüft in `belege.ki_vorschlag` (JSON) gespeichert. `_showKiBadgeDone()` in `src/Views/belege/detail.php` baute daraus String-Konkatenation und wies sie ohne Escaping über `innerHTML` zu. Da der Belegtext (PDF/Bild/ZUGFeRD-XML) von einem externen Rechnungssteller stammt, konnte ein präparierter Beleg das LLM per Prompt-Injection dazu bringen, für `supplier_name` HTML/JS-Payload statt eines Firmennamens auszugeben – dieser wurde beim Öffnen des Belegs im Browser-Kontext des eingeloggten Nutzers ausgeführt (CWE-79). diff --git a/docs/funktionen.md b/docs/funktionen.md index 34a29c1..c54da1c 100644 --- a/docs/funktionen.md +++ b/docs/funktionen.md @@ -72,6 +72,7 @@ Anlagenspiegel für abnutzbare Wirtschaftsgüter – siehe [anlagevermögen.md]( - **Direktverbindung „PayPal"** (NVP/SOAP-API): API-Benutzer, API-Kennwort, Signature und Sandbox-Flag werden verschlüsselt abgelegt. Live-Log via Server-Sent Events - **Direktverbindung „FinTS"** (Comdirect, DKB und weitere): BLZ, Benutzerkennung und PIN, optional FinTS-Server-URL (sonst Default aus dem Provider) und IBAN-Filter. PIN wird verschlüsselt abgelegt (AES-256-GCM). Leseabruf mit PIN; TAN-pflichtige Aktionen werden mit Fehlermeldung abgebrochen. Die FinTS-Server-URL wird beim Speichern und erneut vor jedem Abruf mit `SsrfGuard` geprüft: nur `https://` zu öffentlich erreichbaren Hosts ist erlaubt, private/interne Ziele (RFC1918, Loopback, Link-Local) werden abgelehnt (Schutz vor SSRF) - **Letzter Abrufzeitpunkt** wird pro Verbindung gespeichert; nächster Abruf startet ab 00:00 dieses Tages (max. ein Tag Überlappung für Dedup) +- **Kontostand**: FinTS-Abrufe liefern den Schlusssaldo des letzten Statements, PayPal den aktuellen Saldo über `GetBalance`; der Wert wird zusammen mit dem Abrufzeitpunkt auf der Verbindungskachel angezeigt und bleibt bei Abrufen ohne ermittelten Kontostand erhalten - **Abrufen ab**: Im Bearbeiten-Dialog kann pro Direktverbindung ausgewählt werden, ab wann der nächste Lauf Buchungen holt: *Ab letztem Abruf* (Default), *Letzte 7 Tage*, *Aktueller Monat (ab 01.)*, *Letzte 30 Tage* oder *Ohne Datum (letzte 365 Tage)*. Die Sonderauswahl gilt nur einmalig – nach erfolgreichem Abruf fällt die Verbindung wieder auf den Default zurück - **Dublettenprüfung**: CSV-Import vergleicht Datum+Betrag+Text; Direktverbindungen nutzen eine `external_id` (Hash aus IBAN/Datum/Betrag/Verwendungszweck bei FinTS, Transaktions-ID bei PayPal) - **Beleg-Zuordnung**: jede Banktransaktion kann einem oder mehreren Belegen zugeordnet werden; der zugeordnete Betrag wird separat erfasst diff --git a/migrations/087_bank_sources_last_balance.sql b/migrations/087_bank_sources_last_balance.sql new file mode 100644 index 0000000..0a41aec --- /dev/null +++ b/migrations/087_bank_sources_last_balance.sql @@ -0,0 +1,2 @@ +ALTER TABLE bank_sources + ADD COLUMN IF NOT EXISTS last_balance DECIMAL(14,2) NULL DEFAULT NULL AFTER last_fetched_at; diff --git a/src/Models/BankSource.php b/src/Models/BankSource.php index 366cb5c..3cd34b1 100644 --- a/src/Models/BankSource.php +++ b/src/Models/BankSource.php @@ -85,10 +85,23 @@ class BankSource Database::execute('DELETE FROM bank_sources WHERE id = ?', [$id]); } - public static function updateLastFetched(int $id): void + public static function updateLastFetched(int $id, ?float $balance = null): void { // Setzt zugleich fetch_range auf den Default zurück: einmalige Sonderauswahl // (z.B. "letzte Woche") gilt nur für genau einen Abruf. + // last_balance wird nur überschrieben, wenn der Abruf einen Kontostand geliefert + // hat – sonst bleibt der zuletzt bekannte Stand erhalten. + if ($balance !== null) { + Database::execute( + "UPDATE bank_sources + SET last_fetched_at = NOW(), + fetch_range = 'last_fetched', + last_balance = ? + WHERE id = ?", + [$balance, $id] + ); + return; + } Database::execute( "UPDATE bank_sources SET last_fetched_at = NOW(), diff --git a/src/Services/FinTsFetchService.php b/src/Services/FinTsFetchService.php index 93190fd..74978c1 100644 --- a/src/Services/FinTsFetchService.php +++ b/src/Services/FinTsFetchService.php @@ -205,6 +205,7 @@ class FinTsFetchService $totalNew = 0; $totalSkip = 0; + $balance = null; foreach ($accounts as $account) { $iban = $account->getIban(); @@ -224,6 +225,16 @@ class FinTsFetchService continue; } + $statements = $soa->getStatements(); + if (!empty($statements)) { + $lastStatement = end($statements); + $accountBalance = $lastStatement->getEndBalance() ?? $lastStatement->getStartBalance(); + if ($accountBalance !== null) { + $balance = ($balance ?? 0.0) + $accountBalance; + $log(' → Kontostand: ' . number_format($accountBalance, 2, ',', '.') . ' €'); + } + } + $accountNew = 0; foreach ($soa->getStatements() as $statement) { foreach ($statement->getTransactions() as $tx) { @@ -276,7 +287,7 @@ class FinTsFetchService } $this->updateBatchCount($batchId, $totalNew); - $this->updateLastFetched(); + $this->updateLastFetched($balance); $log(sprintf('Fertig. %d neu importiert, %d bereits vorhanden.', $totalNew, $totalSkip)); } @@ -533,9 +544,9 @@ class FinTsFetchService ); } - private function updateLastFetched(): void + private function updateLastFetched(?float $balance): void { - BankSource::updateLastFetched($this->sourceId); + BankSource::updateLastFetched($this->sourceId, $balance); } private function externalIdExists(string $externalId): bool diff --git a/src/Services/PayPalNvpFetchService.php b/src/Services/PayPalNvpFetchService.php index b1c39ca..665389d 100644 --- a/src/Services/PayPalNvpFetchService.php +++ b/src/Services/PayPalNvpFetchService.php @@ -19,6 +19,7 @@ class PayPalNvpFetchService private string $signature; private int $sourceId; private array $source; + private ?float $lastBalance = null; /** @var callable */ private $log; @@ -311,6 +312,9 @@ class PayPalNvpFetchService } return false; } + if (isset($result['AMT']) && is_numeric($result['AMT'])) { + $this->lastBalance = (float)$result['AMT']; + } return true; } @@ -422,7 +426,7 @@ class PayPalNvpFetchService private function updateLastFetched(): void { - BankSource::updateLastFetched($this->sourceId); + BankSource::updateLastFetched($this->sourceId, $this->lastBalance); } private function externalIdExists(string $externalId): bool diff --git a/src/Views/bank/fints.php b/src/Views/bank/fints.php index b6f7433..cd470c3 100644 --- a/src/Views/bank/fints.php +++ b/src/Views/bank/fints.php @@ -85,9 +85,22 @@ $csvDateFormatOptions = [ + +
+
+
Kontostand
+
+ Abruf: +
+
+ +
+
+
Abruf: ' ?>
+