diff --git a/Tests/Unit/Integration/Atruvia/SendTransferVoPTest.php b/Tests/Unit/Integration/Atruvia/SendTransferVoPTest.php index 294719e4..ef23f77a 100644 --- a/Tests/Unit/Integration/Atruvia/SendTransferVoPTest.php +++ b/Tests/Unit/Integration/Atruvia/SendTransferVoPTest.php @@ -33,6 +33,11 @@ class SendTransferVoPTest extends AtruviaIntegrationTestBase public const VOP_REPORT_PARTIAL_MATCH_RESPONSE = "HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.'HIRMS:4:2:3+3090::Ergebnis des Namensabgleichs prüfen.'HIVPP:5:1:3+@36@5e3b5c99-df27-4d42-835b-18b35d0c66ff+++urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.002.001.10+++Bei mindestens einem Zahlungsempfänger stimmt der Name mit dem für diese IBAN bei der Zahlungsempfängerbank hinterlegten Namen nicht oder nur nahezu überein.
Alternativ konnte der Name des Zahlungsempfängers nicht mit dem bei der Zahlungsempfängerbank hinterlegten Namen abgeglichen werden.

Eine nicht mögliche Empfängerüberprüfung kann auftreten, wenn ein technisches Problem vorliegt, die Empfängerbank diesen Service nicht anbietet oder eine Prüfung für das Empfängerkonto nicht möglich ist.

Wichtiger Hinweis?: Die Überweisung wird ohne Korrektur ausgeführt.

Dies kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen Inhaber nicht der von Ihnen angegebene Empfänger ist.
In diesem Fall haftet die Bank nicht für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.

Eine Haftung der an der Ausführung der Überweisung beteiligten Zahlungsdienstleister ist ebenfalls ausgeschlossen.'"; public const VOP_REPORT_PARTIAL_MATCH_XML_PAYLOAD = "ATRUVIA-20251010-125258-X2025-10-10T12:52:58.283+02:00ABCDEFGHIJKM1234567890pain.001.001.09110.00RVCMRCVC Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt mit dem für diese IBANRCVC hinterlegten Namen bei der Zahlungsempfängerbank überein.RVMC Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt nahezu mit dem für diese IBANRVMC hinterlegten Namen bei der Zahlungsempfängerbank überein. Die Autorisierung der ZahlungRVMC kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen Inhaber nichtRVMC der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nicht fürRVMC die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.RVNM Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt nicht mit dem für diese IBAN hinter-RVNM legten Namen bei der Zahlungsempfängerbank überein. Bitte prüfen Sie den Empfängernamen. Die Autori-RVNM sierung der Zahlung kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen InhaberRVNM nicht der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nichtRVNM für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.RVNA Der von Ihnen eingegebene Name des Zahlungsempfängers konnte nicht mit dem für diese IBAN hinter-RVNA legten Namen bei der Zahlungsempfängerbank abgeglichen werden (z.B. technischer Fehler). Die Autori-RVNA sierung der Zahlung kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen InhaberRVNA nicht der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nichtRVNA für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.0RCVC0RVMC1RVNM0RVNA176009357610RCVC0RVMC1RVNM0RVNANOTPROVIDEDRVNMTestempfängerDE00ABCDEFGH1234567890"; + // Instead of a pain.002 message, banks may report the result of a single transfer in the HIVPP segment itself + // (DEG "Ergebnis VOP-Prüfung Einzeltransaktion"). These two were recorded with comdirect, anonymized. + public const VOP_SINGLE_RESULT_CLOSE_MATCH_RESPONSE = "HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.'HIRMS:4:2:3+3090::Ergebnis des Namensabgleichs prüfen.'HIVPP:5:1:3+@36@5e3b5c99-df27-4d42-835b-18b35d0c66ff+++++DE00ABCDEFGH1234567890::Testempfaenger GmbH::RVMC+Der Name weicht ab. Sie können die Zahlung trotzdem freigeben.'"; + public const VOP_SINGLE_RESULT_NO_MATCH_RESPONSE = "HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.'HIRMS:4:2:3+3090::Ergebnis des Namensabgleichs prüfen.'HIVPP:5:1:3+@36@5e3b5c99-df27-4d42-835b-18b35d0c66ff+++++DE00ABCDEFGH1234567890::::RVNM+Der Name stimmt nicht überein.'"; + public const CONFIRM_VOP_REQUEST = ( 'HKCCS:3:1+DE00ABCDEFGH1234567890:ABCDEFGHIJK:1234567890::280:11223344+urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.09+@1161@' . self::XML_PAYLOAD @@ -227,6 +232,56 @@ public function testVopWithResultImmediateSuccess(): void $action->ensureDone(); } + /** + * The bank reports the result of the single transfer in the HIVPP segment instead of a pain.002 message, and + * offers the name it has on file as a decision aid, which only happens for a close match. + * @throws \Throwable + */ + public function testVopWithSingleTransactionResultCloseMatch(): void + { + $this->initDialog(); + $action = $this->createAction(); + + $this->expectMessage(static::SEND_TRANSFER_REQUEST, mb_convert_encoding(static::SEND_TRANSFER_RESPONSE_POLLING_NEEDED, 'ISO-8859-1', 'UTF-8')); + $this->fints->execute($action); + $this->assertTrue($action->needsPollingWait()); + + $this->expectMessage(static::POLL_VOP_REQUEST, mb_convert_encoding(static::VOP_SINGLE_RESULT_CLOSE_MATCH_RESPONSE, 'ISO-8859-1', 'UTF-8')); + $this->fints->pollAction($action); + + $this->assertTrue($action->needsVopConfirmation()); + $this->assertEquals( + VopVerificationResult::CompletedCloseMatch, + $action->getVopConfirmationRequest()->getVerificationResult() + ); + $this->assertEquals('Testempfaenger GmbH', $action->getVopConfirmationRequest()->getDifferingPayeeName()); + } + + /** + * For anything but a close match, the specification does not allow the bank to disclose the name it has on file, + * so there is nothing to show to the user. + * @throws \Throwable + */ + public function testVopWithSingleTransactionResultNoMatch(): void + { + $this->initDialog(); + $action = $this->createAction(); + + $this->expectMessage(static::SEND_TRANSFER_REQUEST, mb_convert_encoding(static::SEND_TRANSFER_RESPONSE_POLLING_NEEDED, 'ISO-8859-1', 'UTF-8')); + $this->fints->execute($action); + $this->assertTrue($action->needsPollingWait()); + + $this->expectMessage(static::POLL_VOP_REQUEST, mb_convert_encoding(static::VOP_SINGLE_RESULT_NO_MATCH_RESPONSE, 'ISO-8859-1', 'UTF-8')); + $this->fints->pollAction($action); + + $this->assertTrue($action->needsVopConfirmation()); + $this->assertEquals( + VopVerificationResult::CompletedNoMatch, + $action->getVopConfirmationRequest()->getVerificationResult() + ); + $this->assertNull($action->getVopConfirmationRequest()->getDifferingPayeeName()); + } + protected function createAction(): SendSEPATransfer { return SendSEPATransfer::create($this->getTestAccount(), self::XML_PAYLOAD); diff --git a/src/Model/VopConfirmationRequest.php b/src/Model/VopConfirmationRequest.php index af2e9cad..985e2712 100644 --- a/src/Model/VopConfirmationRequest.php +++ b/src/Model/VopConfirmationRequest.php @@ -22,4 +22,13 @@ public function getVerificationResult(): ?string; * return an additional explanation (in the user's language or in English), but it may also return null. */ public function getVerificationNotApplicableReason(): ?string; + + /** + * The payee name that the payee's bank has on file, which the bank offers as a decision aid when the name we asked + * for is a close match ({@link VopVerificationResult::CompletedCloseMatch}). + * + * The specification only allows banks to disclose it in that case, so this returns null for all other results, and + * it may be null even for a close match if the bank sends the result as a pain.002 message. + */ + public function getDifferingPayeeName(): ?string; } diff --git a/src/Model/VopConfirmationRequestImpl.php b/src/Model/VopConfirmationRequestImpl.php index f410eaa8..8adcbd6c 100644 --- a/src/Model/VopConfirmationRequestImpl.php +++ b/src/Model/VopConfirmationRequestImpl.php @@ -12,6 +12,7 @@ class VopConfirmationRequestImpl implements VopConfirmationRequest private ?string $informationForUser; private ?string $verificationResult; private ?string $verificationNotApplicableReason; + private ?string $differingPayeeName; public function __construct( Bin $vopId, @@ -19,12 +20,14 @@ public function __construct( ?string $informationForUser, ?string $verificationResult, ?string $verificationNotApplicableReason, + ?string $differingPayeeName = null, ) { $this->vopId = $vopId; $this->expiration = $expiration; $this->informationForUser = $informationForUser; $this->verificationResult = $verificationResult; $this->verificationNotApplicableReason = $verificationNotApplicableReason; + $this->differingPayeeName = $differingPayeeName; } public function getVopId(): Bin @@ -51,4 +54,9 @@ public function getVerificationNotApplicableReason(): ?string { return $this->verificationNotApplicableReason; } + + public function getDifferingPayeeName(): ?string + { + return $this->differingPayeeName; + } } diff --git a/src/Segment/VPP/VopHelper.php b/src/Segment/VPP/VopHelper.php index 1bc5cefe..f7ad86fe 100644 --- a/src/Segment/VPP/VopHelper.php +++ b/src/Segment/VPP/VopHelper.php @@ -105,6 +105,7 @@ public static function checkVopConfirmationRequired( } $verificationNotApplicableReason = null; + $differingPayeeName = null; if ($hivpp->paymentStatusReport === null) { if ($hivpp->ergebnisVopPruefungEinzeltransaktion === null) { throw new UnsupportedException('Missing paymentStatusReport and ergebnisVopPruefungEinzeltransaktion'); @@ -113,6 +114,8 @@ public static function checkVopConfirmationRequired( $hivpp->ergebnisVopPruefungEinzeltransaktion->vopPruefergebnis ); $verificationNotApplicableReason = $hivpp->ergebnisVopPruefungEinzeltransaktion->grundRVNA; + // Banks only fill this in for a close match, where they offer it as a decision aid for the user. + $differingPayeeName = $hivpp->ergebnisVopPruefungEinzeltransaktion->abweichenderEmpfaengername; } else { $report = simplexml_load_string($hivpp->paymentStatusReport->getData()); $verificationResult = VopVerificationResult::parse( @@ -135,6 +138,7 @@ public static function checkVopConfirmationRequired( $hivpp->aufklaerungstextAutorisierungTrotzAbweichung, $verificationResult, $verificationNotApplicableReason, + $differingPayeeName, ); }