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 {