Fix: FinTS-Kontostand über dedizierte GetBalance-Abfrage statt Statement-Saldo

This commit is contained in:
fds2610-bot 2026-07-21 18:56:32 +02:00
parent 2b93d38513
commit ff4e0aac31
3 changed files with 30 additions and 7 deletions

View file

@ -6,8 +6,8 @@
- **`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/Services/FinTsFetchService.php`:** Fragt pro Konto zusätzlich per dedizierter `GetBalance`-Aktion (HKSAL) den aktuellen gebuchten Saldo ab und summiert diesen. Fällt nur auf den Schlusssaldo des letzten Statements zurück, wenn die Saldenabfrage fehlschlägt – der ursprüngliche Ansatz (Saldo nur aus dem Statement ableiten) lieferte keinen Wert, wenn die Bank mangels neuer Buchungen kein MT940/CAMT-Statement zurückgab (häufigster Fall bei täglichen Abrufen ohne neue Umsätze).
- **`src/Services/PayPalNvpFetchService.php`:** Der bereits vorhandene `GetBalance`-Aufruf in `testCredentials()` liefert den aktuellen Kontostand (`AMT`), der nun mitgespeichert und im Abruf-Log ausgegeben 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.

View file

@ -7,6 +7,7 @@ namespace Steuerkram\Services;
use Steuerkram\Database;
use Steuerkram\Models\BankSource;
use Fhp\Action\GetBalance;
use Fhp\Action\GetSEPAAccounts;
use Fhp\Action\GetStatementOfAccount;
use Fhp\BaseAction;
@ -225,14 +226,33 @@ class FinTsFetchService
continue;
}
$accountBalance = null;
try {
$balAction = GetBalance::create($account);
$fints->execute($balAction);
if ($balAction->needsTan()) {
if (!$this->handleStrongAuthentication($balAction, $iban . ' (Saldo)')) {
throw new \RuntimeException('TAN für Saldenabfrage nicht bestätigt.');
}
}
foreach ($balAction->getBalances() as $hisal) {
$accountBalance = ($accountBalance ?? 0.0) + $hisal->getGebuchterSaldo()->getAmount();
}
} catch (\Throwable $e) {
$log(' → Saldenabfrage fehlgeschlagen: ' . $e->getMessage());
}
// Fallback: Saldo aus dem letzten Statement ableiten, falls die dedizierte
// Saldenabfrage (HKSAL) fehlschlägt oder von der Bank nicht unterstützt wird.
$statements = $soa->getStatements();
if (!empty($statements)) {
if ($accountBalance === null && !empty($statements)) {
$lastStatement = end($statements);
$accountBalance = $lastStatement->getEndBalance() ?? $lastStatement->getStartBalance();
if ($accountBalance !== null) {
$balance = ($balance ?? 0.0) + $accountBalance;
$log(' → Kontostand: ' . number_format($accountBalance, 2, ',', '.') . ' €');
}
}
if ($accountBalance !== null) {
$balance = ($balance ?? 0.0) + $accountBalance;
$log(' → Kontostand: ' . number_format($accountBalance, 2, ',', '.') . ' €');
}
$accountNew = 0;

View file

@ -314,6 +314,9 @@ class PayPalNvpFetchService
}
if (isset($result['AMT']) && is_numeric($result['AMT'])) {
$this->lastBalance = (float)$result['AMT'];
($this->log)('Kontostand: ' . number_format($this->lastBalance, 2, ',', '.') . ' ' . ($result['CURRENCYCODE'] ?? 'EUR'));
} else {
($this->log)('Kontostand: nicht in GetBalance-Antwort enthalten (Felder: ' . implode(', ', array_keys($result)) . ')');
}
return true;
}