From e95fc7302dcc597c6b6fc85a643b237ad0f76639 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Tue, 6 Oct 2026 15:01:24 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20#117:=20Nicht=20freigegebene=20=C3=9Cberw?= =?UTF-8?q?eisungen=20nachvollziehbar=20machen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Status „wartet auf Freigabe“, solange die Bank auf TAN/Namensabgleich wartet - Abbruch der Freigabe (auch durch neuen Sendeversuch) im Übermittlungsprotokoll als „abgebrochen“ festhalten, Auftrag als fehlgeschlagen vermerken - Aufträge mit Übermittlungsprotokoll nur stornieren, nicht löschen - Migration 025, PHP-Test 57, E2E-Spec zahlungsfreigabe Co-Authored-By: Claude Opus 5.5 --- Changes.md | 9 + docs/banking.md | 13 +- migrations/025_freigabe_nachvollziehen.sql | 15 ++ public/static/app.css | 2 + src/Controllers/ZahlungsverkehrController.php | 11 +- src/Models/Bankprotokoll.php | 7 +- src/Models/Zahlungsauftrag.php | 35 +++- src/Services/E2EAufraeumer.php | 1 + src/Services/E2EGrundbestand.php | 27 +++ src/Services/FinTsUeberweisung.php | 56 +++++- src/Services/Finanzkalender.php | 2 +- src/Views/zahlungsverkehr/formular.php | 18 +- src/Views/zahlungsverkehr/index.php | 11 +- tests/57_FreigabeNachvollziehenTest.php | 175 ++++++++++++++++++ tests/e2e/specs/zahlungsfreigabe.spec.js | 38 ++++ 15 files changed, 404 insertions(+), 16 deletions(-) create mode 100644 migrations/025_freigabe_nachvollziehen.sql create mode 100644 tests/57_FreigabeNachvollziehenTest.php create mode 100644 tests/e2e/specs/zahlungsfreigabe.spec.js diff --git a/Changes.md b/Changes.md index 0084957..00e796d 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,14 @@ # Änderungen +## Zahlungsverkehr: nicht freigegebene Überweisungen nachvollziehbar (06.10.2026) + +- Anlass (#117): Die comdirect begründete zwei nicht ausgeführte Überweisungen damit, dass eine TAN angefordert, aber nicht verwendet wurde – im Finanzmanager war das nicht nachprüfbar. +- Verlangt die Bank eine Freigabe, steht der Auftrag auf „wartet auf Freigabe“ statt auf „Entwurf“; die Liste nennt, ob die TAN-Eingabe noch offen oder nicht abgeschlossen ist. +- Abbruch der Freigabe – auch durch einen neuen Sendeversuch – erzeugt einen Eintrag „Freigabe abgebrochen“ im Übermittlungsprotokoll und vermerkt den Auftrag als fehlgeschlagen. +- Aufträge mit Übermittlungsprotokoll lassen sich nicht mehr löschen, nur stornieren (Knopf im Formular). +- Migration `025_freigabe_nachvollziehen.sql`; E2E-Grundbestand enthält eine wartende Überweisung, der Aufräumer räumt `bank_protokolle` des Testnutzers mit ab. +- Tests: `tests/57_FreigabeNachvollziehenTest.php`, `tests/e2e/specs/zahlungsfreigabe.spec.js`. + ## TAN-Anfrage: Startcode hervorheben (04.10.2026) - Nennt die Bank bei der TAN-Abfrage einen Startcode für den TAN-Generator, geht er im Fließtext leicht unter. Erkennt der Finanzmanager einen hinterlegten Beispielsatz (jede Ziffernfolge darin gilt als Platzhalter für den Startcode), zeigt er den gefundenen Code fett, zentriert und in eigener Zeile. diff --git a/docs/banking.md b/docs/banking.md index 4347322..b3f0863 100644 --- a/docs/banking.md +++ b/docs/banking.md @@ -519,10 +519,21 @@ Zeile ankommt. Eine Überweisung mit TAN-Freigabe hat mehrere Durchläufe. Zu sehen ist das Protokoll an zwei Stellen: im Zahlungsverkehr über den Status der Überweisung (modaler Dialog) und als Gesamtprotokoll unter *Verwaltung › Online-Banking › Protokoll* (`/banking/protokoll`), filterbar nach Bank, Zeitraum und Freitext. Es wird nie -automatisch gelöscht; wird ein Zahlungsauftrag entfernt, bleibt sein Protokoll stehen. +automatisch gelöscht. Ein Zahlungsauftrag mit Protokoll lässt sich nicht mehr löschen, nur +stornieren – er bleibt als Beleg stehen. `tests/42_UebermittlungsprotokollTest.php` prüft Mitschnitt, Zuordnung zur Überweisung, den Fehlerfall und die Filter. +**Nicht erteilte Freigabe.** Verlangt die Bank eine TAN oder die Bestätigung des +Namensabgleichs, trägt der Auftrag bis dahin den Status `freigabe` („wartet auf Freigabe“) – +die Bank führt ihn ohne Freigabe nicht aus. Bricht der Nutzer ab oder löst ein neuer +Sendeversuch den wartenden Dialog ab, schreibt `FinTsUeberweisung::abbrechen()` einen +Protokolleintrag mit Status `abgebrochen` und vermerkt den Auftrag als fehlgeschlagen. Verfällt +nur die Sitzung, bleibt der Auftrag auf „wartet auf Freigabe“ stehen und die Liste weist darauf +hin, dass die Freigabe nicht abgeschlossen ist. So lässt sich eine Auskunft der Bank wie „TAN +angefordert, aber nicht verwendet“ am Protokoll prüfen. `tests/57_FreigabeNachvollziehenTest.php` +und `tests/e2e/specs/zahlungsfreigabe.spec.js`. + ## Tests `tests/16_BankingTest.php` prüft alles um den Dialog herum: Verschlüsselung (Kennung, kein diff --git a/migrations/025_freigabe_nachvollziehen.sql b/migrations/025_freigabe_nachvollziehen.sql new file mode 100644 index 0000000..625bf42 --- /dev/null +++ b/migrations/025_freigabe_nachvollziehen.sql @@ -0,0 +1,15 @@ +-- Nicht freigegebene Überweisungen nachvollziehbar machen (#117). +-- +-- Ein Zahlungsauftrag, für den die Bank eine TAN verlangt hat, trägt bis zur +-- Freigabe den Status „freigabe“ – er sieht dann nicht mehr aus wie ein nie +-- gesendeter Entwurf. Bricht der Nutzer die Freigabe ab, hält das +-- Übermittlungsprotokoll das mit dem Status „abgebrochen“ fest. +-- +-- MODIFY mit vollständiger Werteliste ist wiederholbar und damit idempotent. + +ALTER TABLE zahlungsauftraege + MODIFY COLUMN status ENUM('entwurf','freigabe','gesendet','fehler','storniert') + NOT NULL DEFAULT 'entwurf'; + +ALTER TABLE bank_protokolle + MODIFY COLUMN status ENUM('erfolg','fehler','offen','abgebrochen') NOT NULL; diff --git a/public/static/app.css b/public/static/app.css index 8c9f3a4..26ffc4f 100644 --- a/public/static/app.css +++ b/public/static/app.css @@ -2351,6 +2351,8 @@ table.liste tr.luecke td { } .marke-entwurf { color: var(--warnung); border-color: var(--warnung); } +.marke-freigabe { color: var(--warnung); border-color: var(--warnung); } +.marke-abgebrochen { color: var(--negativ); border-color: var(--negativ); } .marke-gesendet { color: var(--positiv); border-color: var(--positiv); } .marke-storniert { color: var(--text-gedaempft); } diff --git a/src/Controllers/ZahlungsverkehrController.php b/src/Controllers/ZahlungsverkehrController.php index eafdbee..9311578 100644 --- a/src/Controllers/ZahlungsverkehrController.php +++ b/src/Controllers/ZahlungsverkehrController.php @@ -31,6 +31,7 @@ class ZahlungsverkehrController extends BaseController 'status' => $status, 'offen' => Zahlungsauftrag::anzahlOffen(), 'wartetTan' => FinTsUeberweisung::wartetAufTan(), + 'wartender' => FinTsUeberweisung::wartenderAuftrag(), ]); } @@ -145,8 +146,9 @@ class ZahlungsverkehrController extends BaseController } $this->render('zahlungsverkehr/formular.php', [ - 'titel' => 'Überweisung an ' . $auftrag['empfaenger'], - 'auftrag' => $auftrag, + 'titel' => 'Überweisung an ' . $auftrag['empfaenger'], + 'auftrag' => $auftrag, + 'hatProtokoll' => Zahlungsauftrag::hatProtokoll((int)$auftrag['id']), 'konten' => $konten, 'adressen' => Adresse::alle(), 'kategorien' => Kategorie::baumFlach(null, true), @@ -260,7 +262,10 @@ class ZahlungsverkehrController extends BaseController FinTsUeberweisung::abbrechen(); - View::redirect('/zahlungsverkehr', 'Vorgang abgebrochen. Der Auftrag bleibt als Entwurf stehen.'); + View::redirect( + '/zahlungsverkehr', + 'Freigabe abgebrochen. Die Bank führt den Auftrag nicht aus – er kann erneut gesendet werden.' + ); } /** @param array $ergebnis */ diff --git a/src/Models/Bankprotokoll.php b/src/Models/Bankprotokoll.php index 68a104d..ffb707d 100644 --- a/src/Models/Bankprotokoll.php +++ b/src/Models/Bankprotokoll.php @@ -24,9 +24,10 @@ class Bankprotokoll ]; public const STATUS = [ - 'erfolg' => 'erfolgreich', - 'fehler' => 'fehlgeschlagen', - 'offen' => 'wartet auf Freigabe', + 'erfolg' => 'erfolgreich', + 'fehler' => 'fehlgeschlagen', + 'offen' => 'wartet auf Freigabe', + 'abgebrochen' => 'Freigabe abgebrochen', ]; public static function speichern( diff --git a/src/Models/Zahlungsauftrag.php b/src/Models/Zahlungsauftrag.php index 5726dd6..1ba3905 100644 --- a/src/Models/Zahlungsauftrag.php +++ b/src/Models/Zahlungsauftrag.php @@ -20,6 +20,7 @@ class Zahlungsauftrag { public const STATUS = [ 'entwurf' => 'Entwurf', + 'freigabe' => 'wartet auf Freigabe', 'gesendet' => 'gesendet', 'fehler' => 'fehlgeschlagen', 'storniert' => 'storniert', @@ -109,6 +110,12 @@ class Zahlungsauftrag if ($auftrag !== null && $auftrag['status'] === 'gesendet') { throw new \RuntimeException('Ein gesendeter Auftrag bleibt als Beleg stehen.'); } + if ($auftrag !== null && self::hatProtokoll($id)) { + // Was die Bank gesehen hat, bleibt nachweisbar: stornieren ja, löschen nein. + throw new \RuntimeException( + 'Zu diesem Auftrag gibt es ein Übermittlungsprotokoll – er bleibt als Beleg stehen und kann nur storniert werden.' + ); + } Database::execute('DELETE FROM zahlungsauftraege WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]); } @@ -159,11 +166,35 @@ class Zahlungsauftrag }); } + /** Hat der Auftrag die Bank schon einmal erreicht? Dann gibt es ein Protokoll. */ + public static function hatProtokoll(int $id): bool + { + return (int)Database::fetchValue( + 'SELECT COUNT(*) FROM bank_protokolle WHERE auftrag_id = ? AND benutzer_id = ?', + [$id, Mandant::id()], + 0 + ) > 0; + } + + /** + * Die Bank hat den Auftrag entgegengenommen, führt ihn aber erst nach der + * Freigabe (TAN, Namensabgleich) aus. Bis dahin ist er weder Entwurf noch + * gesendet. + */ + public static function alsWartendVermerken(int $id, string $meldung): void + { + Database::execute( + "UPDATE zahlungsauftraege SET status = 'freigabe', meldung = ? + WHERE id = ? AND benutzer_id = ? AND status <> 'gesendet'", + [mb_substr($meldung, 0, 2000), $id, Mandant::id()] + ); + } + public static function alsFehlerVermerken(int $id, string $meldung): void { Database::execute( "UPDATE zahlungsauftraege SET status = 'fehler', meldung = ? - WHERE id = ? AND benutzer_id = ?", + WHERE id = ? AND benutzer_id = ? AND status <> 'gesendet'", [mb_substr($meldung, 0, 2000), $id, Mandant::id()] ); } @@ -171,7 +202,7 @@ class Zahlungsauftrag public static function anzahlOffen(): int { return (int)Database::fetchValue( - "SELECT COUNT(*) FROM zahlungsauftraege WHERE benutzer_id = ? AND status = 'entwurf'", + "SELECT COUNT(*) FROM zahlungsauftraege WHERE benutzer_id = ? AND status IN ('entwurf', 'freigabe')", [Mandant::id()], 0 ); diff --git a/src/Services/E2EAufraeumer.php b/src/Services/E2EAufraeumer.php index 80b3f24..d060d3f 100644 --- a/src/Services/E2EAufraeumer.php +++ b/src/Services/E2EAufraeumer.php @@ -56,6 +56,7 @@ class E2EAufraeumer 'vertraege', 'sparziele', 'kredite', + 'bank_protokolle', 'zahlungsauftraege', 'bank_umsaetze', 'bank_abrufe', diff --git a/src/Services/E2EGrundbestand.php b/src/Services/E2EGrundbestand.php index d0e80b4..493858c 100644 --- a/src/Services/E2EGrundbestand.php +++ b/src/Services/E2EGrundbestand.php @@ -6,12 +6,15 @@ namespace Finanzmanager\Services; use Finanzmanager\Database; use Finanzmanager\Mandant; +use Finanzmanager\Models\Bankprotokoll; +use Finanzmanager\Models\Bankzugang; use Finanzmanager\Models\Depotbuchung; use Finanzmanager\Models\Buchung; use Finanzmanager\Models\Kategorie; use Finanzmanager\Models\Klasse; use Finanzmanager\Models\Konto; use Finanzmanager\Models\Wertpapier; +use Finanzmanager\Models\Zahlungsauftrag; /** * Legt den Grundbestand an, auf dem die E2E-Tests aufsetzen. @@ -35,6 +38,9 @@ use Finanzmanager\Models\Wertpapier; * - **Ein Vorjahr.** Der Vergleichsbericht stellt zwei Zeiträume nebeneinander. * - **Ein Depot mit Wertpapier und Geschäft.** `/wertpapiere` und * `/wertpapiere/buchungen` sind sonst leere Listen ohne Zahlenspalten. + * - **Eine Überweisung, die auf die Freigabe wartet.** Mit Übermittlungsprotokoll, + * damit der Zahlungsverkehr Status, Hinweis und Stornieren statt Löschen zeigt. + * Die Bankverbindung zeigt ins Leere; gesendet wird nichts. */ class E2EGrundbestand { @@ -135,12 +141,33 @@ class E2EGrundbestand ]); $buchungen++; + $zugang = Bankzugang::anlegen([ + 'name' => self::PRAEFIX . ' Bank', + 'bankname' => self::PRAEFIX . ' Bank AG', + 'blz' => '12030000', + 'fints_url' => 'https://banking.example.org/fints', + 'benutzerkennung' => 'e2e', + 'pin' => 'e2e-pin', + 'abrufzeitraum' => 'letzter_abruf', + 'aktiv' => 1, + ]); + $auftrag = Zahlungsauftrag::anlegen([ + 'konto_id' => $konten[0], + 'zugang_id' => $zugang, + 'empfaenger' => self::PRAEFIX . ' Finanzamt', + 'iban' => 'DE02120300000000202051', + 'betrag' => 4715.67, + ]); + Bankprotokoll::speichern($zugang, 'ueberweisung', 'offen', 'Die Bank verlangt eine Freigabe.', '> HKTAN', $auftrag); + Zahlungsauftrag::alsWartendVermerken($auftrag, 'Die Bank verlangt eine Freigabe.'); + return [ 'konten' => count($konten) + 1, 'kategorien' => count($kategorien), 'klassen' => 1, 'wertpapiere' => 1, 'buchungen' => $buchungen, + 'zahlungsauftraege' => 1, ]; }); } diff --git a/src/Services/FinTsUeberweisung.php b/src/Services/FinTsUeberweisung.php index 2df6169..bd6d918 100644 --- a/src/Services/FinTsUeberweisung.php +++ b/src/Services/FinTsUeberweisung.php @@ -109,7 +109,9 @@ class FinTsUeberweisung return ['status' => 'fehler', 'meldung' => 'Die Bankverbindung gibt es nicht.']; } - self::vergessen(); + // Wartet noch ein anderer Durchlauf auf seine Freigabe, verfällt er jetzt – + // das soll im Protokoll stehen und nicht stillschweigend passieren. + self::abbrechen('Abgelöst durch einen neuen Sendeversuch, bevor die Freigabe erteilt war.'); return (new self($zugang, $auftragId, ['schritt' => 'anmelden']))->lauf(null); } @@ -171,6 +173,14 @@ class FinTsUeberweisung return self::zustandLesen() !== null; } + /** Der Auftrag, dessen Freigabe in dieser Sitzung gerade aussteht. */ + public static function wartenderAuftrag(): ?int + { + $auftragId = (int)(self::zustandLesen()['auftrag_id'] ?? 0); + + return $auftragId === 0 ? null : $auftragId; + } + /** @return array|null */ public static function offeneTanAnfrage(): ?array { @@ -179,9 +189,47 @@ class FinTsUeberweisung return $zustand === null ? null : ($zustand['tan'] ?? null); } - public static function abbrechen(): void + /** + * Verwirft eine ausstehende Freigabe. + * + * Die Bank hat den Auftrag dann zwar gesehen und eine TAN angefordert, führt + * ihn ohne Freigabe aber nicht aus. Genau das muss sich später nachweisen + * lassen – deshalb ein Eintrag im Übermittlungsprotokoll und eine Meldung am + * Auftrag, statt den Zustand nur aus der Sitzung zu löschen. + */ + public static function abbrechen(string $grund = 'Vom Nutzer abgebrochen.'): void { + $zustand = self::zustandLesen(); self::vergessen(); + + $auftragId = (int)($zustand['auftrag_id'] ?? 0); + if ($auftragId === 0) { + return; + } + + $meldung = 'Freigabe nicht erteilt – keine TAN eingereicht, die Bank führt den Auftrag nicht aus. ' + . $grund; + + try { + $mitschnitt = new BankProtokollLogger(); + $mitschnitt->vermerke(sprintf( + 'Ausstehender Schritt: %s', + ($zustand['schritt'] ?? '') === 'vop' ? 'Namensabgleich bestätigen' : 'TAN eingeben' + )); + $mitschnitt->vermerke('Ergebnis: ' . $meldung); + + Bankprotokoll::speichern( + (int)($zustand['zugang_id'] ?? 0), + 'ueberweisung', + 'abgebrochen', + $meldung, + $mitschnitt->text(), + $auftragId + ); + Zahlungsauftrag::alsFehlerVermerken($auftragId, $meldung); + } catch (\Throwable $e) { + DebugLog::fehler($e); + } } // ── Ablauf ─────────────────────────────────────────────────────────────── @@ -218,6 +266,10 @@ class FinTsUeberweisung }; $this->mitschnitt->vermerke('Ergebnis: ' . $ergebnis['meldung']); + if ($status === 'offen') { + Zahlungsauftrag::alsWartendVermerken($this->auftragId, (string)$ergebnis['meldung']); + } + try { Bankprotokoll::speichern( (int)$this->zugang['id'], diff --git a/src/Services/Finanzkalender.php b/src/Services/Finanzkalender.php index 3d083c9..8d78053 100644 --- a/src/Services/Finanzkalender.php +++ b/src/Services/Finanzkalender.php @@ -87,7 +87,7 @@ class Finanzkalender ]; } - foreach (Zahlungsauftrag::alle('entwurf') as $auftrag) { + foreach ([...Zahlungsauftrag::alle('entwurf'), ...Zahlungsauftrag::alle('freigabe')] as $auftrag) { $datum = (string)($auftrag['ausfuehrung_am'] ?? ''); if ($datum === '' || $datum > $bis) { continue; diff --git a/src/Views/zahlungsverkehr/formular.php b/src/Views/zahlungsverkehr/formular.php index 2fa4aab..9eae25f 100644 --- a/src/Views/zahlungsverkehr/formular.php +++ b/src/Views/zahlungsverkehr/formular.php @@ -10,6 +10,7 @@ use Finanzmanager\View; * @var list> $kategorien * @var list> $klassen * @var string $csrfToken + * @var bool|null $hatProtokoll */ $neu = $auftrag === null; @@ -170,12 +171,25 @@ include __DIR__ . '/../layout/kopf.php'; - + + + +
+ +
+ +
+

Zu diesem Auftrag gibt es ein Übermittlungsprotokoll – er wird + nicht gelöscht, sondern bleibt storniert als Beleg stehen.

+
+ +
- +
diff --git a/src/Views/zahlungsverkehr/index.php b/src/Views/zahlungsverkehr/index.php index 92d7bab..7f1a32d 100644 --- a/src/Views/zahlungsverkehr/index.php +++ b/src/Views/zahlungsverkehr/index.php @@ -7,6 +7,7 @@ use Finanzmanager\View; /** * @var list> $auftraege * @var string $status + * @var int|null $wartender * @var int $offen * @var bool $wartetTan * @var string $csrfToken @@ -39,7 +40,7 @@ include __DIR__ . '/../layout/kopf.php';

Eine Überweisung wird erst gebucht, wenn die Bank sie angenommen hat – vorher steht sie - als Entwurf hier. Ohne Ausführungstermin geht sie sofort hinaus, mit Termin als + als Entwurf hier, nach der TAN-Anforderung als „wartet auf Freigabe“. Ohne Ausführungstermin geht sie sofort hinaus, mit Termin als Terminüberweisung, als Echtzeitüberweisung binnen Sekunden.

@@ -88,7 +89,13 @@ include __DIR__ . '/../layout/kopf.php'; - + + + TAN jetzt eingeben – bis dahin führt die Bank den Auftrag nicht aus.' + : 'Freigabe nicht abgeschlossen – die Bank führt den Auftrag ohne TAN nicht aus. Erneut senden.' ?> + + diff --git a/tests/57_FreigabeNachvollziehenTest.php b/tests/57_FreigabeNachvollziehenTest.php new file mode 100644 index 0000000..d71c401 --- /dev/null +++ b/tests/57_FreigabeNachvollziehenTest.php @@ -0,0 +1,175 @@ +mandantAnlegen(); + $this->stammdaten(); + + $this->einWartenderAuftragIstKeinEntwurf(); + $this->derAbbruchStehtImProtokoll(); + $this->einNeuerSendeversuchLoestDieFreigabeAbUndProtokolliert(); + $this->ohneWartendeFreigabePassiertBeimAbbruchNichts(); + $this->einAuftragMitProtokollWirdNichtGeloescht(); + } + + private function stammdaten(): void + { + $this->giro = Konto::anlegen([ + 'name' => 'Freigabe-Giro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1, + ]); + + $this->zugang = Bankzugang::anlegen([ + 'name' => 'Freigabe-Bank', + 'bankname' => 'Freigabe-Bank AG', + 'blz' => '12030000', + 'fints_url' => 'https://banking.example.org/fints', + 'benutzerkennung' => 'testkunde', + 'pin' => 'geheim1234', + 'abrufzeitraum' => 'letzter_abruf', + 'aktiv' => 1, + ]); + + // Auf dem eigenen Rechner lauscht niemand – ein echter Versuch scheitert sofort. + Database::execute( + 'UPDATE bank_zugaenge SET fints_url = ? WHERE id = ?', + ['http://127.0.0.1:9/fints', $this->zugang] + ); + } + + private function auftrag(): int + { + return Zahlungsauftrag::anlegen([ + 'konto_id' => $this->giro, + 'zugang_id' => $this->zugang, + 'empfaenger' => 'Finanzamt Musterstadt', + 'iban' => 'DE02120300000000202051', + 'betrag' => 4715.67, + ]); + } + + /** Legt einen Dialog in die Sitzung, der auf die TAN zum Auftrag wartet. */ + private function tanAngefordert(int $auftragId): void + { + Bankprotokoll::speichern($this->zugang, 'ueberweisung', 'offen', 'Die Bank verlangt eine Freigabe.', '> HKTAN', $auftragId); + Zahlungsauftrag::alsWartendVermerken($auftragId, 'Die Bank verlangt eine Freigabe.'); + + Auth::startSession(); + $_SESSION[FinTsUeberweisung::SITZUNG] = [ + 'schritt' => 'tan', + 'zugang_id' => $this->zugang, + 'auftrag_id' => $auftragId, + 'tan' => ['bezeichnung' => 'Überweisung'], + ]; + } + + private function einWartenderAuftragIstKeinEntwurf(): void + { + $auftragId = $this->auftrag(); + $this->tanAngefordert($auftragId); + + $this->assertEquals('freigabe', Zahlungsauftrag::find($auftragId)['status'], 'Nach der TAN-Anforderung wartet der Auftrag auf Freigabe'); + $this->assertEquals($auftragId, FinTsUeberweisung::wartenderAuftrag(), 'Die Sitzung kennt den wartenden Auftrag'); + $this->assertEquals(1, count(Zahlungsauftrag::alle('freigabe')), 'Der Reiter „wartet auf Freigabe“ zeigt ihn'); + $this->assertGreaterThan(0, Zahlungsauftrag::anzahlOffen(), 'Er zählt zu den offenen Aufträgen'); + + FinTsUeberweisung::abbrechen(); + } + + private function derAbbruchStehtImProtokoll(): void + { + $auftragId = $this->auftrag(); + $this->tanAngefordert($auftragId); + + FinTsUeberweisung::abbrechen(); + + $this->assertNull(FinTsUeberweisung::wartenderAuftrag(), 'Nach dem Abbruch wartet nichts mehr'); + + $auftrag = Zahlungsauftrag::find($auftragId); + $this->assertEquals('fehler', $auftrag['status'], 'Der abgebrochene Auftrag gilt als nicht ausgeführt'); + $this->assert(str_contains((string)$auftrag['meldung'], 'keine TAN eingereicht'), 'Die Meldung nennt die fehlende TAN'); + + $eintraege = Bankprotokoll::fuerAuftrag($auftragId); + $this->assertEquals(2, count($eintraege), 'TAN-Anforderung und Abbruch stehen im Protokoll'); + $this->assertEquals('abgebrochen', $eintraege[1]['status'], 'Der Abbruch trägt den Status „abgebrochen“'); + $this->assert(str_contains((string)$eintraege[1]['protokoll'], 'TAN eingeben'), 'Das Protokoll nennt den offenen Schritt'); + $this->assert(str_contains(Bankprotokoll::nachweis($auftrag), 'Freigabe abgebrochen'), 'Der Nachweis zur Überweisung zeigt den Abbruch'); + } + + private function einNeuerSendeversuchLoestDieFreigabeAbUndProtokolliert(): void + { + $alt = $this->auftrag(); + $this->tanAngefordert($alt); + + $neu = $this->auftrag(); + $ergebnis = FinTsUeberweisung::senden($neu); + + $this->assertEquals('fehler', $ergebnis['status'], 'Ohne erreichbare Bank scheitert der neue Versuch'); + + $eintraege = Bankprotokoll::fuerAuftrag($alt); + $this->assertEquals('abgebrochen', end($eintraege)['status'], 'Die verdrängte Freigabe steht als abgebrochen im Protokoll'); + $this->assert( + str_contains((string)end($eintraege)['meldung'], 'neuen Sendeversuch'), + 'Das Protokoll nennt den Grund' + ); + $this->assertEquals('fehler', Zahlungsauftrag::find($alt)['status'], 'Der verdrängte Auftrag wartet nicht mehr'); + } + + private function ohneWartendeFreigabePassiertBeimAbbruchNichts(): void + { + $vorher = (int)Database::fetchValue('SELECT COUNT(*) FROM bank_protokolle', [], 0); + + FinTsUeberweisung::abbrechen(); + + $nachher = (int)Database::fetchValue('SELECT COUNT(*) FROM bank_protokolle', [], 0); + $this->assertEquals($vorher, $nachher, 'Ohne wartende Freigabe entsteht kein Protokolleintrag'); + } + + private function einAuftragMitProtokollWirdNichtGeloescht(): void + { + $mitProtokoll = $this->auftrag(); + $this->tanAngefordert($mitProtokoll); + FinTsUeberweisung::abbrechen(); + + $abgelehnt = false; + try { + Zahlungsauftrag::loeschen($mitProtokoll); + } catch (\RuntimeException) { + $abgelehnt = true; + } + $this->assert($abgelehnt, 'Ein Auftrag mit Übermittlungsprotokoll lässt sich nicht löschen'); + $this->assert(Zahlungsauftrag::find($mitProtokoll) !== null, 'Er bleibt als Beleg stehen'); + + Zahlungsauftrag::stornieren($mitProtokoll); + $this->assertEquals('storniert', Zahlungsauftrag::find($mitProtokoll)['status'], 'Stornieren geht weiterhin'); + + $ohneProtokoll = $this->auftrag(); + Zahlungsauftrag::loeschen($ohneProtokoll); + $this->assertNull(Zahlungsauftrag::find($ohneProtokoll), 'Ein nie gesendeter Entwurf lässt sich weiterhin löschen'); + } +} diff --git a/tests/e2e/specs/zahlungsfreigabe.spec.js b/tests/e2e/specs/zahlungsfreigabe.spec.js new file mode 100644 index 0000000..9a45f55 --- /dev/null +++ b/tests/e2e/specs/zahlungsfreigabe.spec.js @@ -0,0 +1,38 @@ +// @ts-check +const { test, expect } = require('@playwright/test') + +const BENUTZER = process.env.E2E_USER || 'e2e' +const PASSWORT = process.env.E2E_PASSWORT || process.env.E2E_PASSWORD || 'e2e-testpasswort' + +// Der Grundbestand (E2EGrundbestand) legt eine Überweisung an, für die die Bank +// eine TAN angefordert hat, die nie eingereicht wurde (#117). +const EMPFAENGER = 'E2E Grundbestand Finanzamt' + +async function anmelden(page) { + await page.goto('/login') + await page.fill('#benutzername', BENUTZER) + await page.fill('#passwort', PASSWORT) + await page.click('button[type="submit"]') + await expect(page).toHaveURL(/\/$/) +} + +test.describe('Zahlungsverkehr: nicht freigegebene Überweisung', () => { + test.beforeEach(async ({ page }) => { + await anmelden(page) + }) + + test('Liste zeigt „wartet auf Freigabe“ mit Hinweis', async ({ page }) => { + await page.goto('/zahlungsverkehr?status=freigabe') + const zeile = page.locator('table.liste tr', { hasText: EMPFAENGER }) + await expect(zeile).toHaveCount(1) + await expect(zeile.locator('.marke-freigabe')).toHaveText('wartet auf Freigabe') + await expect(zeile).toContainText('die Bank führt den Auftrag ohne TAN nicht aus') + }) + + test('Auftrag mit Übermittlungsprotokoll: Stornieren statt Löschen', async ({ page }) => { + await page.goto('/zahlungsverkehr?status=freigabe') + await page.locator('table.liste tr', { hasText: EMPFAENGER }).getByRole('link', { name: EMPFAENGER }).click() + await expect(page.locator('#auftrag-stornieren')).toBeVisible() + await expect(page.locator('#auftrag-loeschen')).toHaveCount(0) + }) +})