Finanzmanager/tests/57_FreigabeNachvollziehenTest.php
fds2610-bot e95fc7302d Fix #117: Nicht freigegebene Überweisungen nachvollziehbar machen
- 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 <noreply@anthropic.com>
2026-10-06 15:01:24 +02:00

175 lines
7.1 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
use Finanzmanager\Auth;
use Finanzmanager\Database;
use Finanzmanager\Models\Bankprotokoll;
use Finanzmanager\Models\Bankzugang;
use Finanzmanager\Models\Konto;
use Finanzmanager\Models\Zahlungsauftrag;
use Finanzmanager\Services\FinTsUeberweisung;
/**
* Nicht freigegebene Überweisungen nachvollziehen (#117).
*
* Verlangt die Bank eine TAN und wird sie nie eingereicht, führt die Bank den
* Auftrag nicht aus. Geprüft wird, dass das am Auftrag sichtbar ist, dass der
* Abbruch im Übermittlungsprotokoll steht und dass ein Auftrag mit Protokoll
* nicht mehr gelöscht werden kann. Der wartende Dialog wird direkt in die
* Sitzung gelegt – es verlässt nichts den Container.
*/
class FreigabeNachvollziehenTest extends TestCase
{
protected string $testName = 'Test 57: Freigabe nachvollziehen';
private int $giro = 0;
private int $zugang = 0;
public function run(): void
{
$this->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');
}
}