mandantAnlegen(); $this->stammdaten(); $this->dasMerkmalWirdGespeichert(); $this->einTerminVertraegtSichNichtMitEchtzeit(); $this->dieNachrichtTraegtDieZahlungsartInst(); $this->nurDasIsoFormatKenntEchtzeit(); $this->derSendeversuchNenntDieEchtzeitueberweisung(); } 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, ]); } /** @param array $daten */ private function auftrag(array $daten = []): int { return Zahlungsauftrag::anlegen(array_merge([ 'konto_id' => $this->giro, 'zugang_id' => $this->zugang, 'empfaenger' => 'Handwerker Schnell', 'iban' => 'DE02120300000000202051', 'betrag' => 250.0, 'echtzeit' => true, ], $daten)); } private function dasMerkmalWirdGespeichert(): void { $id = $this->auftrag(); $this->assertEquals(1, (int)Zahlungsauftrag::find($id)['echtzeit'], 'Die Echtzeitüberweisung ist vermerkt'); $gewoehnlich = $this->auftrag(['echtzeit' => false]); $this->assertEquals(0, (int)Zahlungsauftrag::find($gewoehnlich)['echtzeit'], 'Ohne Häkchen bleibt es eine gewöhnliche Überweisung'); Zahlungsauftrag::speichern($id, [ 'konto_id' => $this->giro, 'empfaenger' => 'Handwerker Schnell', 'iban' => 'DE02120300000000202051', 'betrag' => 250.0, ]); $this->assertEquals(0, (int)Zahlungsauftrag::find($id)['echtzeit'], 'Abwählen beim Bearbeiten wirkt'); } private function einTerminVertraegtSichNichtMitEchtzeit(): void { $abgewiesen = false; try { $this->auftrag(['ausfuehrung_am' => date('Y-m-d', strtotime('+7 days'))]); } catch (\InvalidArgumentException) { $abgewiesen = true; } $this->assert($abgewiesen, 'Eine Echtzeitüberweisung mit Termin in der Zukunft wird abgewiesen'); $heute = $this->auftrag(['ausfuehrung_am' => date('Y-m-d')]); $this->assertNull( Zahlungsauftrag::find($heute)['ausfuehrung_am'], 'Der heutige Tag ist „sofort“ – der Termin fällt weg' ); } private function dieNachrichtTraegtDieZahlungsartInst(): void { $auftrag = [ 'absender_name' => 'Max Mustermann', 'absender_iban' => 'DE02120300000000202051', 'empfaenger_name' => 'Handwerker Schnell', 'empfaenger_iban' => 'DE02500105170137075030', 'betrag' => 250.0, 'verwendungszweck' => 'Rechnung 4711', // Ein Termin wird bei Echtzeit nicht übernommen. 'ausfuehrung_am' => '2099-09-05', 'echtzeit' => true, ]; $pfad = static function (string $xml): DOMXPath { $dokument = new DOMDocument(); $dokument->loadXML($xml); $xpath = new DOMXPath($dokument); $xpath->registerNamespace('p', SepaXml::SCHEMA_PAIN_001_001_03); return $xpath; }; $xpath = $pfad(SepaXml::ueberweisung($auftrag)); $this->assertEquals( SepaXml::ECHTZEIT, $xpath->evaluate('string(//p:PmtInf/p:PmtTpInf/p:LclInstrm/p:Cd)'), 'Die Zahlungsart INST steht in der Nachricht' ); $this->assertEquals( 'LclInstrm', $xpath->evaluate('local-name(//p:PmtTpInf/p:SvcLvl/following-sibling::*[1])'), 'Die Zahlungsart folgt unmittelbar auf die Service-Ebene, wie das Schema es verlangt' ); $this->assertEquals( SepaXml::SOFORT, $xpath->evaluate('string(//p:PmtInf/p:ReqdExctnDt)'), 'Eine Echtzeitüberweisung geht immer sofort hinaus' ); $auftrag['echtzeit'] = false; $this->assertEquals( 0.0, $pfad(SepaXml::ueberweisung($auftrag))->evaluate('count(//p:LclInstrm)'), 'Eine gewöhnliche Überweisung trägt keine Zahlungsart' ); } private function nurDasIsoFormatKenntEchtzeit(): void { $this->assertNull( SepaXml::schemaWaehlen([SepaXml::SCHEMA_PAIN_001_003_03], true), 'Führt die Bank nur das DK-Format, gibt es keine Echtzeitüberweisung' ); $this->assertEquals( SepaXml::SCHEMA_PAIN_001_001_03, SepaXml::schemaWaehlen([SepaXml::SCHEMA_PAIN_001_003_03, SepaXml::SCHEMA_PAIN_001_001_03 . '_GBIC_4'], true), 'Sonst wird das ISO-Format genommen' ); $abgewiesen = false; try { SepaXml::ueberweisung([ 'absender_name' => 'Max Mustermann', 'absender_iban' => 'DE02120300000000202051', 'empfaenger_name' => 'Handwerker Schnell', 'empfaenger_iban' => 'DE02500105170137075030', 'betrag' => 1.0, 'echtzeit' => true, ], SepaXml::SCHEMA_PAIN_001_003_03); } catch (\InvalidArgumentException) { $abgewiesen = true; } $this->assert($abgewiesen, 'Eine Echtzeitüberweisung im DK-Format wird nicht erzeugt'); } private function derSendeversuchNenntDieEchtzeitueberweisung(): void { // Auf dem eigenen Rechner lauscht niemand – der Versuch scheitert sofort. Database::execute( 'UPDATE bank_zugaenge SET fints_url = ? WHERE id = ?', ['http://127.0.0.1:9/fints', $this->zugang] ); $id = $this->auftrag(); $ergebnis = FinTsUeberweisung::senden($id); FinTsUeberweisung::abbrechen(); $this->assertEquals('fehler', $ergebnis['status'], 'Ohne erreichbare Bank scheitert das Senden'); $this->assertEquals('fehler', Zahlungsauftrag::find($id)['status'], 'Der Auftrag bleibt als fehlgeschlagen stehen'); $eintraege = Bankprotokoll::fuerAuftrag($id); $this->assert( $eintraege !== [] && str_contains((string)$eintraege[0]['protokoll'], 'Echtzeitüberweisung'), 'Das Übermittlungsprotokoll nennt die Echtzeitüberweisung' ); } }