From 290ab86c26075fa48103752b59c4b71c808959a9 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Mon, 28 Sep 2026 22:54:58 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20#89:=20Echtzeit=C3=BCberweisung=20scheite?= =?UTF-8?q?rt=20an=20SEPA-Format=20und=20VOP/TAN-Kombination?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SepaXml erzeugt jetzt zusätzlich pain.001.001.09 (Dt-Element für den Ausführungstermin, BICFI statt BIC, UETR je Zahlung) für Banken, die das ältere pain.001.001.03 nicht mehr führen. FinTsUeberweisung fängt außerdem die UnsupportedException ab, die php-fints wirft, wenn die Bank Namensabgleich-Volltreffer und TAN-Anfrage in einem Antwortsatz kombiniert, und lässt den vorhandenen automatischen Volltreffer-Namensabgleich den Rest erledigen. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 13 ++++ Changes.md | 27 +++++++ src/Services/FinTsUeberweisung.php | 33 +++++++- src/Services/SepaXml.php | 84 +++++++++++++++++---- tests/39_SepaSchemaTest.php | 104 ++++++++++++++++++++++++-- tests/40_NamensabgleichTest.php | 26 +++++++ tests/43_EchtzeitueberweisungTest.php | 5 ++ 7 files changed, 271 insertions(+), 21 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..944f9e8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +Alle wesentlichen Änderungen an diesem Projekt werden hier festgehalten. + +Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/). + +## [Unreleased] + +### Fixed + +- Echtzeitüberweisung scheiterte an Banken, die nur das ISO-Format pain.001.001.09 führen; + ein Zusammentreffen von Namensabgleich-Volltreffer und TAN-Anfrage in einer Antwort ließ + auch die gewöhnliche Überweisung mit einer rohen Bibliotheksmeldung scheitern (#89) diff --git a/Changes.md b/Changes.md index a9f83a0..c16df84 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,32 @@ # Änderungen +## Zahlungsverkehr: Echtzeitüberweisung scheiterte am SEPA-Format der Bank (28.09.2026) + +Eine Echtzeitüberweisung schlug mit „Für Echtzeitüberweisungen nimmt die Bank nur +SEPA-Formate an, die der Finanzmanager nicht erzeugt: …pain.001.001.09…“ fehl; ohne das +Echtzeit-Häkchen kam stattdessen die rohe Bibliotheksmeldung „Combined VOP match +confirmation and TAN request“ (#89). Zwei unabhängige Ursachen: + +- `SepaXml` erzeugte ausschließlich pain.001.001.03 und das DK-Format pain.001.003.03. + Banken, die inzwischen nur noch das neuere ISO-Format **pain.001.001.09** führen, wiesen + jede Überweisung schon vor der TAN ab. `SepaXml` erzeugt es jetzt mit: Der + Ausführungstermin steht in einem eigenen `Dt`-Element (`ReqdExctnDt/Dt`), die + Bankkennung heißt `BICFI` statt `BIC`, und jede Zahlung trägt eine `UETR` (eine UUID, die + das SEPA-Instant-Regelwerk seit November 2019 verlangt und die für eine + Echtzeitüberweisung ohnehin nötig ist). `SepaXml::schemaWaehlen()` bevorzugt es, wenn die + Bank es meldet, vor dem älteren pain.001.001.03. +- Bestätigt die Bank den Namensabgleich mit einem Volltreffer und verlangt im selben + Antwortsatz schon die TAN, kennt `nemiah/php-fints` (Stand v4.1.4) diese Kombination noch + nicht und wirft dafür eine rohe `UnsupportedException` – obwohl der Vorgang beide + Angaben zu diesem Zeitpunkt schon erhalten hat. `FinTsUeberweisung` fängt genau diese + Ausnahme jetzt ab und lässt den ohnehin schon vorhandenen automatischen + Volltreffer-Namensabgleich (siehe unten) den Rest erledigen, statt den Auftrag mit der + englischen Bibliotheksmeldung scheitern zu lassen. + +Erweiterte Tests in `tests/39_SepaSchemaTest.php` und `tests/43_EchtzeitueberweisungTest.php` +für das neue Format, neuer Test in `tests/40_NamensabgleichTest.php` für die abgefangene +Ausnahme. + ## Buchungsmaske: Splitbuchung und direkte Zuordnung sperren sich gegenseitig (28.09.2026) Splitzeilen und die Felder *Kategorie / Gegenkonto* und *Klasse* ließen sich in der diff --git a/src/Services/FinTsUeberweisung.php b/src/Services/FinTsUeberweisung.php index 416a728..939ac97 100644 --- a/src/Services/FinTsUeberweisung.php +++ b/src/Services/FinTsUeberweisung.php @@ -18,6 +18,7 @@ use Fhp\Options\FinTsOptions; use Fhp\Segment\IPZ\HIIPZSv1; use Fhp\Segment\IPZ\HIIPZSv2; use Fhp\Segment\SPA\HISPAS; +use Fhp\UnsupportedException; use Finanzmanager\Auth; use Finanzmanager\Models\Bankprotokoll; use Finanzmanager\Models\Bankzugang; @@ -42,7 +43,10 @@ use Finanzmanager\Models\Zahlungsauftrag; * Empfängernamen mit dem Kontoinhaber ab (Verification of Payee). Stimmt beides * überein, geht es ohne Rückfrage weiter zur TAN. Weicht der Name ab oder konnte * die Bank nicht prüfen, wird zuerst gefragt – mit dem Text, den die Bank dazu - * mitliefert. + * mitliefert. Meldet die Bank Volltreffer und TAN-Anfrage in einem Rutsch, + * kennt php-fints diese Kombination noch nicht und wirft eine rohe Ausnahme + * (`istBekannteVopTanLuecke()`) – die wird hier abgefangen, statt den Auftrag + * mit ihrem englischen Text scheitern zu lassen. * * **Echtzeitüberweisung.** Ist sie am Auftrag gewählt, geht der Auftrag als * eigener Geschäftsvorfall HKIPZ hinaus statt als HKCCS – so will es das @@ -341,7 +345,16 @@ class FinTsUeberweisung // Keine Umwandlung in eine gewöhnliche Überweisung – siehe oben. ? SendSEPARealtimeTransfer::create($sepa, $xml, false) : SendSEPATransfer::create($sepa, $xml); - $fints->execute($aktion); + + try { + $fints->execute($aktion); + } catch (UnsupportedException $e) { + if (!self::istBekannteVopTanLuecke($e)) { + throw $e; + } + // Der Auftrag hat Namensabgleich und TAN-Anfrage trotz der + // Ausnahme schon erhalten (siehe unten) – ganz normal weiter. + } return $this->nachDemSenden($fints, $aktion); } @@ -458,6 +471,22 @@ class FinTsUeberweisung return $this->angenommen($fints); } + /** + * Ob eine Ausnahme die bekannte Lücke von php-fints (Stand v4.1.4) ist: + * Bestätigt die Bank den Namensabgleich mit einem Volltreffer und verlangt + * im selben Antwortsatz schon die TAN, kennt die Bibliothek diese + * Kombination nicht und wirft eine rohe `UnsupportedException` – obwohl sie + * dem Vorgang (`BaseAction::setTanRequest()` und + * `::setVopConfirmationRequest()`) beide Angaben schon mitgegeben hat, bevor + * sie wirft. `nachDemSenden()` bestätigt einen Volltreffer ohnehin + * automatisch (siehe oben) – die Ausnahme muss also nur abgefangen werden, + * damit ihr englischer Text nicht als Fehlermeldung beim Nutzer landet. + */ + private static function istBekannteVopTanLuecke(UnsupportedException $e): bool + { + return $e->getMessage() === 'Combined VOP match confirmation and TAN request'; + } + /** * Der Vorgang, der in der Sitzung auf die Fortsetzung wartet. */ diff --git a/src/Services/SepaXml.php b/src/Services/SepaXml.php index deb5906..96f5df9 100644 --- a/src/Services/SepaXml.php +++ b/src/Services/SepaXml.php @@ -13,28 +13,43 @@ namespace Finanzmanager\Services; * Einzelüberweisung, um die es hier geht, ist das Format überschaubar und in der * Spezifikation der Deutschen Kreditwirtschaft vollständig beschrieben. * - * Erzeugt wird **pain.001.001.03** oder – wenn die Bank nur das ältere - * DK-Format führt – **pain.001.003.03**. Für die Einzelüberweisung sind beide - * Fassungen im Aufbau gleich (Reihenfolge und Pflichtfelder stimmen überein), - * sie unterscheiden sich nur im Namensraum. Welches Format die Bank annimmt, - * steht im FinTS-Segment HISPAS – siehe `Services\FinTsUeberweisung`. + * Erzeugt wird **pain.001.001.03**, das neuere **pain.001.001.09** oder – wenn + * die Bank nur das ältere DK-Format führt – **pain.001.003.03**. Für die + * Einzelüberweisung sind alle drei Fassungen im Aufbau fast gleich; welches + * Format die Bank annimmt, steht im FinTS-Segment HISPAS – siehe + * `Services\FinTsUeberweisung`. + * + * **pain.001.001.09** unterscheidet sich von .001.03 an drei Stellen, die hier + * wirklich vorkommen: Der Ausführungstermin steht in einem eigenen `
` + * (`ReqdExctnDt/Dt` statt eines nackten Datums), die Bankkennung heißt `BICFI` + * statt `BIC`, und die Zahlung trägt eine `UETR` (eine UUID, die eine + * Echtzeitüberweisung durch alle beteiligten Systeme verfolgbar macht – das + * SEPA-Instant-Regelwerk verlangt sie seit November 2019). Die Bibliothek + * `nemiah/php-fints` liest schon länger beide Fassungen (siehe `CAMT.php`: + * `$finInstn->BICFI ?? $finInstn->BIC` und `SendSEPATransfer.php`: + * `$pmtInfo->ReqdExctnDt->Dt ?? $pmtInfo->ReqdExctnDt`), erzeugt sie aber + * nicht – das holt diese Klasse nach. * * **Echtzeitüberweisung.** SEPA behandelt sie als eigenes Verfahren (SCT Inst): * In der Nachricht steht zusätzlich die Zahlungsart `LclInstrm/Cd = INST`, und * die Bank bekommt sie über einen eigenen Geschäftsvorfall (HKIPZ). Das ältere * DK-Format pain.001.003.03 kennt die Zahlungsart nicht – eine - * Echtzeitüberweisung gibt es deshalb nur im ISO-Format. + * Echtzeitüberweisung gibt es deshalb nur im ISO-Format (pain.001.001.03 oder + * pain.001.001.09). */ class SepaXml { public const SCHEMA_PAIN_001_001_03 = 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.03'; + public const SCHEMA_PAIN_001_001_09 = 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.09'; public const SCHEMA_PAIN_001_003_03 = 'urn:iso:std:iso:20022:tech:xsd:pain.001.003.03'; /** Die Formate, die hier erzeugt werden – in der Reihenfolge des Vorzugs. */ - private const SCHEMATA = [self::SCHEMA_PAIN_001_001_03, self::SCHEMA_PAIN_001_003_03]; + private const SCHEMATA = [ + self::SCHEMA_PAIN_001_001_09, self::SCHEMA_PAIN_001_001_03, self::SCHEMA_PAIN_001_003_03, + ]; /** Die Formate, in denen sich eine Echtzeitüberweisung ausdrücken lässt. */ - private const SCHEMATA_ECHTZEIT = [self::SCHEMA_PAIN_001_001_03]; + private const SCHEMATA_ECHTZEIT = [self::SCHEMA_PAIN_001_001_09, self::SCHEMA_PAIN_001_001_03]; /** Zahlungsart (Local Instrument) der Echtzeitüberweisung. */ public const ECHTZEIT = 'INST'; @@ -69,9 +84,15 @@ class SepaXml $echtzeit = !empty($auftrag['echtzeit']); if ($echtzeit && !in_array($schema, self::SCHEMATA_ECHTZEIT, true)) { - throw new \InvalidArgumentException('Eine Echtzeitüberweisung gibt es nur im Format pain.001.001.03.'); + throw new \InvalidArgumentException( + 'Eine Echtzeitüberweisung gibt es nur im ISO-Format (pain.001.001.03 oder pain.001.001.09).' + ); } + // pain.001.001.09 kennt drei Neuerungen gegenüber .001.03, die hier vorkommen – + // siehe Klassenkommentar. + $neuesSchema = $schema === self::SCHEMA_PAIN_001_001_09; + // Eindeutige Kennungen: Die Bank lehnt eine Nachricht ab, deren Kennung // sie schon kennt. $kennung = 'FM' . date('YmdHis') . substr(bin2hex(random_bytes(4)), 0, 6); @@ -126,7 +147,13 @@ class SepaXml self::kind($dokument, $zahlungsart, 'Cd', self::ECHTZEIT); } - self::kind($dokument, $sammler, 'ReqdExctnDt', $ausfuehrung); + if ($neuesSchema) { + $termin = $dokument->createElement('ReqdExctnDt'); + $sammler->appendChild($termin); + self::kind($dokument, $termin, 'Dt', $ausfuehrung); + } else { + self::kind($dokument, $sammler, 'ReqdExctnDt', $ausfuehrung); + } $schuldner = $dokument->createElement('Dbtr'); $sammler->appendChild($schuldner); @@ -143,8 +170,10 @@ class SepaXml $bankKennung = $dokument->createElement('FinInstnId'); $schuldnerBank->appendChild($bankKennung); // Ohne BIC gilt „Other/NOTPROVIDED“ – im SEPA-Raum ist die IBAN genug. + // Seit pain.001.001.09 heißt das Element BICFI statt BIC. + $bicElement = $neuesSchema ? 'BICFI' : 'BIC'; if (($auftrag['absender_bic'] ?? '') !== '') { - self::kind($dokument, $bankKennung, 'BIC', strtoupper(trim((string)$auftrag['absender_bic']))); + self::kind($dokument, $bankKennung, $bicElement, strtoupper(trim((string)$auftrag['absender_bic']))); } else { $anders = $dokument->createElement('Othr'); $bankKennung->appendChild($anders); @@ -160,6 +189,9 @@ class SepaXml $zahlungsId = $dokument->createElement('PmtId'); $zahlung->appendChild($zahlungsId); self::kind($dokument, $zahlungsId, 'EndToEndId', 'NOTPROVIDED'); + if ($neuesSchema) { + self::kind($dokument, $zahlungsId, 'UETR', self::uetr()); + } $summe = $dokument->createElement('Amt'); $zahlung->appendChild($summe); @@ -172,7 +204,7 @@ class SepaXml $zahlung->appendChild($empfaengerBank); $empfaengerBankId = $dokument->createElement('FinInstnId'); $empfaengerBank->appendChild($empfaengerBankId); - self::kind($dokument, $empfaengerBankId, 'BIC', strtoupper(trim((string)$auftrag['empfaenger_bic']))); + self::kind($dokument, $empfaengerBankId, $bicElement, strtoupper(trim((string)$auftrag['empfaenger_bic']))); } $empfaenger = $dokument->createElement('Cdtr'); @@ -215,8 +247,9 @@ class SepaXml * * Die Banken melden ihre Formate in HISPAS, manche mit dem Zusatz `_GBIC_4` * – deshalb der Präfixvergleich. Meldet die Bank nichts, bleibt es beim - * Vorzugsformat; meldet sie nur Formate, die hier nicht erzeugt werden, - * kommt `null` zurück. + * Vorzugsformat pain.001.001.03; meldet sie nur Formate, die hier nicht + * erzeugt werden, kommt `null` zurück. Führt die Bank sowohl .001.03 als + * auch das neuere .001.09, gewinnt .001.09. * * Für eine Echtzeitüberweisung kommt nur das ISO-Format infrage; führt die * Bank ausschließlich das DK-Format, ist das Ergebnis ebenfalls `null`. @@ -273,4 +306,27 @@ class SepaXml { return strtoupper(str_replace(' ', '', $iban)); } + + /** + * Eine UETR (Unique End-to-end Transaction Reference): eine UUID Version 4 + * nach RFC 4122, mit der eine Echtzeitüberweisung durch alle beteiligten + * Systeme hindurch verfolgbar bleibt. Seit pain.001.001.09 gehört sie in + * jede Zahlung dieses Formats. + */ + private static function uetr(): string + { + $bytes = random_bytes(16); + $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40); + $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); + $hex = bin2hex($bytes); + + return sprintf( + '%s-%s-%s-%s-%s', + substr($hex, 0, 8), + substr($hex, 8, 4), + substr($hex, 12, 4), + substr($hex, 16, 4), + substr($hex, 20, 12) + ); + } } diff --git a/tests/39_SepaSchemaTest.php b/tests/39_SepaSchemaTest.php index 0e21453..b5b6159 100644 --- a/tests/39_SepaSchemaTest.php +++ b/tests/39_SepaSchemaTest.php @@ -8,13 +8,16 @@ use Finanzmanager\Services\SepaXml; * Die SEPA-Nachricht im Format, das die Bank annimmt. * * Nicht jede Bank führt das ISO-Format pain.001.001.03. Etliche Häuser nehmen - * ausschließlich das ältere DK-Format pain.001.003.03 an und weisen alles - * andere schon beim Einreichen ab ("The bank does not support the XML schema - * …"). Welche Formate eine Bank führt, meldet sie im Segment HISPAS. + * ausschließlich das ältere DK-Format pain.001.003.03 an, andere melden nur + * noch das neuere pain.001.001.09 – und weisen alles andere schon beim + * Einreichen ab ("The bank does not support the XML schema …"). Welche + * Formate eine Bank führt, meldet sie im Segment HISPAS. * * Das DK-Schema liegt als `tests/schemata/pain.001.003.03.xsd` bei, damit die * erzeugte Nachricht hier wirklich geprüft wird und nicht nur auf Textbausteine - * hin abgeklopft. + * hin abgeklopft. Für pain.001.001.09 gibt es kein XSD in diesem Repository; + * geprüft werden hier die drei Stellen, an denen sich das Format von .001.03 + * unterscheidet (siehe `SepaXml`-Klassenkommentar). */ class SepaSchemaTest extends TestCase { @@ -29,6 +32,7 @@ class SepaSchemaTest extends TestCase $this->dkFormatIstSchemakonform(); $this->dasFormatStehtImNamensraum(); $this->ohneTerminWirdSofortUeberwiesen(); + $this->pain09TraegtDieNeuerungen(); } /** @@ -100,12 +104,23 @@ class SepaSchemaTest extends TestCase SepaXml::schemaWaehlen([SepaXml::SCHEMA_PAIN_001_001_03 . '_GBIC_4']), 'Der Zusatz _GBIC_4 stört den Vergleich nicht' ); + + $this->assertEquals( + SepaXml::SCHEMA_PAIN_001_001_09, + SepaXml::schemaWaehlen([SepaXml::SCHEMA_PAIN_001_001_09]), + 'Meldet die Bank nur das neuere ISO-Format, wird das erzeugt' + ); + $this->assertEquals( + SepaXml::SCHEMA_PAIN_001_001_09, + SepaXml::schemaWaehlen([SepaXml::SCHEMA_PAIN_001_001_03, SepaXml::SCHEMA_PAIN_001_001_09]), + 'Führt die Bank beide ISO-Fassungen, gewinnt die neuere' + ); } private function unbekannteFormateFuehrenZuNull(): void { $this->assertNull( - SepaXml::schemaWaehlen(['urn:iso:std:iso:20022:tech:xsd:pain.001.001.09']), + SepaXml::schemaWaehlen(['urn:iso:std:iso:20022:tech:xsd:pain.001.001.99']), 'Führt die Bank nur Formate, die hier niemand erzeugt, kommt null zurück' ); } @@ -160,6 +175,85 @@ class SepaSchemaTest extends TestCase ); } + /** + * pain.001.001.09 unterscheidet sich von .001.03 an drei Stellen (siehe + * `SepaXml`-Klassenkommentar): der Ausführungstermin steht in `
`, die + * Bankkennung heißt `BICFI`, und die Zahlung trägt eine `UETR`. + */ + private function pain09TraegtDieNeuerungen(): void + { + $pfad = static function (string $xml): DOMXPath { + $dokument = new DOMDocument(); + $dokument->loadXML($xml); + $xpath = new DOMXPath($dokument); + $xpath->registerNamespace('p', SepaXml::SCHEMA_PAIN_001_001_09); + + return $xpath; + }; + + $mitBic = $pfad(SepaXml::ueberweisung($this->auftraege()['mit BIC'], SepaXml::SCHEMA_PAIN_001_001_09)); + + $this->assertEquals( + '2099-09-05', + $mitBic->evaluate('string(//p:PmtInf/p:ReqdExctnDt/p:Dt)'), + 'Der Ausführungstermin steht in einem eigenen Dt-Element' + ); + $this->assertEquals( + 0.0, + $mitBic->evaluate("count(//p:PmtInf/p:ReqdExctnDt/text()[normalize-space(.) != ''])"), + 'Und nicht mehr als nackter Text direkt in ReqdExctnDt – nur noch das Dt-Kind' + ); + $this->assertEquals( + 'BYLADEM1001', + $mitBic->evaluate('string(//p:DbtrAgt/p:FinInstnId/p:BICFI)'), + 'Die Bankkennung des Schuldners heißt BICFI' + ); + $this->assertEquals( + 'INGDDEFFXXX', + $mitBic->evaluate('string(//p:CdtrAgt/p:FinInstnId/p:BICFI)'), + 'Ebenso die des Empfängers' + ); + $this->assertEquals( + 0.0, + $mitBic->evaluate('count(//p:FinInstnId/p:BIC)'), + 'Das alte Element BIC kommt in diesem Format nicht mehr vor' + ); + + $uetr = $mitBic->evaluate('string(//p:PmtId/p:UETR)'); + $this->assert( + (bool)preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uetr), + 'Die UETR ist eine UUID Version 4' + ); + + $zweiteUetr = $pfad(SepaXml::ueberweisung($this->auftraege()['mit BIC'], SepaXml::SCHEMA_PAIN_001_001_09)) + ->evaluate('string(//p:PmtId/p:UETR)'); + $this->assert($uetr !== $zweiteUetr, 'Jede Nachricht bekommt ihre eigene UETR'); + + // Ohne BIC bleibt es bei der bekannten Ersatzkennung – nur eben unter + // dem neuen Namensraum. + $ohneBic = $pfad(SepaXml::ueberweisung($this->auftraege()['ohne BIC'], SepaXml::SCHEMA_PAIN_001_001_09)); + $this->assertEquals( + 'NOTPROVIDED', + $ohneBic->evaluate('string(//p:DbtrAgt/p:FinInstnId/p:Othr/p:Id)'), + 'Ohne BIC steht weiterhin die Ersatzkennung, nicht ein leeres BICFI' + ); + + // Eine Echtzeitüberweisung im neuen Format trägt beide Merkmale zugleich. + $echtzeit = $this->auftraege()['ohne BIC']; + $echtzeit['echtzeit'] = true; + $echtzeitPfad = $pfad(SepaXml::ueberweisung($echtzeit, SepaXml::SCHEMA_PAIN_001_001_09)); + $this->assertEquals( + SepaXml::ECHTZEIT, + $echtzeitPfad->evaluate('string(//p:PmtTpInf/p:LclInstrm/p:Cd)'), + 'Echtzeit und pain.001.001.09 vertragen sich (Zahlungsart INST)' + ); + $this->assertEquals( + SepaXml::SOFORT, + $echtzeitPfad->evaluate('string(//p:ReqdExctnDt/p:Dt)'), + 'Und weiterhin sofort' + ); + } + /** @return array> */ private function auftraege(): array { diff --git a/tests/40_NamensabgleichTest.php b/tests/40_NamensabgleichTest.php index aa5a8ce..571635e 100644 --- a/tests/40_NamensabgleichTest.php +++ b/tests/40_NamensabgleichTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Fhp\Model\VopVerificationResult; +use Fhp\UnsupportedException; use Finanzmanager\Services\FinTsUeberweisung; /** @@ -23,6 +24,7 @@ class NamensabgleichTest extends TestCase $this->jedesErgebnisHatEinenDeutschenSatz(); $this->derBanktextWirdKlartext(); $this->ohneWartendenVorgangGehtNichts(); + $this->dieBekannteVopTanLueckeWirdErkannt(); } private function jedesErgebnisHatEinenDeutschenSatz(): void @@ -83,4 +85,28 @@ class NamensabgleichTest extends TestCase $this->assertEquals('fehler', $ergebnis['status'], 'Ohne Vorgang lässt sich nichts bestätigen'); $this->assertNotEmpty($ergebnis['meldung'], 'Und es gibt eine Erklärung dazu'); } + + /** + * Meldet die Bank Namensabgleich (Volltreffer) und TAN-Anfrage im selben + * Antwortsatz, wirft php-fints (Stand v4.1.4) dafür eine rohe + * `UnsupportedException`, statt die Kombination zu behandeln – obwohl der + * Vorgang beide Angaben zu diesem Zeitpunkt schon trägt. Ungefangen landete + * ihr englischer Text als Fehlermeldung beim Nutzer (Fix #89). Geprüft wird + * hier die Erkennung selbst; der volle Ablauf (automatische Bestätigung, + * anschließende TAN-Anfrage) lässt sich ohne Bankverbindung nicht auslösen. + */ + private function dieBekannteVopTanLueckeWirdErkannt(): void + { + $istLuecke = new ReflectionMethod(FinTsUeberweisung::class, 'istBekannteVopTanLuecke'); + $istLuecke->setAccessible(true); + + $this->assert( + $istLuecke->invoke(null, new UnsupportedException('Combined VOP match confirmation and TAN request')), + 'Die bekannte Meldung wird erkannt' + ); + $this->assert( + !$istLuecke->invoke(null, new UnsupportedException('The bank does not support the XML schema x')), + 'Eine andere UnsupportedException der Bibliothek wird nicht mit ihr verwechselt' + ); + } } diff --git a/tests/43_EchtzeitueberweisungTest.php b/tests/43_EchtzeitueberweisungTest.php index bee5dca..2f12b5a 100644 --- a/tests/43_EchtzeitueberweisungTest.php +++ b/tests/43_EchtzeitueberweisungTest.php @@ -163,6 +163,11 @@ class EchtzeitueberweisungTest extends TestCase SepaXml::schemaWaehlen([SepaXml::SCHEMA_PAIN_001_003_03, SepaXml::SCHEMA_PAIN_001_001_03 . '_GBIC_4'], true), 'Sonst wird das ISO-Format genommen' ); + $this->assertEquals( + SepaXml::SCHEMA_PAIN_001_001_09, + SepaXml::schemaWaehlen([SepaXml::SCHEMA_PAIN_001_001_03, SepaXml::SCHEMA_PAIN_001_001_09], true), + 'Führt die Bank beide ISO-Fassungen, gewinnt für Echtzeit ebenfalls die neuere' + ); $abgewiesen = false; try {