diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php
index cc2370d7..e9ed0b38 100644
--- a/lib/Fhp/Action/SendSEPATransfer.php
+++ b/lib/Fhp/Action/SendSEPATransfer.php
@@ -32,12 +32,12 @@ class SendSEPATransfer extends BaseAction
* to create this.
* @return SendSEPATransfer A new action for executing this the given PAIN message.
*/
- public static function create(SEPAAccount $account, string $painMessage): SendSEPATransfer
+ public static function create(SEPAAccount $account, string $painMessage): static
{
if (preg_match('/xmlns="(.*?)"/', $painMessage, $match) === false) {
throw new \InvalidArgumentException('xmlns not found in the PAIN message');
}
- $result = new SendSEPATransfer();
+ $result = new static();
$result->account = $account;
$result->painMessage = $painMessage;
$result->xmlSchema = $match[1];
diff --git a/lib/Fhp/Action/SendSEPATransferVoP.php b/lib/Fhp/Action/SendSEPATransferVoP.php
new file mode 100644
index 00000000..4624efe9
--- /dev/null
+++ b/lib/Fhp/Action/SendSEPATransferVoP.php
@@ -0,0 +1,151 @@
+vopIsPending) {
+ $this->hkvpp->pollingId = $this->hivpp->pollingId;
+ $this->hkvpp->aufsetzpunkt = $this->paginationToken;
+ return $this->hkvpp;
+ }
+
+ $requestSegment = parent::createRequest($bpd, $upd);
+ $requestSegments = [$requestSegment];
+
+ if ($this->vopNeedsConfirmation && $this->vopConfirmed) {
+
+ $hkvpa = HKVPAv1::createEmpty();
+ $hkvpa->vopId = $this->hivpp->vopId;
+ return [$hkvpa, $requestSegment];
+ }
+
+ // Check if VoP is supported by the bank
+
+ /** @var HIVPPSv1 $hivpps */
+ if ($hivpps = $bpd->getLatestSupportedParameters('HIVPPS')) {
+ // Check if the request segment is in the list of VoP-supported segments
+ if (in_array($requestSegment->getName(), $hivpps->parameter->vopPflichtigerZahlungsverkehrsauftrag)) {
+
+ $this->vopRequired = true;
+
+ // Send VoP confirmation
+ if ($this->needsConfirmation() && $this->hivpp?->vopId) {
+ $hkvpp = HKVPAv1::createEmpty();
+ $hkvpp->vopId = $this->hivpp->vopId;
+ $requestSegments = [$hkvpp, $requestSegment];
+ } else {
+ // Ask for VoP
+ $this->hkvpp = $hkvpp = HKVPPv1::createEmpty();
+
+ // For now just pretend we support all formats
+ $supportedFormats = explode(';', $hivpps->parameter->unterstuetztePaymentStatusReportDatenformate);
+ $hkvpp->unterstuetztePaymentStatusReports->paymentStatusReportDescriptor = $supportedFormats;
+
+ // VoP before the transfer request
+ $requestSegments = [$hkvpp, $requestSegment];
+ }
+ }
+ }
+
+ return $requestSegments;
+ }
+
+ public function processResponse(Message $response)
+ {
+ $this->vopIsPending = false;
+ $this->hivpp = $response->findSegment(HIVPPv1::class);
+
+ // The Bank does not want a separate HKVPA ("VoP Ausführungsauftrag").
+ if ($response->findRueckmeldung(Rueckmeldungscode::VOP_AUSFUEHRUNGSAUFTRAG_NICHT_BENOETIGT) !== null) {
+ $this->vopRequired = false;
+ $this->vopNeedsConfirmation = false;
+ parent::processResponse($response);
+ return;
+ }
+
+ if ($response->findRueckmeldung(Rueckmeldungscode::VOP_NAMENSABGLEICH_IST_NOCH_IN_BEARBEITUNG) !== null) {
+ $this->vopIsPending = true;
+ $this->vopNeedsConfirmation = false;
+ return;
+ }
+
+ if (($pagination = $response->findRueckmeldung(Rueckmeldungscode::PAGINATION)) !== null) {
+ $this->paginationToken = $pagination->rueckmeldungsparameter[0];
+ }
+
+ if (
+ $response->findRueckmeldung(Rueckmeldungscode::VOP_KEINE_NAMENSABWEICHUNG) !== null
+ // The bank has discarded the request, and wants us to resend it with a HKVPA
+ // This can happen even if the name matches.
+ || $response->findRueckmeldung(Rueckmeldungscode::FREIGABE_KANN_NICHT_ERTEILT_WERDEN) !== null
+ // The user needs to check the result of the name check.
+ // This can be sent by the bank even if the name matches.
+ || $response->findRueckmeldung(Rueckmeldungscode::VOP_ERGEBNIS_NAMENSABGLEICH_PRUEFEN) !== null
+ ) {
+ $this->vopNeedsConfirmation = true;
+ // Is the result already available?
+ if (!$this->hivpp->vopId) {
+ $this->vopIsPending = true;
+ }
+ return;
+ }
+
+ // The bank accepted the request as is.
+ if ($response->findRueckmeldung(Rueckmeldungscode::ENTGEGENGENOMMEN) !== null || $response->findRueckmeldung(Rueckmeldungscode::AUSGEFUEHRT) !== null) {
+ $this->vopRequired = false;
+ parent::processResponse($response);
+ return;
+ }
+
+ throw new UnsupportedException('Unexpected state in VoP process');
+ }
+
+ public function needsTime()
+ {
+ return $this->vopIsPending;
+ }
+
+ public function needsConfirmation()
+ {
+ return $this->vopNeedsConfirmation;
+ }
+
+ public function setConfirmed()
+ {
+ $this->vopConfirmed = true;
+ }
+}
\ No newline at end of file
diff --git a/lib/Fhp/BaseAction.php b/lib/Fhp/BaseAction.php
index 76ab7fc3..c7d3d55f 100644
--- a/lib/Fhp/BaseAction.php
+++ b/lib/Fhp/BaseAction.php
@@ -139,6 +139,16 @@ public function getTanRequest(): ?TanRequest
return $this->tanRequest;
}
+ public function needsConfirmation()
+ {
+ return false;
+ }
+
+ public function needsTime()
+ {
+ return false;
+ }
+
/**
* Throws an exception unless this action has been successfully executed, i.e. in the following cases:
* - the action has not been {@link FinTs::execute()}-d at all or the {@link FinTs::execute()} call for it threw an
diff --git a/lib/Fhp/FinTs.php b/lib/Fhp/FinTs.php
index a124c27a..defaf6d7 100644
--- a/lib/Fhp/FinTs.php
+++ b/lib/Fhp/FinTs.php
@@ -320,8 +320,8 @@ public function execute(BaseAction $action)
$message = MessageBuilder::create()->add($requestSegments); // This fills in the segment numbers.
if (!($this->getSelectedTanMode() instanceof NoPsd2TanMode)) {
if (($needTanForSegment = $action->getNeedTanForSegment()) !== null) {
- $message->add(HKTANFactory::createProzessvariante2Step1(
- $this->requireTanMode(), $this->selectedTanMedium, $needTanForSegment));
+ $hktan = HKTANFactory::createProzessvariante2Step1($this->requireTanMode(), $this->selectedTanMedium, $needTanForSegment);
+ $message->add($hktan);
}
}
$request = $this->buildMessage($message, $this->getSelectedTanMode());
@@ -354,7 +354,11 @@ public function execute(BaseAction $action)
}
// If no TAN is needed, process the response normally, and maybe keep going for more pages.
- $this->processActionResponse($action, $response->filterByReferenceSegments($action->getRequestSegmentNumbers()));
+ $requestSegmentsNumbers = $action->getRequestSegmentNumbers();
+ if (isset($hktan)) {
+ $requestSegmentsNumbers[] = $hktan->getSegmentNumber();
+ }
+ $this->processActionResponse($action, $response->filterByReferenceSegments($requestSegmentsNumbers));
if ($action instanceof PaginateableAction && $action->hasMorePages()) {
$this->execute($action);
}
diff --git a/lib/Fhp/Segment/VPP/HIVPPv1.php b/lib/Fhp/Segment/VPP/HIVPPv1.php
index c70a32f2..01e90f03 100644
--- a/lib/Fhp/Segment/VPP/HIVPPv1.php
+++ b/lib/Fhp/Segment/VPP/HIVPPv1.php
@@ -14,6 +14,15 @@
*/
class HIVPPv1 extends BaseSegment
{
+ public const VOP_RESULT_CODES = [
+ 'RCVC' => 'ReceivedVerificationCompleted',
+ 'RVNA' => 'ReceivedVerificationCompletedNotApplicable',
+ 'RVNM' => 'ReceivedVerificationCompletedNoMatch',
+ 'RVMC' => 'ReceivedVerificationCompletedMatchClosely',
+ 'RVNC' => 'ReceivedVerificationNotCompleted',
+ 'RVCM' => 'ReceivedVerificationCompletedWithMismatches'
+ ];
+
public ?Bin $vopId = null;
public ?Tsp $vopIdGueltigBis = null;
@@ -30,4 +39,14 @@ class HIVPPv1 extends BaseSegment
// This value is in seconds
public ?int $wartezeitVorNaechsterAbfrage = null;
+
+ public function getVopResultCode(): ?string
+ {
+ if ($this->paymentStatusReport) {
+ $report = simplexml_load_string($this->paymentStatusReport->getData());
+ return $report->CstmrPmtStsRpt->OrgnlGrpInfAndSts->GrpSts;
+ } else {
+ return $this->ergebnisVopPruefungEinzeltransaktion?->vopPruefergebnis;
+ }
+ }
}
\ No newline at end of file
diff --git a/lib/Tests/Fhp/Integration/Atruvia/AtruviaIntegrationTestBase.php b/lib/Tests/Fhp/Integration/Atruvia/AtruviaIntegrationTestBase.php
new file mode 100644
index 00000000..a466893a
--- /dev/null
+++ b/lib/Tests/Fhp/Integration/Atruvia/AtruviaIntegrationTestBase.php
@@ -0,0 +1,90 @@
+expectMessage(static::ANONYMOUS_INIT_REQUEST, mb_convert_encoding(static::ANONYMOUS_INIT_RESPONSE, '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();
+ }
+
+ /**
+ * Executes dialog synchronization and initialization, so that BPD and UPD are filled.
+ * @throws \Throwable
+ */
+ 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_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.
+ $this->expectMessage(static::SYNC_REQUEST, mb_convert_encoding(static::SYNC_RESPONSE, 'ISO-8859-1', 'UTF-8'));
+ $this->expectMessage(static::SYNC_END_REQUEST, mb_convert_encoding(static::SYNC_END_RESPONSE, 'ISO-8859-1', 'UTF-8'));
+
+ // And finally it can initialize the main dialog.
+ $this->expectMessage(static::INIT_REQUEST, mb_convert_encoding(static::INIT_RESPONSE, 'ISO-8859-1', 'UTF-8'));
+
+ $this->fints->selectTanMode(intval(static::TEST_TAN_MODE));
+ $login = $this->fints->login();
+ $login->ensureDone(); // No TAN required upon login.*/
+ $this->assertAllMessagesSeen();
+ }
+
+ protected function getTestAccount(): SEPAAccount
+ {
+ $sepaAccount = new SEPAAccount();
+ $sepaAccount->setIban('DE00ABCDEFGH1234567890');
+ $sepaAccount->setBic('ABCDEFGHIJK');
+ $sepaAccount->setAccountNumber('1234567890');
+ $sepaAccount->setBlz(self::TEST_BANK_CODE);
+ return $sepaAccount;
+ }
+}
diff --git a/lib/Tests/Fhp/Integration/Atruvia/SendTransferVoPTest.php b/lib/Tests/Fhp/Integration/Atruvia/SendTransferVoPTest.php
new file mode 100644
index 00000000..06fdceff
--- /dev/null
+++ b/lib/Tests/Fhp/Integration/Atruvia/SendTransferVoPTest.php
@@ -0,0 +1,97 @@
+' . "\n" . '
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 POLL_VOP_REPORT_NO_MATCH_RESPONSE_XML_PAYLOAD = "