bibliothekErkenntDenOffenenStatus(); $this->bibliothekLiestDieStatusantwort(); $this->mandantAnlegen(); $this->stammdaten(); $this->offenerStatusWirdVermerkt(); $this->statusabfrageWartetDieMindestwartezeitAb(); $this->faelligeAbfragenWerdenGefunden(); $this->ausgefuehrtWirdVermerkt(); $this->abgelehntEntferntDieBuchung(); $this->umsatzKlaertDenOffenenStatus(); $this->einNeuerSendeversuchLoeschtDenAltenStatus(); } private static function nachricht(string $roh): Message { return Message::parse(mb_convert_encoding($roh, 'ISO-8859-1', 'UTF-8')); } private function bibliothekErkenntDenOffenenStatus(): void { $aktion = SendSEPARealtimeTransfer::create(new SEPAAccount(), ''); $aktion->processResponse(self::nachricht(self::ANTWORT_OFFEN)); $this->assert($aktion->isStatusPending(), 'Rückmeldung 3045 gilt als „Status offen“, nicht als Fehler'); $this->assertEquals('C6592DB8C24247C493F1B27377EC63DA', $aktion->getOrderId(), 'Auftragsidentifikation aus HIIPZ'); $this->assertNull($aktion->getStatus(), 'Ohne Statusangabe in HIIPZ kein Status'); } private function bibliothekLiestDieStatusantwort(): void { $abfrage = GetSEPARealtimeTransferStatus::create(new SEPAAccount(), 'C6592DB8C24247C493F1B27377EC63DA'); $abfrage->processResponse(self::nachricht(self::ANTWORT_STATUS)); $this->assert(!$abfrage->isStatusPending(), 'Mit Statusangabe ist nichts mehr offen'); $this->assertEquals(7, $abfrage->getStatus(), 'Status 7 aus HIIPS gelesen'); $this->assert($abfrage->isExecuted(), 'Status 7 heißt ausgeführt'); } private function stammdaten(): void { $this->giro = Konto::anlegen([ 'name' => 'Echtzeit-Giro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1, ]); $this->zugang = Bankzugang::anlegen([ 'name' => 'Echtzeit-Bank', 'bankname' => 'Echtzeit-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] ); } /** Ein gesendeter Auftrag mit offenem Status, frühestens in `$wartezeit` Sekunden abfragbar. */ private function offenerAuftrag(int $wartezeit = 0): int { $id = Zahlungsauftrag::anlegen([ 'konto_id' => $this->giro, 'zugang_id' => $this->zugang, 'empfaenger' => 'Rheinwerk Verlag GmbH', 'iban' => 'DE26380601863001400028', 'betrag' => 260.61, 'echtzeit' => 1, ]); Zahlungsauftrag::alsGesendetVermerken($id, 'Entgegengenommen.'); Zahlungsauftrag::echtzeitOffenVermerken($id, 'C6592DB8C24247C493F1B27377EC63DA', $wartezeit, 'Status offen.'); return $id; } private function offenerStatusWirdVermerkt(): void { $id = $this->offenerAuftrag(99); $auftrag = Zahlungsauftrag::find($id); $this->assertEquals('gesendet', $auftrag['status'], 'Auftrag bleibt gesendet – erneutes Senden wäre eine Doppelzahlung'); $this->assertNotEmpty($auftrag['buchung_id'], 'Auftrag ist gebucht'); $this->assertEquals('offen', $auftrag['echtzeit_status'], 'Echtzeit-Status offen'); $this->assertEquals('C6592DB8C24247C493F1B27377EC63DA', $auftrag['bank_auftrag_id'], 'Auftragsidentifikation gespeichert'); $sekunden = Zahlungsauftrag::sekundenBisStatusabfrage($auftrag); $this->assert($sekunden !== null && $sekunden >= 97 && $sekunden <= 99, 'Mindestwartezeit der Bank (99 s) gilt bis zur nächsten Abfrage'); } private function statusabfrageWartetDieMindestwartezeitAb(): void { $ergebnis = FinTsUeberweisung::statusImHintergrund($this->offenerAuftrag(99)); $this->assertEquals('fehler', $ergebnis['status'], 'Vor Ablauf der Mindestwartezeit keine Abfrage'); $this->assert(str_contains($ergebnis['meldung'], 'frühestens'), 'Meldung nennt die Wartezeit'); $ohne = Zahlungsauftrag::anlegen([ 'konto_id' => $this->giro, 'zugang_id' => $this->zugang, 'empfaenger' => 'Niemand', 'iban' => 'DE26380601863001400028', 'betrag' => 1, 'echtzeit' => 1, ]); $this->assertNull(Zahlungsauftrag::sekundenBisStatusabfrage(Zahlungsauftrag::find($ohne)), 'Ohne offenen Status keine Abfrage'); $this->assertEquals('fehler', FinTsUeberweisung::statusImHintergrund($ohne)['status'], 'Statusabfrage ohne offenen Status wird abgewiesen'); } private function faelligeAbfragenWerdenGefunden(): void { $faellig = $this->offenerAuftrag(0); $spaeter = $this->offenerAuftrag(99); $ids = Zahlungsauftrag::faelligeStatusabfragen(); $this->assert(in_array($faellig, $ids, true), 'Fällige Statusabfrage wird gefunden (Neustart nach Container-Neustart)'); $this->assert(!in_array($spaeter, $ids, true), 'Noch nicht fällige Statusabfrage bleibt außen vor'); Zahlungsauftrag::echtzeitAbfrageEinstellen($faellig, 'Eingestellt.'); $auftrag = Zahlungsauftrag::find($faellig); $this->assertEquals('offen', $auftrag['echtzeit_status'], 'Eingestellte Abfrage lässt den Ausgang offen'); $this->assertNull(Zahlungsauftrag::sekundenBisStatusabfrage($auftrag), 'Eingestellte Abfrage wird nicht mehr angestoßen'); $this->assert(!in_array($faellig, Zahlungsauftrag::faelligeStatusabfragen(), true), 'Eingestellte Abfrage ist nicht mehr fällig'); } private function ausgefuehrtWirdVermerkt(): void { $id = $this->offenerAuftrag(); Zahlungsauftrag::echtzeitAusgefuehrtVermerken($id, 'Bestätigt.'); $auftrag = Zahlungsauftrag::find($id); $this->assertEquals('ausgefuehrt', $auftrag['echtzeit_status'], 'Ausführung vermerkt'); $this->assertEquals('gesendet', $auftrag['status'], 'Auftrag bleibt gesendet'); $this->assertNull(Zahlungsauftrag::sekundenBisStatusabfrage($auftrag), 'Danach keine Statusabfrage mehr'); } private function abgelehntEntferntDieBuchung(): void { $id = $this->offenerAuftrag(); $buchungId = (int)Zahlungsauftrag::find($id)['buchung_id']; Zahlungsauftrag::echtzeitAbgelehntVermerken($id, 'Abgelehnt.'); $auftrag = Zahlungsauftrag::find($id); $this->assertEquals('fehler', $auftrag['status'], 'Abgelehnter Auftrag lässt sich erneut senden'); $this->assertEquals('abgelehnt', $auftrag['echtzeit_status'], 'Ablehnung vermerkt'); $this->assertNull($auftrag['buchung_id'], 'Buchungsverweis entfernt'); $this->assertNull(Buchung::find($buchungId), 'Buchung entfernt – das Geld ist nicht abgegangen'); } private function umsatzKlaertDenOffenenStatus(): void { $id = $this->offenerAuftrag(); $buchungId = (int)Zahlungsauftrag::find($id)['buchung_id']; $umsatzId = Bankumsatz::anlegen([ 'zugang_id' => $this->zugang, 'konto_id' => $this->giro, 'extern_id' => 'echtzeit-test-' . $id, 'buchungstag' => date('Y-m-d'), 'betrag' => -260.61, 'empfaenger' => 'Rheinwerk Verlag GmbH', 'verwendungszweck' => 'ECHTZEIT-UEBERWEISUNG', ]); Bankumsatz::verbinden((int)$umsatzId, $buchungId); $this->assertEquals('ausgefuehrt', Zahlungsauftrag::find($id)['echtzeit_status'], 'Verbundener Umsatz klärt den offenen Status'); } private function einNeuerSendeversuchLoeschtDenAltenStatus(): void { $id = $this->offenerAuftrag(); Zahlungsauftrag::echtzeitAbgelehntVermerken($id, 'Abgelehnt.'); Zahlungsauftrag::alsGesendetVermerken($id, 'Diesmal angenommen.'); $auftrag = Zahlungsauftrag::find($id); $this->assertNull($auftrag['echtzeit_status'], 'Erneutes Senden setzt den Echtzeit-Status zurück'); $this->assertNull($auftrag['bank_auftrag_id'], 'Erneutes Senden vergisst die alte Auftragsidentifikation'); } }