-
Notifications
You must be signed in to change notification settings - Fork 47
Support for Verification of Payee #499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b0c9991
22fb299
b6f1a57
868560f
ce4245a
41c456e
eb781da
55d127b
a2a474a
7e4bd72
60a62ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
{ | ||
// 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
{ | ||
return $this->vopNeedsConfirmation; | ||
} | ||
|
||
public function setConfirmed() | ||
{ | ||
$this->vopConfirmed = true; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()); | ||
|
@@ -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); | ||
} | ||
|
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; | ||
} |
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; | ||
} |
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; | ||
} |
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; | ||
} |
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; | ||
} | ||
} |
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; | ||
} |
There was a problem hiding this comment.
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
if
s 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.)There was a problem hiding this comment.
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: