diff --git a/Tests/Unit/Integration/Atruvia/AtruviaIntegrationTestBase.php b/Tests/Unit/Integration/Atruvia/AtruviaIntegrationTestBase.php index d03e7479..522985a6 100644 --- a/Tests/Unit/Integration/Atruvia/AtruviaIntegrationTestBase.php +++ b/Tests/Unit/Integration/Atruvia/AtruviaIntegrationTestBase.php @@ -49,7 +49,7 @@ class AtruviaIntegrationTestBase extends FinTsTestCase */ protected function InitAnonymous() { - $this->expectMessage(static::ANONYMOUS_INIT_REQUEST, mb_convert_encoding(static::ANONYMOUS_INIT_RESPONSE, 'ISO-8859-1', 'UTF-8')); + $this->expectMessage(static::ANONYMOUS_INIT_REQUEST, mb_convert_encoding(static::anonymousInitResponse(), 'ISO-8859-1', 'UTF-8')); $this->expectMessage(static::ANONYMOUS_END_REQUEST, mb_convert_encoding(static::ANONYMOUS_END_RESPONSE, 'ISO-8859-1', 'UTF-8')); $this->fints->getBpd(); @@ -62,7 +62,7 @@ protected function InitAnonymous() protected function initDialog() { // We already know the TAN mode, so it will only fetch the BPD (anonymously) to verify it. - $this->expectMessage(static::ANONYMOUS_INIT_REQUEST, mb_convert_encoding(static::ANONYMOUS_INIT_RESPONSE, 'ISO-8859-1', 'UTF-8')); + $this->expectMessage(static::ANONYMOUS_INIT_REQUEST, mb_convert_encoding(static::anonymousInitResponse(), 'ISO-8859-1', 'UTF-8')); $this->expectMessage(static::ANONYMOUS_END_REQUEST, mb_convert_encoding(static::ANONYMOUS_END_RESPONSE, 'ISO-8859-1', 'UTF-8')); // Then when we initialize a dialog, it's going to request a Kundensystem-ID and UPD. @@ -78,6 +78,15 @@ protected function initDialog() $this->assertAllMessagesSeen(); } + /** + * The response that carries the BPD. Subclasses can override this to test with different bank parameters, without + * having to duplicate the (very long) message. + */ + protected static function anonymousInitResponse(): string + { + return static::ANONYMOUS_INIT_RESPONSE; + } + protected function getTestAccount(): SEPAAccount { $sepaAccount = new SEPAAccount(); diff --git a/Tests/Unit/Integration/Atruvia/SendTransferVoPStepwiseTest.php b/Tests/Unit/Integration/Atruvia/SendTransferVoPStepwiseTest.php new file mode 100644 index 00000000..12ac17b9 --- /dev/null +++ b/Tests/Unit/Integration/Atruvia/SendTransferVoPStepwiseTest.php @@ -0,0 +1,50 @@ +initDialog(); + $action = $this->createAction(); + + $response = static::buildVopReportResponse( + static::SEND_TRANSFER_RESPONSE_POLLING_NEEDED, + static::VOP_REPORT_PARTIAL_MATCH_XML_PAYLOAD + ); + $this->expectMessage(static::SEND_TRANSFER_REQUEST, $response); + + $this->expectException(UnsupportedException::class); + $this->expectExceptionMessage('The stepwise transfer of VOP reports is not yet supported'); + $this->fints->execute($action); + } +} diff --git a/Tests/Unit/Integration/Atruvia/SendTransferVoPTest.php b/Tests/Unit/Integration/Atruvia/SendTransferVoPTest.php index ef23f77a..cc15b791 100644 --- a/Tests/Unit/Integration/Atruvia/SendTransferVoPTest.php +++ b/Tests/Unit/Integration/Atruvia/SendTransferVoPTest.php @@ -282,6 +282,29 @@ public function testVopWithSingleTransactionResultNoMatch(): void $this->assertNull($action->getVopConfirmationRequest()->getDifferingPayeeName()); } + /** + * The bank tells us that it needs more time (Aufsetzpunkt) and already sends a first part of the report. With + * "vollstaendige Lieferung" (V) every delivery contains all data accumulated so far, so we can simply discard the + * intermediate one and keep polling for the final report. + * @throws \Throwable + */ + public function testIntermediateReportDelivery(): void + { + $this->initDialog(); + $action = $this->createAction(); + + $response = static::buildVopReportResponse( + static::SEND_TRANSFER_RESPONSE_POLLING_NEEDED, + static::VOP_REPORT_PARTIAL_MATCH_XML_PAYLOAD + ); + $this->expectMessage(static::SEND_TRANSFER_REQUEST, $response); + $this->fints->execute($action); + + $this->assertTrue($action->needsPollingWait()); + $this->assertFalse($action->needsVopConfirmation()); + $this->assertFalse($action->isDone()); + } + protected function createAction(): SendSEPATransfer { return SendSEPATransfer::create($this->getTestAccount(), self::XML_PAYLOAD); diff --git a/src/FinTs.php b/src/FinTs.php index 18c5a61a..aac493e6 100644 --- a/src/FinTs.php +++ b/src/FinTs.php @@ -408,7 +408,7 @@ private function processServerResponse(BaseAction $action, Message $response, ?H // Detect if the bank needs us to do something for Verification of Payee. if ($hkvpp != null) { - if ($pollingInfo = VopHelper::checkPollingRequired($response, $hkvpp->getSegmentNumber())) { + if ($pollingInfo = VopHelper::checkPollingRequired($response, $hkvpp->getSegmentNumber(), $this->bpd)) { $action->setPollingInfo($pollingInfo); if ($action->needsTan()) { throw new UnexpectedResponseException('Unexpected polling and TAN request in the same response.'); diff --git a/src/Segment/VPP/VopHelper.php b/src/Segment/VPP/VopHelper.php index f7ad86fe..6e6348d3 100644 --- a/src/Segment/VPP/VopHelper.php +++ b/src/Segment/VPP/VopHelper.php @@ -28,9 +28,11 @@ public static function createHKVPPForInitialRequest(BPD $bpd): HKVPPv1 /** @var HIVPPSv1 $hivpps */ $hivpps = $bpd->getLatestSupportedParameters('HIVPPS'); $supportedFormats = explode(';', $hivpps->parameter->unterstuetztePaymentStatusReportDatenformate); - if ($hivpps->parameter->artDerLieferungPaymentStatusReport !== 'V') { - throw new UnsupportedException('The stepwise transfer of VOP reports is not yet supported'); - } + + // Note: The "Art der Lieferung Payment Status Report" (V/S) parameter only describes how the bank splits the + // pain.002 message *if* it uses the Aufsetzpunkt mechanism, which it typically only does for large batches. + // So we can always send the initial request and only need to look at the parameter once the bank actually + // sends us a partial report, see checkPollingRequired(). $hkvpp = HKVPPv1::createEmpty(); $hkvpp->unterstuetztePaymentStatusReports->paymentStatusReportDescriptor = $supportedFormats; @@ -53,12 +55,16 @@ public static function createHKVPPForPollingRequest(BPD $bpd, VopPollingInfo $po /** * @param Message $response The response we just received from the server. * @param int $hkvppSegmentNumber The number of the HKVPP segment in the request we had sent. + * @param BPD $bpd The BPD, which tells us how the bank splits the report across multiple deliveries. * @return ?VopPollingInfo If the response indicates that the Verification of Payee is still ongoing, such that the * client should keep polling the server to (actively) wait until the result is available, this function returns * a corresponding polling info object. If no polling is required, it returns null. */ - public static function checkPollingRequired(Message $response, int $hkvppSegmentNumber): ?VopPollingInfo - { + public static function checkPollingRequired( + Message $response, + int $hkvppSegmentNumber, + BPD $bpd, + ): ?VopPollingInfo { // Note: We determine whether polling is required purely based on the presence of the primary polling token ( // the Aufsetzpunkt is mandatory, the polling ID is optional). // The specification also contains the code "3093 Namensabgleich ist noch in Bearbeitung", which could also be @@ -70,9 +76,20 @@ public static function checkPollingRequired(Message $response, int $hkvppSegment } /** @var HIVPPv1 $hivpp */ $hivpp = $response->findSegment(HIVPPv1::class); - if ($hivpp->vopId !== null || $hivpp->paymentStatusReport !== null) { - // Implementation note: If this ever happens, it could be related to $artDerLieferungPaymentStatusReport. - throw new UnexpectedResponseException('Got response with Aufsetzpunkt AND vopId/paymentStatusReport.'); + if ($hivpp->vopId !== null) { + // The specification says that the VOP ID is only present in the final HIVPP of an Aufsetzpunkt sequence. + throw new UnexpectedResponseException('Got response with Aufsetzpunkt AND vopId.'); + } + if ($hivpp->paymentStatusReport !== null) { + // This is an intermediate delivery of the report. With "vollstaendige Lieferung" (V) each delivery + // contains all data accumulated so far, so the final one is enough and we can discard this one. With + // "schrittweise Lieferung" (S) the bank only sends the delta, so the client would have to stitch the + // deliveries together, which is not implemented. + /** @var HIVPPSv1 $hivpps */ + $hivpps = $bpd->getLatestSupportedParameters('HIVPPS'); + if ($hivpps->parameter->artDerLieferungPaymentStatusReport !== 'V') { + throw new UnsupportedException('The stepwise transfer of VOP reports is not yet supported'); + } } return new VopPollingInfo( $aufsetzpunkt->rueckmeldungsparameter[0],