Skip to content
4 changes: 2 additions & 2 deletions lib/Fhp/Action/SendSEPATransfer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
151 changes: 151 additions & 0 deletions lib/Fhp/Action/SendSEPATransferVoP.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

namespace Fhp\Action;

use Fhp\Protocol\BPD;
use Fhp\Protocol\Message;
use Fhp\Protocol\UPD;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
use Fhp\Segment\VPP\HIVPPSv1;
use Fhp\Segment\VPP\HIVPPv1;
use Fhp\Segment\VPP\HKVPAv1;
use Fhp\Segment\VPP\HKVPPv1;
use Fhp\UnsupportedException;

/**
* Initiates an outgoing wire transfer in SEPA format (PAIN XML) with VoP.
* @see FinTS_3.0_Messages_Geschaeftsvorfaelle_VOP_1.01_2025_06_27_FV.pdf
*/
class SendSEPATransferVoP extends SendSEPATransfer
{
protected $vopRequired = false;
protected $vopIsPending = false;
protected $vopNeedsConfirmation = false;

protected $vopConfirmed = false;

/**
* If set, the last response from the server regarding this action indicated that there are more results to be
* fetched using this pagination token. This is called "Aufsetzpunkt" in the specification.
* Pagination is used in VoP to poll for the result of the name check.
*/
protected ?string $paginationToken = null;

public ?HKVPPv1 $hkvpp = null;
public ?HIVPPv1 $hivpp = null;

protected function createRequest(BPD $bpd, ?UPD $upd)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Für mein besseres Verständnis: Diese Funktion wird ja nun vermutlich einige Male nacheinander aufgerufen. Anhand des Zustands der obigen Member-Variablen muss die Action dann entscheiden können, welche Requests jeweils gesendet werden sollen.

Könntest du mir bitte einen Überblick geben, was der Reihe nach passiert? Vielleicht in Form einer Chronologie, welche von den ifs unten nacheinander zutreffen? Oder in Form eines Logs von Requests und Responses? Oder als Beschreibung der Zustands-Änderungen (also wann geht vopNeedsConfirmation auf true, wann auf false, wann relativ dazu geht vopIsPending auf true und so weiter)? Ich weiß nicht, in welcher Form es sich am besten erklären lässt. (Im Idealfall kann man eine einfachst-mögliche Erklärung am Ende auch in einfachen Code gießen.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Der Ablauf ist in der Spezifikation bereits als Diagram dargestellt. Grob gesagt:

  • Prüfauftrag (HKVPP) + Geschäftsvorfall abschicken
  • Bank antwortet mit, keine Prüfautrag nötig oder mit HIVPP (der dann entweder das Prüfergebnis + VoP-Id enthält oder eine Polling-Id und keine VoP-Id).
  • Abhängig davon muss ggf. solange HKVPP (Polling-Id + Aufsetzpunkt) nochmal schicken abfragen bis man eine VoP-Id ()bekommen hat.
  • Wenn man endlich eine VoP-Id hat, dann kann man den Ausführungsauftrag (HKVPA) + den ursprünglichen Geschäftsvorfall abschicken.
  • Dann kommt die normale Tan-Behandlung

{
// Do we need to ask for the VoP result?
if ($this->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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hier fehlt mir ein erklärender Kommentar.


$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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das ist code 3040. In der VoP-Spezifikation kommt der nicht so explizit vor. Ergibt sich das daraus, dass dort von "Aufsetzpunktmechanismus" die Rede ist? Oder hast du die 3040 einfach in der freien Wildbahn beobachtet?

Der Begriff "Pagination" passt dann nicht mehr so richtig. Wenn das wirklich identisch ist mit dem Aufsetzpunkt der für Pagination verwendet wird, sollten wir es in "continuation (token)" oder so umbenennen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich habe lange nicht verstanden wo der Aufsetzpunkt herkommen soll, der in der Spezifikation erwähnt wird. Bis ich dann draufgekommen bin das es Aufsetzpunkte ja schon öfter gab. Und ja: ohne den Aufsetzpunkt funktioniert das Polling der Ergebnisse nicht.

$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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needsConfirmation() ist, wenn der Endnutzer etwas tun muss (z.b. 2FA bestätigen auf dem Handy). Das scheint hier nicht der Fall zu sein, oder? (Sonst würde ich eine neue Methode FinTs::confirmVoP() erwarten analog zu submitTan().)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah jetzt sehe ich deinen Beispielcode. Also braucht es doch beides und statt FinTs::confirmVoP() haben wir im Moment Action::setConfirmed() (was für mich wie ein dummer Setter klang, aber was wohl eine Funktion mit mehr Tragweite ist).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needsConfirmation ist wenn die Bank einen HKVPA verlangt. Das ist also außer in bestimmten Sonderfällen eigentlich immer der Fall.
Dann muss der Nutzer ja irgendwie mitteilen, dass er diese Bestätigung auch erteilt. Das hatte ich setConfirmed genannt. Beides muss True sein bevor man einen Ausführungsauftrag sendet. FinTs::confirmVoP() ist aber in der Tat viel klarer.

{
return $this->vopNeedsConfirmation;
}

public function setConfirmed()
{
$this->vopConfirmed = true;
}
}
10 changes: 10 additions & 0 deletions lib/Fhp/BaseAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,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
Expand Down
10 changes: 7 additions & 3 deletions lib/Fhp/FinTs.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diese Code-Änderung hat keine Auswirkung, oder? Könnte man also auch rückgängig machen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doch, ich brauche die Variable um die Segmentnummer zu ermitteln, damit es nicht rausgefiltert wird. Da ist aber nur relevant wenn die Action selbst die VoP sachen macht.

$message->add($hktan);
}
}
$request = $this->buildMessage($message, $this->getSelectedTanMode());
Expand Down Expand Up @@ -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);
}
Expand Down
18 changes: 18 additions & 0 deletions lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ public static function isError(int $code): bool
*/
public const PAGINATION = 3040;

public const VOP_KEINE_NAMENSABWEICHUNG = 25;

public const VOP_ERGEBNIS_NAMENSABGLEICH_PRUEFEN = 3090;

public const VOP_AUSFUEHRUNGSAUFTRAG_NICHT_BENOETIGT = 3091;

public const VOP_NAMENSABGLEICH_IST_NOCH_IN_BEARBEITUNG = 3093;

public const VOP_NAMENSABGLEICH_IST_KOMPLETT = 3094;

/**
* Zugelassene Ein- und Zwei-Schritt-Verfahren für den Benutzer (+ Rückmeldungsparameter).
* The parameters reference the VerfahrensparameterZweiSchrittVerfahren.sicherheitsfunktion values (900..997) from
Expand All @@ -99,6 +109,14 @@ public static function isError(int $code): bool
*/
public const ZUGANG_VORLAEUFIG_GESPERRT = 3938;

/**
* Der eingereichte HKTAN ist entwertet und der Auftrag (nach
* vollständiger Übermittlung des Prüfergebnisses) soll erneut mit einem neuen
* HKTAN in Verbindung mit einem HKVPA eingereicht werden, sofern der
* Kunde die Ausführung weiterhin wünscht.
*/
public const FREIGABE_KANN_NICHT_ERTEILT_WERDEN = 3945;

/**
* Starke Kundenauthentifizierung noch ausstehend.
* Indicates that the decoupled authentication is still outstanding.
Expand Down
21 changes: 21 additions & 0 deletions lib/Fhp/Segment/VPP/ErgebnisVopPruefungEinzeltransaktion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Fhp\Segment\VPP;

use Fhp\Segment\BaseDeg;

class ErgebnisVopPruefungEinzeltransaktion extends BaseDeg
{
public string $ibanEmpfaenger;

public ?string $ibanZusatzinformationen = null;

public ?string $abweichenderEmpfaengername = null;

public ?string $anderesIdentifikationmerkmal = null;

/** Allowed values: RVMC, RCVC, RVNM, RVNA, PDNG */
public string $vopPruefergebnis;

public ?string $grundRVNA = null;
}
16 changes: 16 additions & 0 deletions lib/Fhp/Segment/VPP/HIVPPSv1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Fhp\Segment\VPP;

use Fhp\Segment\BaseGeschaeftsvorfallparameter;

/**
* Segment: Namensabgleich Prüfauftrag Parameter
*
* @see FinTS_3.0_Messages_Geschaeftsvorfaelle_VOP_1.01_2025_06_27_FV.pdf
* Section: C.10.7.1 c)
*/
class HIVPPSv1 extends BaseGeschaeftsvorfallparameter
{
public ParameterNamensabgleichPruefauftrag $parameter;
}
33 changes: 33 additions & 0 deletions lib/Fhp/Segment/VPP/HIVPPv1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Fhp\Segment\VPP;

use Fhp\Segment\BaseSegment;
use Fhp\Segment\Common\Tsp;
use Fhp\Syntax\Bin;

/**
* Segment: Namensabgleich Prüfergebnis
*
* @see FinTS_3.0_Messages_Geschaeftsvorfaelle_VOP_1.01_2025_06_27_FV.pdf
* Section: C.10.7.1 b)
*/
class HIVPPv1 extends BaseSegment
{
public ?Bin $vopId = null;

public ?Tsp $vopIdGueltigBis = null;

public ?Bin $pollingId = null;

public ?string $paymentStatusReportDescriptor = null;

public ?Bin $paymentStatusReport = null;

public ?ErgebnisVopPruefungEinzeltransaktion $ergebnisVopPruefungEinzeltransaktion = null;

public ?string $aufklaerungstextAutorisierungTrotzAbweichung = null;

// This value is in seconds
public ?int $wartezeitVorNaechsterAbfrage = null;
}
17 changes: 17 additions & 0 deletions lib/Fhp/Segment/VPP/HKVPAv1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Fhp\Segment\VPP;

use Fhp\Segment\BaseSegment;
use Fhp\Syntax\Bin;

/**
* Segment: Namensabgleich Ausführungsauftrag
*
* @see FinTS_3.0_Messages_Geschaeftsvorfaelle_VOP_1.01_2025_06_27_FV.pdf
* Section: C.10.7.1.2 a)
*/
class HKVPAv1 extends BaseSegment
{
public Bin $vopId;
}
32 changes: 32 additions & 0 deletions lib/Fhp/Segment/VPP/HKVPPv1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Fhp\Segment\VPP;

use Fhp\Segment\BaseSegment;
use Fhp\Syntax\Bin;

/**
* Segment: Namensabgleich Prüfauftrag
*
* @see FinTS_3.0_Messages_Geschaeftsvorfaelle_VOP_1.01_2025_06_27_FV.pdf
* Section: C.10.7.1 a)
*/
class HKVPPv1 extends BaseSegment
{
public UnterstuetztePaymentStatusReports $unterstuetztePaymentStatusReports;

public ?Bin $pollingId = null;

/** Only allowed if {@link ParameterNamensabgleichPruefauftrag::$eingabeAnzahlEintraegeErlaubt} says so. */
public ?int $maximaleAnzahlEintraege = null;

/** For pagination. Max length: 35 */
public ?string $aufsetzpunkt = null;

public static function createEmpty(): static
{
$hkvpp = parent::createEmpty();
$hkvpp->unterstuetztePaymentStatusReports = new UnterstuetztePaymentStatusReports();
return $hkvpp;
}
}
24 changes: 24 additions & 0 deletions lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Fhp\Segment\VPP;

use Fhp\Segment\BaseDeg;

class ParameterNamensabgleichPruefauftrag extends BaseDeg
{
public int $maximaleAnzahlCreditTransferTransactionInformationOptIn;

public bool $aufklaerungstextStrukturiert;

/** Allowed values: V, S */
public string $artDerLieferungPaymentStatusReport;

public bool $sammelzahlungenMitEinemAuftragErlaubt;

public bool $eingabeAnzahlEintraegeErlaubt;

public string $unterstuetztePaymentStatusReportDatenformate;

/** @var string[] @Max(999999) Max length each: 6 */
public array $vopPflichtigerZahlungsverkehrsauftrag;
}
Loading