- 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>
175 lines
7.1 KiB
PHP
175 lines
7.1 KiB
PHP
<?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');
|
||
}
|
||
}
|