Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions Tests/Unit/Integration/Atruvia/AtruviaIntegrationTestBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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.
Expand All @@ -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();
Expand Down
50 changes: 50 additions & 0 deletions Tests/Unit/Integration/Atruvia/SendTransferVoPStepwiseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Fhp\Tests\Unit\Integration\Atruvia;

use Fhp\UnsupportedException;

/**
* Runs the same Verification of Payee scenarios as {@link SendTransferVoPTest}, but against a bank that announces
* "schrittweise Lieferung" (S) of the payment status report in HIVPPS, as e.g. comdirect does.
*
* That parameter only describes how the bank splits the pain.002 message *if* it uses the Aufsetzpunkt mechanism, so
* all of the inherited scenarios have to work just the same. Only when the bank actually delivers a partial report do
* we have to give up, because stitching the deltas back together is not implemented.
*/
class SendTransferVoPStepwiseTest extends SendTransferVoPTest
{
protected static function anonymousInitResponse(): string
{
$stepwise = str_replace(
'HIVPPS:78:1:3+1+1+1+999:J:V:J:J:',
'HIVPPS:78:1:3+1+1+1+999:J:S:J:J:',
parent::anonymousInitResponse()
);
if ($stepwise === parent::anonymousInitResponse()) {
throw new \AssertionError('Failed to patch HIVPPS in the test BPD');
}
return $stepwise;
}

/**
* With "schrittweise Lieferung" (S) an intermediate delivery only contains the delta, so the client would have to
* stitch the deliveries together. That is not implemented, and we expect a clear error instead of a wrong result.
* @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->expectException(UnsupportedException::class);
$this->expectExceptionMessage('The stepwise transfer of VOP reports is not yet supported');
$this->fints->execute($action);
}
}
23 changes: 23 additions & 0 deletions Tests/Unit/Integration/Atruvia/SendTransferVoPTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/FinTs.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
33 changes: 25 additions & 8 deletions src/Segment/VPP/VopHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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],
Expand Down