Skip to content
Draft
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
6 changes: 3 additions & 3 deletions Samples/directDebit_Sephpa.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@

$sendSEPADirectDebit = \Fhp\Action\SendSEPADirectDebit::create($oneAccount, $xml);
$fints->execute($sendSEPADirectDebit);
if ($sendSEPADirectDebit->needsTan()) {
handleStrongAuthentication($sendSEPADirectDebit); // See login.php for the implementation.
}

require_once 'vop.php';
handleVopAndAuthentication($sendSEPADirectDebit);
6 changes: 3 additions & 3 deletions Samples/directDebit_phpSepaXml.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,6 @@

$sendSEPADirectDebit = \Fhp\Action\SendSEPADirectDebit::create($oneAccount, $sepaDD->toXML('pain.008.001.02'));
$fints->execute($sendSEPADirectDebit);
if ($sendSEPADirectDebit->needsTan()) {
handleStrongAuthentication($sendSEPADirectDebit); // See login.php for the implementation.
}

require_once 'vop.php';
handleVopAndAuthentication($sendSEPADirectDebit);
15 changes: 12 additions & 3 deletions Samples/transfer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@
/** @var \Fhp\FinTs $fints */
$fints = require_once 'login.php';

// Just pick the first account, for demonstration purposes. You could also have the user choose, or have SEPAAccount
// hard-coded and not call getSEPAAccounts() at all.
$getSepaAccounts = \Fhp\Action\GetSEPAAccounts::create();
$fints->execute($getSepaAccounts);
if ($getSepaAccounts->needsTan()) {
handleStrongAuthentication($getSepaAccounts); // See login.php for the implementation.
}
$oneAccount = $getSepaAccounts->getAccounts()[0];

$dt = new \DateTime();
$dt->add(new \DateInterval('P1D'));

Expand Down Expand Up @@ -49,6 +58,6 @@

$sendSEPATransfer = \Fhp\Action\SendSEPATransfer::create($oneAccount, $sepaDD->toXML());
$fints->execute($sendSEPATransfer);
if ($sendSEPATransfer->needsTan()) {
handleStrongAuthentication($sendSEPATransfer); // See login.php for the implementation.
}

require_once 'vop.php';
handleVopAndAuthentication($sendSEPATransfer);
138 changes: 138 additions & 0 deletions Samples/vop.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

use Fhp\CurlException;
use Fhp\Protocol\ServerException;
use Fhp\Protocol\UnexpectedResponseException;

/**
* SAMPLE - Helper functions for Verification of Payee. To be used together with init.php.
*/

/** @var \Fhp\FinTs $fints */
$fints = require_once 'init.php';

/**
* To be called after the $action was already executed, this function takes care of asking the user for a TAN and VOP
* confirmation, if necessary.
* @param \Fhp\BaseAction $action The action, which must already have been run through {@link \Fhp\FinTs::execute()}.
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
*/
function handleVopAndAuthentication(\Fhp\BaseAction $action): void
{
// NOTE: This is implemented as a `while` loop here, because this sample script runs entirely in one PHP process.
// If you want to make real use of the serializations demonstrated below, in order to resume processing in a new
// PHP process later (once the user has responded via your browser/client-side application), then you won't have a
// loop like this, but instead you'll just run the code within each time you get a new request from the user.
while (!$action->isDone()) {
if ($action->needsTan()) {
handleStrongAuthentication($action); // See login.php for the implementation.
} elseif ($action->needsPollingWait()) {
handlePollingWait($action);
} elseif ($action->needsVopConfirmation()) {
handleVopConfirmation($action);
} else {
throw new \AssertionError(
'Action is not done but also does not need anything to be done. Did you execute() it?'
);
}
}
}

/**
* Waits for the amount of time that the bank prescribed and then polls the server for a status update.
* @param \Fhp\BaseAction $action An action for which {@link \Fhp\BaseAction::needsPollingWait()} returns true.
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
*/
function handlePollingWait(\Fhp\BaseAction $action): void
{
global $fints, $options, $credentials; // From login.php

// Tell the user what the bank had to say (if anything).
$pollingInfo = $action->getPollingInfo();
if ($infoText = $pollingInfo->getInformationForUser()) {
echo $infoText . PHP_EOL;
}

// Optional: If the wait is too long for your PHP process to remain alive (i.e. your server would kill the process),
// you can persist the state as shown here and instead send a response to the client-side application indicating
// that the operation is still ongoing. Then after an appropriate amount of time, the client can send another
// request, spawning a new PHP process, where you can restore the state as shown below.
if ($optionallyPersistEverything = false) {
$persistedAction = serialize($action);
$persistedFints = $fints->persist();

// These are two strings (watch out, they are NOT necessarily UTF-8 encoded), which you can store anywhere.
// This example code stores them in a text file, but you might write them to your database (use a BLOB, not a
// CHAR/TEXT field to allow for arbitrary encoding) or in some other storage (possibly base64-encoded to make it
// ASCII).
file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction]));
}

// Wait for (at least) the prescribed amount of time. --------------------------------------------------------------
// Note: In your real application, you may be doing this waiting on the client and then send a fresh request to your
// server.
$waitSecs = $pollingInfo->getNextAttemptInSeconds() ?: 5;
echo "Waiting for $waitSecs seconds before polling the bank server again..." . PHP_EOL;
sleep($waitSecs);

// Optional: If the state was persisted above, we can restore it now (imagine this is a new PHP process).
if ($optionallyPersistEverything) {
$restoredState = file_get_contents(__DIR__ . '/state.txt');
list($persistedInstance, $persistedAction) = unserialize($restoredState);
$fints = \Fhp\FinTs::new($options, $credentials, $persistedInstance);
$action = unserialize($persistedAction);
}

$fints->pollAction($action);
// Now the action is in a new state, which the caller of this function (handleVopAndAuthentication) will deal with.
}

/**
* Asks the user to confirm
* @param \Fhp\BaseAction $action An action for which {@link \Fhp\BaseAction::needsVopConfirmation()} returns true.
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
*/
function handleVopConfirmation(\Fhp\BaseAction $action): void
{
global $fints, $options, $credentials; // From login.php

$vopConfirmationRequest = $action->getVopConfirmationRequest();
if ($infoText = $vopConfirmationRequest->getInformationForUser()) {
echo $infoText . PHP_EOL;
}
echo match ($vopConfirmationRequest->getVerificationResult()) {
\Fhp\Model\VopVerificationResult::CompletedFullMatch =>
'The bank says the payee information matched perfectly, but still wants you to confirm.',
\Fhp\Model\VopVerificationResult::CompletedCloseMatch =>
'The bank says the payee information does not match exactly, so please confirm.',
\Fhp\Model\VopVerificationResult::CompletedPartialMatch =>
'The bank says the payee information does not match for all transfers, so please confirm.',
\Fhp\Model\VopVerificationResult::CompletedNoMatch =>
'The bank says the payee information does not match, but you can still confirm the transfer if you want.',
\Fhp\Model\VopVerificationResult::NotApplicable =>
$vopConfirmationRequest->getVerificationNotApplicableReason() == null
? 'The bank did not provide any information about payee verification, but you can still confirm.'
: 'The bank says: ' . $vopConfirmationRequest->getVerificationNotApplicableReason(),
default => 'The bank failed to provide information about payee verification, but you can still confirm.',
} . PHP_EOL;

// Just like in handleTan(), handleDecoupledSubmission() or handlePollingWait(), we have the option to interrupt the
// PHP process at this point, so that we can ask the user in a client application for their confirmation.
if ($optionallyPersistEverything = false) {
$persistedAction = serialize($action);
$persistedFints = $fints->persist();
// See handlePollingWait() for how to deal with this in practice.
file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction]));
}

echo "In light of the information provided above, do you want to confirm the execution of the transfer?" . PHP_EOL;
// Note: We currently have no way canceling the transfer; the only thing we can do is never to confirm it.
echo "If so, please type 'confirm' and hit Return. Otherwise, please kill this PHP process." . PHP_EOL;
while (trim(fgets(STDIN)) !== 'confirm') {
echo "Try again." . PHP_EOL;
}
echo "Confirming the transfer." . PHP_EOL;
$fints->confirmVop($action);
echo "Confirmed" . PHP_EOL;
// Now the action is in a new state, which the caller of this function (handleVopAndAuthentication) will deal with.
}
67 changes: 59 additions & 8 deletions lib/Fhp/BaseAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@

namespace Fhp;

use Fhp\Model\PollingInfo;
use Fhp\Model\TanRequest;
use Fhp\Model\VopConfirmationRequest;
use Fhp\Protocol\ActionIncompleteException;
use Fhp\Protocol\ActionPendingException;
use Fhp\Protocol\BPD;
use Fhp\Protocol\Message;
use Fhp\Protocol\TanRequiredException;
use Fhp\Protocol\UnexpectedResponseException;
use Fhp\Protocol\UPD;
use Fhp\Protocol\VopConfirmationRequiredException;
use Fhp\Segment\BaseSegment;
use Fhp\Segment\HIRMS\Rueckmeldung;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
Expand Down Expand Up @@ -48,6 +52,12 @@ abstract class BaseAction implements \Serializable
/** If set, the last response from the server regarding this action asked for a TAN from the user. */
protected ?TanRequest $tanRequest = null;

/** If set, this action is currently waiting for a long-running operation on the server to complete. */
protected ?PollingInfo $pollingInfo = null;

/** If set, this action needs the user's confirmation to be completed. */
protected ?VopConfirmationRequest $vopConfirmationRequest = null;

protected bool $isDone = false;

/**
Expand All @@ -72,15 +82,15 @@ public function serialize(): string
}

/**
* An action can only be serialized *after* it has been executed in case it needs a TAN, i.e. when the result is not
* present yet.
* An action can only be serialized *after* it has been executed and *if* it wasn't completed yet (e.g. because it
* still requires a TAN or VOP confirmation).
* If a sub-class overrides this, it should call the parent function and include it in its result.
*
* @return array The serialized action, e.g. for storage in a database. This will not contain sensitive user data.
*/
public function __serialize(): array
{
if (!$this->needsTan()) {
if (!$this->needsTan() && !$this->needsPollingWait() && !$this->needsVopConfirmation()) {
throw new \RuntimeException('Cannot serialize this action, because it is not waiting for a TAN.');
}
return [
Expand Down Expand Up @@ -139,25 +149,54 @@ public function getTanRequest(): ?TanRequest
return $this->tanRequest;
}

public function needsPollingWait(): bool
{
return !$this->isDone() && $this->pollingInfo !== null;
}

public function getPollingInfo(): ?PollingInfo
{
return $this->pollingInfo;
}

public function needsVopConfirmation(): bool
{
return !$this->isDone() && $this->vopConfirmationRequest !== null;
}

public function getVopConfirmationRequest(): ?VopConfirmationRequest
{
return $this->vopConfirmationRequest;
}

/**
* 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
* exception,
* - the action is awaiting a TAN/confirmation (as per {@link BaseAction::needsTan()}.
* - the action is awaiting a TAN/confirmation (as per {@link BaseAction::needsTan()},
* - the action is pending a long-running operation on the bank server ({@link BaseAction::needsPollingWait()}),
* - the action is awaiting the user's confirmation of the Verification of Payee result (as per
* {@link BaseAction::needsVopConfirmation()}).
*
* After executing an action, you can use this function to make sure that it succeeded. This is especially useful
* for actions that don't have any results (as each result getter would call {@link ensureDone()} internally).
* On the other hand, you do not need to call this function if you make sure that (1) you called
* {@link FinTs::execute()} and (2) you checked {@link needsTan()} and, if it returned true, supplied a TAN by
* calling {@ink FinTs::submitTan()}. Note that both exception types thrown from this method are sub-classes of
* {@link \RuntimeException}, so you shouldn't need a try-catch block at the call site for this.
* {@link FinTs::execute()} and (2) you checked and resolved all other special outcome states documented there.
* Note that both exception types thrown from this method are sub-classes of {@link \RuntimeException}, so you
* shouldn't need a try-catch block at the call site for this.
* @throws ActionIncompleteException If the action hasn't even been executed.
* @throws ActionPendingException If the action is pending a long-running server operation that needs polling.
* @throws VopConfirmationRequiredException If the action requires the user's confirmation for VOP.
* @throws TanRequiredException If the action needs a TAN.
*/
public function ensureDone()
public function ensureDone(): void
{
if ($this->tanRequest !== null) {
throw new TanRequiredException($this->tanRequest);
} elseif ($this->pollingInfo !== null) {
throw new ActionPendingException($this->pollingInfo);
} elseif ($this->vopConfirmationRequest !== null) {
throw new VopConfirmationRequiredException($this->vopConfirmationRequest);
} elseif (!$this->isDone()) {
throw new ActionIncompleteException();
}
Expand Down Expand Up @@ -248,4 +287,16 @@ final public function setTanRequest(?TanRequest $tanRequest): void
{
$this->tanRequest = $tanRequest;
}

/** To be called only by the FinTs instance that executes this action. */
final public function setPollingInfo(?PollingInfo $pollingInfo): void
{
$this->pollingInfo = $pollingInfo;
}

/** To be called only by the FinTs instance that executes this action. */
final public function setVopConfirmationRequest(?VopConfirmationRequest $vopConfirmationRequest): void
{
$this->vopConfirmationRequest = $vopConfirmationRequest;
}
}
Loading