From d30c0c60a9b3257f75721534288c257e59732686 Mon Sep 17 00:00:00 2001 From: Nena Furtmeier Date: Tue, 25 Mar 2025 13:03:54 +0100 Subject: [PATCH 01/38] version bump --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ade9ea0d..03c9e595 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "nemiah/php-fints", "description": "PHP Library for the protocols fints and hbci", "homepage": "https://github.com/nemiah/phpFinTS", - "version": "3.5.0", + "version": "3.6.0", "license": "MIT", "autoload": { "psr-0": { From 6a43c769bd15f80e17dba5b0cbc380a7ffec87f1 Mon Sep 17 00:00:00 2001 From: Ansas Meyer Date: Mon, 16 Jun 2025 17:30:37 +0200 Subject: [PATCH 02/38] Update HICMEv1.php Commit 5e147cc bugfix --- lib/Fhp/Segment/CME/HICMEv1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Fhp/Segment/CME/HICMEv1.php b/lib/Fhp/Segment/CME/HICMEv1.php index 9b75693a..4fb50613 100644 --- a/lib/Fhp/Segment/CME/HICMEv1.php +++ b/lib/Fhp/Segment/CME/HICMEv1.php @@ -2,7 +2,7 @@ namespace Fhp\Segment\CME; -use Fhp\Segment\BaseGeschaeftsvorfallparameter; +use Fhp\Segment\BaseSegment; /** * Segment: SEPA Einzelüberweisung Parameter (Version 1) From dbcd9f7b7f4ca1701a3c193c5d41234d0409eaec Mon Sep 17 00:00:00 2001 From: Ansas Meyer Date: Mon, 16 Jun 2025 17:31:18 +0200 Subject: [PATCH 03/38] Update HICSEv1.php Commit 5e147cc bugfix --- lib/Fhp/Segment/CSE/HICSEv1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Fhp/Segment/CSE/HICSEv1.php b/lib/Fhp/Segment/CSE/HICSEv1.php index 1b17e5dc..4bedf9fe 100644 --- a/lib/Fhp/Segment/CSE/HICSEv1.php +++ b/lib/Fhp/Segment/CSE/HICSEv1.php @@ -2,7 +2,7 @@ namespace Fhp\Segment\CSE; -use Fhp\Segment\BaseGeschaeftsvorfallparameter; +use Fhp\Segment\BaseSegment; /** * Segment: SEPA Einzelüberweisung Parameter (Version 1) From 184adc937b18a983fd851b6e610f62562c5abc54 Mon Sep 17 00:00:00 2001 From: Philipp Keck Date: Sat, 9 Aug 2025 15:28:03 +0200 Subject: [PATCH 04/38] Clarify post condition of submitTan() Re second question on #485. --- lib/Fhp/FinTs.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Fhp/FinTs.php b/lib/Fhp/FinTs.php index 426ff10c..70006244 100644 --- a/lib/Fhp/FinTs.php +++ b/lib/Fhp/FinTs.php @@ -337,6 +337,9 @@ public function execute(BaseAction $action) * `false`, this function sends the given $tan to the server in order to complete the action. This can be done * asynchronously, i.e. not in the same PHP process as the original {@link execute()} call. * + * After this function returns, the `$action` is completed. That is, its result is available through its getters + * just as if it had been completed by the original call to {@link execute()} right away. + * * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Security_Sicherheitsverfahren_PINTAN_2020-07-10_final_version.pdf * Section B.4.2.1.1 * From cb27985d442000abdb745f3f58cee87273c36a5f Mon Sep 17 00:00:00 2001 From: Philipp Keck Date: Sat, 16 Aug 2025 12:45:33 +0200 Subject: [PATCH 05/38] Improve documentation around FinTs::persist() --- lib/Fhp/FinTs.php | 120 +++++++++++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 39 deletions(-) diff --git a/lib/Fhp/FinTs.php b/lib/Fhp/FinTs.php index 70006244..a124c27a 100644 --- a/lib/Fhp/FinTs.php +++ b/lib/Fhp/FinTs.php @@ -32,7 +32,7 @@ /** * This is the main class of this library. Please see the Samples directory for how to use it. - * This class is not thread-safe, do not call its funtions concurrently. + * This class is not thread-safe. Do not call its funtions concurrently. */ class FinTs { @@ -50,7 +50,7 @@ class FinTs /** @var string|null This is a {@link TanMedium::getName()}, but we don't have the {@link TanMedium} instance. */ private $selectedTanMedium; - // State that persists across physical connections, dialogs and even PHP executions. + // State that persists across physical connections, dialogs and even PHP processes. /** @var BPD|null */ private $bpd; /** @var int[]|null The IDs of the {@link TanMode}s from the BPD which the user is allowed to use. */ @@ -74,8 +74,8 @@ class FinTs * @param Credentials $credentials Authentication information for the user. Note: This library does not support * anonymous connections, so the credentials are mandatory. * @param string|null $persistedInstance The return value of {@link persist()} of a previous FinTs instance, - * usually from an earlier PHP execution. Passing this in here saves 1-2 dialogs that are normally made with the - * bank to obtain the BPD and Kundensystem-ID. + * usually from an earlier PHP process. NOTE: Each persisted instance may be used only once and should be + * considered invalid afterwards. To continue the session, call {@link persist()} again. */ public static function new(FinTsOptions $options, Credentials $credentials, ?string $persistedInstance = null): FinTs { @@ -89,7 +89,7 @@ public static function new(FinTsOptions $options, Credentials $credentials, ?str } /** - * This function allows to fetch the BPD without knowing the user's credentials yet, by using an anonymous dialog. + * This function allows fetching the BPD without knowing the user's credentials yet, by using an anonymous dialog. * Note: If this fails with an error saying that your bank does not support the anonymous dialog, you probably need * to use {@link NoPsd2TanMode} for regular login. * @param FinTsOptions $options Configuration options for the connection to the bank. @@ -118,9 +118,8 @@ protected function __construct(FinTsOptions $options, ?Credentials $credentials) } /** - * Destructing the object only disconnects. Please use {@link close()} if you want to properly "log out", i.e. end - * the FinTs dialog. On the other hand, do *not* close in case you have serialized the FinTs instance and intend - * to resume it later due to a TAN request. + * Destructing the object only disconnects. Please use {@link close()} if you want to properly "log out", i.e., end + * the FinTs dialog. */ public function __destruct() { @@ -128,24 +127,38 @@ public function __destruct() } /** - * Returns a serialized form of parts of this object. This is different from PHP's `\Serializable` in that it only - * serializes parts and cannot simply be restored with `unserialize()` because the `FinTsOptions` and the - * `Credentials` need to be passed to FinTs::new() in addition to the string returned here. + * Returns a serialized form this object. This is different from PHP's {@link \Serializable} in that it only + * serializes parts and cannot simply be restored with {@link unserialize()}, because the {@link FinTsOptions} and + * the {@link Credentials} need to be passed to {@link FinTs::new()}, in addition to the string returned here. * - * Alternatively you can use {@link loadPersistedInstance) to separate constructing the instance and resuming it. + * Alternatively, you can use {@link loadPersistedInstance()} to separate constructing the instance and resuming it. * - * NOTE: Unless you're persisting this object to complete a TAN request later on, you probably want to log the user - * out first by calling {@link close()}. + * There are broadly two reasons to persist the instance: + * 1. During login or certain other actions, you may encounter a TAN/2FA request ({@link BaseAction::needsTan()} + * returns true). In that case, you MUST call {@link submitTan()} or {@link checkDecoupledSubmission()} later, + * without losing the dialog state in between. Depending on your application's circumstances, one option might be + * to simply keep the {@link FinTs} instance itself alive in memory (e.g., in a CLI application, you can block + * until the user provides the TAN). In most server-based scenarios, however, the PHP process will shut down and + * a new PHP process will be started later, when the client calls again to provide the TAN. In this case, you + * need to persist the {@link FinTs} instance and restore it later in order for the action to succeed. + * 2. Even when there is no outstanding action and after logging out with {@link close()}, it's beneficial to + * persist the instance (with $minimal=false). By reusing the cached BPD, UPD and TAN mode information upon the + * next {@link login()}, a few roundtrips to the FinTS server can be avoided. * - * @param bool $minimal If true, the return value only contains only those values that are necessary to complete an - * outstanding TAN request, but not the relatively large BPD/UPD, which can always be retrieved again later with - * a few extra requests to the server. + * IMPORTANT: Each serialized instance (each value returned from {@link persist()}) can only be used once. After + * passing it to {@link FinTs::new()} or {@link loadPersistedInstance()}, you must consider it invalidated. To keep + * the same instance/session alive, you must call {@link persist()} again. + * + * @param bool $minimal If true, the return value only contains the values necessary to complete an outstanding TAN + * request, but not the relatively large BPD/UPD, which can always be retrieved again later with a few extra + * requests to the server. So the persisting doesn't work for use case (2.) from above, but in turn, it saves + * storage space. * @return string A serialized form of those parts of the FinTs instance that can reasonably be persisted (BPD, UPD, * Kundensystem-ID, etc.). Note that this usually contains some user data (user's name, account names and - * sometimes a dialog ID that is equivalent to session cookie), so the returned string needs to be treated + * sometimes a dialog ID that is equivalent to session cookie). So the returned string needs to be treated * carefully (not written to log files, only to a database or other storage system that would normally be used * for user data). The returned string never contains highly sensitive information (not the user's password or - * PIN), so it probably does not need to be encrypted. + * PIN), so it probably does not need to be encrypted. Treat it like a session cookie of the same bank. */ public function persist(bool $minimal = false): string { @@ -175,11 +188,16 @@ public function __unserialize(array $data): void } /** - * Use this to continue a previous FinTs Instance, for example after a TAN was needed and PHP execution was ended to - * obtain it from the user. + * Loads data from a previous {@link FinTs} instance, to reuse cached BPD/UPD information and/or to continue using + * an ongoing session. The latter is necessary to complete a TAN request when the user provides the TAN in a fresh + * PHP process. + * + * Unless it's not available to you at that time already, you can just pass the persisted instance into + * {@link FinTs::new()} instead of calling this function. * * @param string $persistedInstance The return value of {@link persist()} of a previous FinTs instance, usually - * from an earlier PHP execution. + * from an earlier PHP process. NOTE: Each persisted instance may be used only once and should be considered + * invalid afterwards. To continue the session, call {@link persist()} again. * * @throws \InvalidArgumentException */ @@ -245,8 +263,10 @@ public function setTimeouts(int $connectTimeout, int $responseTimeout) /** * Executes a strongly authenticated login action and returns it. With some banks, this requires a TAN. * @return DialogInitialization A {@link BaseAction} for the outcome of the login. You should check whether a TAN is - * needed using {@link BaseAction::needsTan()} and, if so, finish the login by passing the {@link BaseAction} - * returned here to {@link submitTan()} or {@link checkDecoupledSubmission()}. + * needed using {@link BaseAction::needsTan()} and, if so, let the user complete the TAN request from + * {@link BaseAction::getTanRequest()} and then finish the login by passing the {@link BaseAction} + * returned here to {@link submitTan()} or {@link checkDecoupledSubmission()}. See {@link execute()} for + * details. * @throws CurlException When the connection fails in a layer below the FinTS protocol. * @throws UnexpectedResponseException When the server responds with a valid but unexpected message. * @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things @@ -265,10 +285,18 @@ public function login(): DialogInitialization /** * Executes an action. Be sure to {@link login()} first. See the `\Fhp\Action` package for actions that can be - * executed with this function. Note that, after this function returns, the result of the action is stored inside - * the action itself, so you need to check {@link BaseAction::needsTan()} to see if it needs a TAN before being - * completed and use its getters in order to obtain the result. In case the action fails, the corresponding - * exception will be thrown from this function. + * executed with this function. Note that, after this function returns, the action can be in two possible states: + * 1. If {@link BaseAction::needsTan()} returns true, the action isn't completed yet because needs a TAN or other + * kind of two-factor authentication (2FA). In this case, use {@link BaseAction::getTanRequest()} to get more + * information about the TAN/2FA that is needed. Your application then needs to interact with the user to obtain + * the TAN (which should be passed into {@link submitTan()}) or to have them complete the 2FA check (which can + * be verified with {@link checkDecoupledSubmission()}). Both of those functions require passing the same + * {@link BaseAction} argument as an argument, and once they succeed, the action will be in the same completed + * state as if it had been completed right away. + * 2. If {@link BaseAction::needsTan()} returns false, the action was completed right away. Use the respective + * getters on the action instance to retrieve the result. In case the action fails, the corresponding exception + * will be thrown from this function. + * * @param BaseAction $action The action to be executed. Its {@link BaseAction::isDone()} status will be updated when * this function returns successfully. * @throws CurlException When the connection fails in a layer below the FinTS protocol. @@ -334,11 +362,12 @@ public function execute(BaseAction $action) /** * For an action where {@link BaseAction::needsTan()} returns `true` and {@link TanMode::isDecoupled()} returns - * `false`, this function sends the given $tan to the server in order to complete the action. This can be done - * asynchronously, i.e. not in the same PHP process as the original {@link execute()} call. + * `false`, this function sends the given $tan to the server to complete the action. By using {@link persist()}, + * this can be done asynchronously, i.e., not in the same PHP process as the original {@link execute()} call. * * After this function returns, the `$action` is completed. That is, its result is available through its getters - * just as if it had been completed by the original call to {@link execute()} right away. + * just as if it had been completed by the original call to {@link execute()} right away. In case the action fails, + * the corresponding exception will be thrown from this function. * * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Security_Sicherheitsverfahren_PINTAN_2020-07-10_final_version.pdf * Section B.4.2.1.1 @@ -403,10 +432,18 @@ public function submitTan(BaseAction $action, string $tan) /** * For an action where {@link BaseAction::needsTan()} returns `true` and {@link TanMode::isDecoupled()} returns * `true`, this function checks with the server whether the second factor authentication has been completed yet on - * the secondary device of the user. If so, this completes the given action and returns `true`, otherwise it - * returns `false` and the action remains in its previous, uncompleted state. - * This function can be called asynchronously, i.e. not in the same PHP process as the original {@link execute()} - * call, and also repeatedly subject to the delays specified in the {@link TanMode}. + * the secondary device of the user. + * - If so, this completes the given action and returns `true`. + * - In case the action fails, the corresponding exception will be thrown from this function. + * - If the authentication has not been completed yet, this returns `false` and the action remains in its + * previous, uncompleted state. + * + * By using {@link persist()}, this function can be called asynchronously, i.e., not in the same PHP process as the + * original {@link execute()} call. + * + * This function can be called repeatedly, subject to the delays specified in the {@link TanMode}. + * IMPORTANT: Remember to re-{@link persist()} the {@link FinTs} instance after each + * {@link checkDecoupledSubmission()} call. * * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Security_Sicherheitsverfahren_PINTAN_2020-07-10_final_version.pdf * Section B.4.2.2 @@ -494,7 +531,11 @@ public function checkDecoupledSubmission(BaseAction $action): bool } /** - * Closes open dialog/connection if any. This instance remains usable. + * Closes the session/dialog/connection, if open. This is equivalent to logging out. You should call this function + * when you're done with all the actions, but NOT when you're persisting the instance to fulfill the TAN request of + * an outstanding action. + * This FinTs object remains usable even after closing the session. You can still {@link persist()} it to benefit + * from cached BPD/UPD upon the next {@link login()}, for instance. * @throws ServerException When closing the dialog fails. */ public function close() @@ -506,9 +547,10 @@ public function close() } /** - * Assumes that the dialog (if any is open) is gone. This can be called by the application using this library when - * it just restored this FinTs instance from the persisted format after a long time, so that the dialog/session has - * most likely been closed at the server side already. + * Assumes that the session/dialog (if any is open) is gone, but keeps any cached BPD/UPD for reuse (to allow for + * faster re-login). + * This can be called by the application using this library when it just restored this FinTs instance from the + * persisted format after a long time, during which the session/dialog has most likely expired on the server side. */ public function forgetDialog() { From 35f2d2a543670a1110dc99329a138499ee1a5471 Mon Sep 17 00:00:00 2001 From: Philipp Keck Date: Sat, 16 Aug 2025 13:09:36 +0200 Subject: [PATCH 06/38] Demonstrate combined use of checkDecoupledSubmission and persist in code samples --- Samples/browser.php | 23 +++++++++++++++-- Samples/login.php | 62 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/Samples/browser.php b/Samples/browser.php index 3a16b5ce..b6cc2db4 100644 --- a/Samples/browser.php +++ b/Samples/browser.php @@ -58,18 +58,37 @@ function handleRequest(\stdClass $request, \Fhp\FinTs $fints) return ['result' => 'success']; case 'submitTan': $fints->submitTan(unserialize($persistedAction), $request->tan); + $persistedAction = null; return ['result' => 'success']; case 'checkDecoupledSubmission': if ($fints->checkDecoupledSubmission(unserialize($persistedAction))) { + $persistedAction = null; return ['result' => 'success']; } else { + // IMPORTANT: If you pull this example code apart in your real application code, remember that after + // calling checkDecoupledSubmission(), you need to call $fints->persist() again, just like this + // example code will do after we return from handleRequest() here. return ['result' => 'ongoing']; } case 'getBalances': $getAccounts = \Fhp\Action\GetSEPAAccounts::create(); - $fints->execute($getAccounts); // We assume that needsTan() is always false here. + $fints->execute($getAccounts); + if ($getAccounts->needsTan()) { + throw new \Fhp\UnsupportedException( + "This simple example code does not support strong authentication on GetSEPAAccounts calls. " . + "But in your real application, you can do so analogously to how login() is handled above." + ); + } + $getBalances = \Fhp\Action\GetBalance::create($getAccounts->getAccounts()[0], true); - $fints->execute($getBalances); // We assume that needsTan() is always false here. + $fints->execute($getBalances); + if ($getAccounts->needsTan()) { + throw new \Fhp\UnsupportedException( + "This simple example code does not support strong authentication on GetBalance calls. " . + "But in your real application, you can do so analogously to how login() is handled above." + ); + } + $balances = []; foreach ($getBalances->getBalances() as $balance) { $sdo = $balance->getGebuchterSaldo(); diff --git a/Samples/login.php b/Samples/login.php index 11f844f0..f2d020a7 100644 --- a/Samples/login.php +++ b/Samples/login.php @@ -32,7 +32,7 @@ * on its user interfaces. The implementation does not have to be in a single function like this -- it can be inlined * with the calling code, or live elsewhere. The TAN/confirmation can be obtained while the same PHP script is still * running (i.e. handleStrongAuthentication() is a blocking function that only returns once the authentication is done, - * which is useful for a CLI application), but it is also possible to interrupt the PHP execution entirely while asking + * which is useful for a CLI application), but it is also possible to interrupt the PHP process entirely while asking * for the TAN/confirmation and resume it later (which is useful for a web application). * * @param \Fhp\BaseAction $action Some action that requires strong authentication. @@ -51,6 +51,7 @@ function handleStrongAuthentication(\Fhp\BaseAction $action) /** * This function handles strong authentication for the case where the user needs to enter a TAN into the PHP * application. + * * @param \Fhp\BaseAction $action Some action that requires a TAN. * @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details. */ @@ -93,7 +94,7 @@ function handleTan(Fhp\BaseAction $action) // Optional: Instead of printing the above to the console, you can relay the information (challenge and TAN medium) // to the user in any other way (through your REST API, a push notification, ...). If waiting for the TAN requires - // you to interrupt this PHP execution and the TAN will arrive in a fresh (HTTP/REST/...) request, you can do so: + // you to interrupt this PHP process and the TAN will arrive in a fresh (HTTP/REST/...) request, you can do so: if ($optionallyPersistEverything = false) { $persistedAction = serialize($action); $persistedFints = $fints->persist(); @@ -116,7 +117,7 @@ function handleTan(Fhp\BaseAction $action) echo "Please enter the TAN:\n"; $tan = trim(fgets(STDIN)); - // Optional: If the state was persisted above, we can restore it now (imagine this is a new PHP execution). + // 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); @@ -130,14 +131,15 @@ function handleTan(Fhp\BaseAction $action) /** * This function handles strong authentication for the case where the user needs to confirm the action on another - * device. Note: Depending on the banks you need compatibility with you may not need to implement decoupled - * authentication at all, i.e. you could filter out any decoupled TanModes when letting the user choose. + * device. Note: Depending on the banks you need compatibility with, you may not need to implement decoupled + * authentication at all, i.e., you could filter out any decoupled TanModes when letting the user choose. + * * @param \Fhp\BaseAction $action Some action that requires decoupled authentication. * @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details. */ function handleDecoupled(Fhp\BaseAction $action) { - global $fints; + global $fints, $options, $credentials; $tanMode = $fints->getSelectedTanMode(); $tanRequest = $action->getTanRequest(); @@ -150,6 +152,18 @@ function handleDecoupled(Fhp\BaseAction $action) echo 'Please check this device: ' . $tanRequest->getTanMediumName() . "\n"; } + // Just like in handleTan() above, we have the option to interrupt the PHP process at this point. In fact, the + // for-loop below that deals with the polling may even be running on the client side of your larger application, + // polling your application server regularly, which spawns a new PHP process each time. Here, we demonstrate this by + // persisting the instance to a local file and restoring it (even though that's not technically necessary for a + // single-process CLI script like this). + if ($optionallyPersistEverything = false) { + $persistedAction = serialize($action); + $persistedFints = $fints->persist(); + // See handleTan() for how to deal with this in practice. + file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction])); + } + // IMPORTANT: In your real application, you don't have to use sleep() in PHP. You can persist the state in the same // way as in handleTan() and restore it later. This allows you to use some other timer mechanism (e.g. in the user's // browser). This PHP sample code just serves to show the *logic* of the polling. Alternatively, you can even do @@ -162,22 +176,44 @@ function handleDecoupled(Fhp\BaseAction $action) $tanMode->getMaxDecoupledChecks() === 0 || $attempt < $tanMode->getMaxDecoupledChecks(); ++$attempt ) { + // 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); + } + + // Check if we're done. if ($fints->checkDecoupledSubmission($action)) { echo "Confirmed.\n"; return; } echo "Still waiting...\n"; + + // THIS IS CRUCIAL if you're using persistence in between polls. You must re-persist() the instance after + // calling checkDecoupledSubmission() and before calling it the next time. Don't reuse the + // $persistedInstance from above multiple times. + if ($optionallyPersistEverything) { + $persistedAction = serialize($action); + $persistedFints = $fints->persist(); + file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction])); + } + sleep($tanMode->getPeriodicDecoupledCheckDelaySeconds()); } throw new RuntimeException("Not confirmed after $attempt attempts, which is the limit."); } elseif ($tanMode->allowsManualConfirmation()) { - do { - echo "Please type 'done' and hit Return when you've completed the authentication on the other device.\n"; - while (trim(fgets(STDIN)) !== 'done') { - echo "Try again.\n"; - } - echo "Confirming that the action is done.\n"; - } while (!$fints->checkDecoupledSubmission($action)); + echo "Please type 'done' and hit Return when you've completed the authentication on the other device.\n"; + while (trim(fgets(STDIN)) !== 'done') { + echo "Try again.\n"; + } + echo "Confirming that the action is done.\n"; + if (!$fints->checkDecoupledSubmission($action)) { + throw new RuntimeException( + "You confirmed that the authentication for action was copmleted, but the server does not think so." + ); + } echo "Confirmed\n"; } else { throw new AssertionError('Server allows neither automated polling nor manual confirmation'); From 376bb31f62a4205d64691f7007ee64a2008c6794 Mon Sep 17 00:00:00 2001 From: Leo Beal Date: Tue, 26 Aug 2025 23:21:53 +0200 Subject: [PATCH 07/38] adds a failing test --- .../Fhp/Unit/SendSEPADirectDebitTest.php | 21 + lib/Tests/resources/pain.008.002.02.xml | 1477 +++++++++++++++++ 2 files changed, 1498 insertions(+) create mode 100644 lib/Tests/Fhp/Unit/SendSEPADirectDebitTest.php create mode 100644 lib/Tests/resources/pain.008.002.02.xml diff --git a/lib/Tests/Fhp/Unit/SendSEPADirectDebitTest.php b/lib/Tests/Fhp/Unit/SendSEPADirectDebitTest.php new file mode 100644 index 00000000..269ceddc --- /dev/null +++ b/lib/Tests/Fhp/Unit/SendSEPADirectDebitTest.php @@ -0,0 +1,21 @@ + + + + + FAKEPAIN397948648101 + 2025-08-26T21:03:32Z + 300 + 14380.00 + + + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + PMT001 + DD + true + 1 + 50.00 + + SEPA + CORE + FRST + + 2025-08-30 + + Example Sports Club e.V. + + + DE11123456789012345678 + + FAKEDEFFXXX + + DE99ZZZ00000000001SEPA + + + INV001 + 50.00 + + MANDATE0012025-08-01 + + FAKEDEPPXXX + Max Mustermann + DE44123456789012345678 + Membership Fee August + + + + PMT002 + DD + true + 1 + 75.00 + + SEPA + CORE + RCUR + + 2025-08-30 + Example Sports Club e.V. + DE11123456789012345678 + FAKEDEFFXXX + + INV002 + 75.00 + + MANDATE0022024-09-15 + + FAKEXYZZXXX + Jane Doe + DE55123456789012345678 + Quarterly Training Fee + + + + \ No newline at end of file From 2c2a6afe7de55a22504183c71beeaea18172d9ae Mon Sep 17 00:00:00 2001 From: Leo Beal Date: Tue, 26 Aug 2025 23:34:12 +0200 Subject: [PATCH 08/38] tests workflow --- .github/workflows/tests.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..b7872dc0 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +# .github/workflows/tests.yml +name: tests + +on: + push: + branches: + - master + pull_request: + +jobs: + phpunit: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: [ '8.0', '8.1', '8.2', '8.3', '8.4' ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring + + - name: Validate composer.json + run: composer validate --strict + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist --no-progress + + - name: Run PHPUnit + run: ./vendor/bin/phpunit From dedb2b76e4bc05a10ff754c91f2e4556ad7c1d65 Mon Sep 17 00:00:00 2001 From: Leo Beal Date: Tue, 26 Aug 2025 23:40:09 +0200 Subject: [PATCH 09/38] Introduces lazy quantifiers --- lib/Fhp/Action/SendSEPADirectDebit.php | 4 ++-- lib/Tests/Fhp/Unit/SendSEPADirectDebitTest.php | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/Fhp/Action/SendSEPADirectDebit.php b/lib/Fhp/Action/SendSEPADirectDebit.php index 6452c508..ad60ec42 100644 --- a/lib/Fhp/Action/SendSEPADirectDebit.php +++ b/lib/Fhp/Action/SendSEPADirectDebit.php @@ -55,11 +55,11 @@ public static function create(SEPAAccount $account, string $painMessage, bool $t $nbOfTxs = substr_count($painMessage, ''); $ctrlSum = null; - if (preg_match('@.*(?[.0-9]+).*@s', $painMessage, $matches) === 1) { + if (preg_match('@.*?(?[0-9.]+).*?@s', $painMessage, $matches) === 1) { $ctrlSum = $matches['ctrlsum']; } - if (preg_match('@.*.*(?CORE|COR1|B2B).*.*@s', $painMessage, $matches) === 1) { + if (preg_match('@.*?.*?(?CORE|COR1|B2B).*?.*?@s', $painMessage, $matches) === 1) { $coreType = $matches['coretype']; } else { throw new \InvalidArgumentException('The type CORE/COR1/B2B is missing in PAIN message'); diff --git a/lib/Tests/Fhp/Unit/SendSEPADirectDebitTest.php b/lib/Tests/Fhp/Unit/SendSEPADirectDebitTest.php index 269ceddc..96d7d827 100644 --- a/lib/Tests/Fhp/Unit/SendSEPADirectDebitTest.php +++ b/lib/Tests/Fhp/Unit/SendSEPADirectDebitTest.php @@ -15,7 +15,8 @@ public function testCanSendLargeFiles() $painString = file_get_contents(__DIR__ . '/../../resources/pain.008.002.02.xml'); - //this will throw an error - SendSEPADirectDebit::create($account, $painString); + $sepa = SendSEPADirectDebit::create($account, $painString); + + $this->assertInstanceOf(SendSEPADirectDebit::class, $sepa); } } From 5713e50efec9e0467a0c38177858e90491663cac Mon Sep 17 00:00:00 2001 From: Leo Beal Date: Wed, 27 Aug 2025 00:00:02 +0200 Subject: [PATCH 10/38] Allow minor version update on composer --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 03c9e595..25d2c7fb 100644 --- a/composer.json +++ b/composer.json @@ -17,9 +17,9 @@ "ext-mbstring": "*" }, "require-dev": { - "phpunit/phpunit": "9.5.*", - "php-mock/php-mock-phpunit": "2.6.*", - "friendsofphp/php-cs-fixer": "3.*" + "phpunit/phpunit": "^9.5", + "php-mock/php-mock-phpunit": "^2.6", + "friendsofphp/php-cs-fixer": "^3.0" }, "suggest": { "monolog/monolog": "Allow sending log messages to a variety of different handlers", From 1fb1c2674a195f842ef027fea1683e25fb57e9dc Mon Sep 17 00:00:00 2001 From: Leo Beal Date: Wed, 27 Aug 2025 00:47:34 +0200 Subject: [PATCH 11/38] removes composer validate --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b7872dc0..182c67c7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,9 +26,6 @@ jobs: php-version: ${{ matrix.php }} extensions: mbstring - - name: Validate composer.json - run: composer validate --strict - - name: Install dependencies run: composer install --no-interaction --prefer-dist --no-progress From d94129dab0e5599c636ef99c91994afc1dd3c808 Mon Sep 17 00:00:00 2001 From: ampaze Date: Mon, 1 Sep 2025 12:19:59 +0200 Subject: [PATCH 12/38] Better support for new pain formats --- lib/Fhp/Action/SendSEPARealtimeTransfer.php | 23 +++++++++++++++++---- lib/Fhp/Action/SendSEPATransfer.php | 16 +++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/Fhp/Action/SendSEPARealtimeTransfer.php b/lib/Fhp/Action/SendSEPARealtimeTransfer.php index 1180124f..af790837 100644 --- a/lib/Fhp/Action/SendSEPARealtimeTransfer.php +++ b/lib/Fhp/Action/SendSEPARealtimeTransfer.php @@ -58,10 +58,25 @@ protected function createRequest(BPD $bpd, ?UPD $upd) /** @var HIIPZSv1|HIIPZSv2 $hiipzs */ $hiipzs = $bpd->requireLatestSupportedParameters('HIIPZS'); - /** @var HISPAS $hispas */ - $hispas = $bpd->requireLatestSupportedParameters('HISPAS'); - $supportedSchemas = $hispas->getParameter()->getUnterstuetzteSepaDatenformate(); - if (!in_array($this->xmlSchema, $supportedSchemas)) { + $supportedSchemas = $hiipzs->parameter->unterstuetzteSEPADatenformate; + + // If there are no SEPA formats available in the HIIPZS Parameters, we look to the general formats + if (is_null($supportedSchemas)) { + /** @var HISPAS $hispas */ + $hispas = $bpd->requireLatestSupportedParameters('HISPAS'); + $supportedSchemas = $hispas->getParameter()->getUnterstuetzteSepaDatenformate(); + } + + // Sometimes the Bank reports supported schemas with a "_GBIC_X" prefix. + // GIBC_X stands for German Banking Industry Committee and a version counter. + $xmlSchema = $this->xmlSchema; + $matchingSchemas = array_filter($supportedSchemas, function($key) use ($xmlSchema) { + // For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches + // urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4 + return strpos($key, $xmlSchema) === 0; + }, ARRAY_FILTER_USE_KEY); + + if (count($matchingSchemas) > 0) { throw new UnsupportedException("The bank does not support the XML schema $this->xmlSchema, but only " . implode(', ', $supportedSchemas)); } diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 463379d1..530983fc 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -52,13 +52,13 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs; $hasReqdExDates = false; foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) { - if (isset($pmtInfo->ReqdExctnDt) && $pmtInfo->ReqdExctnDt != '1999-01-01') { + // Checks for both, 1999-01-01 and
1999-01-01
+ if (isset($pmtInfo->ReqdExctnDt) && ($pmtInfo->ReqdExctnDt->Dt ?? $pmtInfo->ReqdExctnDt) != '1999-01-01') { $hasReqdExDates = true; break; } } - //NOW READ OUT, WICH SEGMENT SHOULD BE USED: if ($numberOfTransactions > 1 && $hasReqdExDates) { @@ -89,7 +89,17 @@ protected function createRequest(BPD $bpd, ?UPD $upd) /** @var HISPAS $hispas */ $parameters = $bpd->requireLatestSupportedParameters('HISPAS'); $supportedSchemas = $parameters->getParameter()->getUnterstuetzteSepaDatenformate(); - if (!in_array($this->xmlSchema, $supportedSchemas)) { + + // Sometimes the Bank reports supported schemas with a "_GBIC_X" prefix. + // GIBC_X stands for German Banking Industry Committee and a version counter. + $xmlSchema = $this->xmlSchema; + $matchingSchemas = array_filter($supportedSchemas, function($key) use ($xmlSchema) { + // For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches + // urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4 + return strpos($key, $xmlSchema) === 0; + }, ARRAY_FILTER_USE_KEY); + + if (count($matchingSchemas) > 0) { throw new UnsupportedException("The bank does not support the XML schema $this->xmlSchema, but only " . implode(', ', $supportedSchemas)); } From 7af5869b226e028a2fc7bd19bb94c438ccd18530 Mon Sep 17 00:00:00 2001 From: ampaze Date: Mon, 1 Sep 2025 12:38:38 +0200 Subject: [PATCH 13/38] Better support for new pain formats --- lib/Fhp/Action/SendSEPADirectDebit.php | 11 ++++++++++- lib/Fhp/Action/SendSEPARealtimeTransfer.php | 4 ++-- lib/Fhp/Action/SendSEPATransfer.php | 4 ++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/Fhp/Action/SendSEPADirectDebit.php b/lib/Fhp/Action/SendSEPADirectDebit.php index 6452c508..a4dcbe6e 100644 --- a/lib/Fhp/Action/SendSEPADirectDebit.php +++ b/lib/Fhp/Action/SendSEPADirectDebit.php @@ -148,7 +148,16 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $supportedPainNamespaces = $hispas->getParameter()->getUnterstuetzteSepaDatenformate(); } - if (!in_array($this->painNamespace, $supportedPainNamespaces)) { + // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. + // GIBC_X stands for German Banking Industry Committee and a version counter. + $xmlSchema = $this->painNamespace; + $matchingSchemas = array_filter($supportedPainNamespaces, function($key) use ($xmlSchema) { + // For example urn:iso:std:iso:20022:tech:xsd:pain.008.001.08 from the xml matches + // urn:iso:std:iso:20022:tech:xsd:pain.008.001.08_GBIC_4 + return strpos($key, $xmlSchema) === 0; + }, ARRAY_FILTER_USE_KEY); + + if (count($matchingSchemas) > 0) { throw new UnsupportedException("The bank does not support the XML schema $this->painNamespace, but only " . implode(', ', $supportedPainNamespaces)); } diff --git a/lib/Fhp/Action/SendSEPARealtimeTransfer.php b/lib/Fhp/Action/SendSEPARealtimeTransfer.php index af790837..75eaaf2a 100644 --- a/lib/Fhp/Action/SendSEPARealtimeTransfer.php +++ b/lib/Fhp/Action/SendSEPARealtimeTransfer.php @@ -67,8 +67,8 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $supportedSchemas = $hispas->getParameter()->getUnterstuetzteSepaDatenformate(); } - // Sometimes the Bank reports supported schemas with a "_GBIC_X" prefix. - // GIBC_X stands for German Banking Industry Committee and a version counter. + // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. + // GIBC_X stands for German Banking Industry Committee and a version counter. $xmlSchema = $this->xmlSchema; $matchingSchemas = array_filter($supportedSchemas, function($key) use ($xmlSchema) { // For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 530983fc..74ca31b6 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -90,8 +90,8 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $parameters = $bpd->requireLatestSupportedParameters('HISPAS'); $supportedSchemas = $parameters->getParameter()->getUnterstuetzteSepaDatenformate(); - // Sometimes the Bank reports supported schemas with a "_GBIC_X" prefix. - // GIBC_X stands for German Banking Industry Committee and a version counter. + // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. + // GIBC_X stands for German Banking Industry Committee and a version counter. $xmlSchema = $this->xmlSchema; $matchingSchemas = array_filter($supportedSchemas, function($key) use ($xmlSchema) { // For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches From 43d321c9da09685d6f1fc79298e767a890be2337 Mon Sep 17 00:00:00 2001 From: ampaze Date: Tue, 2 Sep 2025 09:12:00 +0200 Subject: [PATCH 14/38] Better support for new pain formats --- lib/Fhp/Action/SendSEPADirectDebit.php | 8 ++++---- lib/Fhp/Action/SendSEPARealtimeTransfer.php | 8 ++++---- lib/Fhp/Action/SendSEPATransfer.php | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/Fhp/Action/SendSEPADirectDebit.php b/lib/Fhp/Action/SendSEPADirectDebit.php index a4dcbe6e..b4ea99cb 100644 --- a/lib/Fhp/Action/SendSEPADirectDebit.php +++ b/lib/Fhp/Action/SendSEPADirectDebit.php @@ -151,13 +151,13 @@ protected function createRequest(BPD $bpd, ?UPD $upd) // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. // GIBC_X stands for German Banking Industry Committee and a version counter. $xmlSchema = $this->painNamespace; - $matchingSchemas = array_filter($supportedPainNamespaces, function($key) use ($xmlSchema) { + $matchingSchemas = array_filter($supportedPainNamespaces, function($value) use ($xmlSchema) { // For example urn:iso:std:iso:20022:tech:xsd:pain.008.001.08 from the xml matches // urn:iso:std:iso:20022:tech:xsd:pain.008.001.08_GBIC_4 - return strpos($key, $xmlSchema) === 0; - }, ARRAY_FILTER_USE_KEY); + return str_starts_with($value, $xmlSchema); + }); - if (count($matchingSchemas) > 0) { + if (count($matchingSchemas) === 0) { throw new UnsupportedException("The bank does not support the XML schema $this->painNamespace, but only " . implode(', ', $supportedPainNamespaces)); } diff --git a/lib/Fhp/Action/SendSEPARealtimeTransfer.php b/lib/Fhp/Action/SendSEPARealtimeTransfer.php index 75eaaf2a..1dafb8b2 100644 --- a/lib/Fhp/Action/SendSEPARealtimeTransfer.php +++ b/lib/Fhp/Action/SendSEPARealtimeTransfer.php @@ -70,13 +70,13 @@ protected function createRequest(BPD $bpd, ?UPD $upd) // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. // GIBC_X stands for German Banking Industry Committee and a version counter. $xmlSchema = $this->xmlSchema; - $matchingSchemas = array_filter($supportedSchemas, function($key) use ($xmlSchema) { + $matchingSchemas = array_filter($supportedSchemas, function($value) use ($xmlSchema) { // For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches // urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4 - return strpos($key, $xmlSchema) === 0; - }, ARRAY_FILTER_USE_KEY); + return str_starts_with($value, $xmlSchema); + }); - if (count($matchingSchemas) > 0) { + if (count($matchingSchemas) === 0) { throw new UnsupportedException("The bank does not support the XML schema $this->xmlSchema, but only " . implode(', ', $supportedSchemas)); } diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 74ca31b6..477bcbeb 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -93,13 +93,13 @@ protected function createRequest(BPD $bpd, ?UPD $upd) // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. // GIBC_X stands for German Banking Industry Committee and a version counter. $xmlSchema = $this->xmlSchema; - $matchingSchemas = array_filter($supportedSchemas, function($key) use ($xmlSchema) { + $matchingSchemas = array_filter($supportedSchemas, function($value) use ($xmlSchema) { // For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches // urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4 - return strpos($key, $xmlSchema) === 0; - }, ARRAY_FILTER_USE_KEY); + return str_starts_with($value, $xmlSchema); + }); - if (count($matchingSchemas) > 0) { + if (count($matchingSchemas) === 0) { throw new UnsupportedException("The bank does not support the XML schema $this->xmlSchema, but only " . implode(', ', $supportedSchemas)); } From 01b29609f8232774b92c3580fd6677d3ad16e6d4 Mon Sep 17 00:00:00 2001 From: ampaze Date: Wed, 3 Sep 2025 12:30:32 +0200 Subject: [PATCH 15/38] Use the UnterstuetzteSEPADatenformateTrait in all Actions to avoid pain namespace missmatches --- lib/Fhp/Action/SendSEPADirectDebit.php | 4 ++-- lib/Fhp/Action/SendSEPARealtimeTransfer.php | 4 ++-- lib/Fhp/Action/SendSEPATransfer.php | 4 ++-- ...minierteSEPAFirmenSammellastschriftEinreichenV2.php | 6 +++++- ...minierteSEPAFirmenEinzellastschriftEinreichenV2.php | 7 ++++++- ...terTerminierteSEPASammellastschriftEinreichenV2.php | 6 +++++- lib/Fhp/Segment/DSE/HIDSESv1.php | 2 +- lib/Fhp/Segment/DSE/HIDSESv2.php | 2 +- ...terTerminierteSEPAEinzellastschriftEinreichenV2.php | 7 ++++++- .../IPZ/ParameterSEPAInstantPaymentZahlungV1.php | 6 +++++- .../IPZ/ParameterSEPAInstantPaymentZahlungV2.php | 6 +++++- .../SPA/ParameterSepaKontoverbindungAnfordern.php | 7 ++++--- .../SPA/ParameterSepaKontoverbindungAnfordernV1.php | 3 ++- .../SPA/ParameterSepaKontoverbindungAnfordernV2.php | 3 ++- .../SPA/ParameterSepaKontoverbindungAnfordernV3.php | 3 ++- lib/Fhp/Segment/UnterstuetzteSEPADatenformate.php | 10 ++++++++++ ...rait.php => UnterstuetzteSEPADatenformateTrait.php} | 8 ++++---- 17 files changed, 64 insertions(+), 24 deletions(-) create mode 100644 lib/Fhp/Segment/UnterstuetzteSEPADatenformate.php rename lib/Fhp/Segment/{SPA/GetUnterstuetzteSepaDatenformateTrait.php => UnterstuetzteSEPADatenformateTrait.php} (73%) diff --git a/lib/Fhp/Action/SendSEPADirectDebit.php b/lib/Fhp/Action/SendSEPADirectDebit.php index b2c8d8b8..1c9fe599 100644 --- a/lib/Fhp/Action/SendSEPADirectDebit.php +++ b/lib/Fhp/Action/SendSEPADirectDebit.php @@ -138,14 +138,14 @@ protected function createRequest(BPD $bpd, ?UPD $upd) if ($hidxes->getVersion() === 2) { /** @var HIDMESv2|HIDSESv2 $hidxes */ - $supportedPainNamespaces = $hidxes->getParameter()->unterstuetzteSEPADatenformate; + $supportedPainNamespaces = $hidxes->getParameter()->getUnterstuetzteSEPADatenformate(); } // If there are no SEPA formats available in the HIDXES Parameters, we look to the general formats if (!is_array($supportedPainNamespaces) || count($supportedPainNamespaces) === 0) { /** @var HISPAS $hispas */ $hispas = $bpd->requireLatestSupportedParameters('HISPAS'); - $supportedPainNamespaces = $hispas->getParameter()->getUnterstuetzteSepaDatenformate(); + $supportedPainNamespaces = $hispas->getParameter()->getUnterstuetzteSEPADatenformate(); } // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. diff --git a/lib/Fhp/Action/SendSEPARealtimeTransfer.php b/lib/Fhp/Action/SendSEPARealtimeTransfer.php index 1dafb8b2..22dfda31 100644 --- a/lib/Fhp/Action/SendSEPARealtimeTransfer.php +++ b/lib/Fhp/Action/SendSEPARealtimeTransfer.php @@ -58,13 +58,13 @@ protected function createRequest(BPD $bpd, ?UPD $upd) /** @var HIIPZSv1|HIIPZSv2 $hiipzs */ $hiipzs = $bpd->requireLatestSupportedParameters('HIIPZS'); - $supportedSchemas = $hiipzs->parameter->unterstuetzteSEPADatenformate; + $supportedSchemas = $hiipzs->parameter->getUnterstuetzteSEPADatenformate(); // If there are no SEPA formats available in the HIIPZS Parameters, we look to the general formats if (is_null($supportedSchemas)) { /** @var HISPAS $hispas */ $hispas = $bpd->requireLatestSupportedParameters('HISPAS'); - $supportedSchemas = $hispas->getParameter()->getUnterstuetzteSepaDatenformate(); + $supportedSchemas = $hispas->getParameter()->getUnterstuetzteSEPADatenformate(); } // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 477bcbeb..68de23bf 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -87,8 +87,8 @@ protected function createRequest(BPD $bpd, ?UPD $upd) } /** @var HISPAS $hispas */ - $parameters = $bpd->requireLatestSupportedParameters('HISPAS'); - $supportedSchemas = $parameters->getParameter()->getUnterstuetzteSepaDatenformate(); + $hispas = $bpd->requireLatestSupportedParameters('HISPAS'); + $supportedSchemas = $hispas->getParameter()->getUnterstuetzteSEPADatenformate(); // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. // GIBC_X stands for German Banking Industry Committee and a version counter. diff --git a/lib/Fhp/Segment/BME/ParameterTerminierteSEPAFirmenSammellastschriftEinreichenV2.php b/lib/Fhp/Segment/BME/ParameterTerminierteSEPAFirmenSammellastschriftEinreichenV2.php index 34f76f12..e4ed651d 100644 --- a/lib/Fhp/Segment/BME/ParameterTerminierteSEPAFirmenSammellastschriftEinreichenV2.php +++ b/lib/Fhp/Segment/BME/ParameterTerminierteSEPAFirmenSammellastschriftEinreichenV2.php @@ -3,9 +3,13 @@ namespace Fhp\Segment\BME; use Fhp\Segment\BSE\ParameterTerminierteSEPAFirmenLastschriftEinreichenV2; +use Fhp\Segment\UnterstuetzteSEPADatenformate; +use Fhp\Segment\UnterstuetzteSEPADatenformateTrait; -class ParameterTerminierteSEPAFirmenSammellastschriftEinreichenV2 extends ParameterTerminierteSEPAFirmenLastschriftEinreichenV2 +class ParameterTerminierteSEPAFirmenSammellastschriftEinreichenV2 extends ParameterTerminierteSEPAFirmenLastschriftEinreichenV2 implements UnterstuetzteSEPADatenformate { + use UnterstuetzteSEPADatenformateTrait; + public int $maximaleAnzahlDirectDebitTransferTransactionInformation; public bool $summenfeldBenoetigt; public bool $einzelbuchungErlaubt; diff --git a/lib/Fhp/Segment/BSE/ParameterTerminierteSEPAFirmenEinzellastschriftEinreichenV2.php b/lib/Fhp/Segment/BSE/ParameterTerminierteSEPAFirmenEinzellastschriftEinreichenV2.php index ee09677d..dc256afe 100644 --- a/lib/Fhp/Segment/BSE/ParameterTerminierteSEPAFirmenEinzellastschriftEinreichenV2.php +++ b/lib/Fhp/Segment/BSE/ParameterTerminierteSEPAFirmenEinzellastschriftEinreichenV2.php @@ -2,8 +2,13 @@ namespace Fhp\Segment\BSE; -class ParameterTerminierteSEPAFirmenEinzellastschriftEinreichenV2 extends ParameterTerminierteSEPAFirmenLastschriftEinreichenV2 +use Fhp\Segment\UnterstuetzteSEPADatenformate; +use Fhp\Segment\UnterstuetzteSEPADatenformateTrait; + +class ParameterTerminierteSEPAFirmenEinzellastschriftEinreichenV2 extends ParameterTerminierteSEPAFirmenLastschriftEinreichenV2 implements UnterstuetzteSEPADatenformate { + use UnterstuetzteSEPADatenformateTrait; + /** Max Length: 4096 */ public ?string $zulaessigePurposecodes = null; diff --git a/lib/Fhp/Segment/DME/ParameterTerminierteSEPASammellastschriftEinreichenV2.php b/lib/Fhp/Segment/DME/ParameterTerminierteSEPASammellastschriftEinreichenV2.php index 02070597..ebaa9f46 100644 --- a/lib/Fhp/Segment/DME/ParameterTerminierteSEPASammellastschriftEinreichenV2.php +++ b/lib/Fhp/Segment/DME/ParameterTerminierteSEPASammellastschriftEinreichenV2.php @@ -3,9 +3,13 @@ namespace Fhp\Segment\DME; use Fhp\Segment\DSE\ParameterTerminierteSEPALastschriftEinreichenV2; +use Fhp\Segment\UnterstuetzteSEPADatenformate; +use Fhp\Segment\UnterstuetzteSEPADatenformateTrait; -class ParameterTerminierteSEPASammellastschriftEinreichenV2 extends ParameterTerminierteSEPALastschriftEinreichenV2 +class ParameterTerminierteSEPASammellastschriftEinreichenV2 extends ParameterTerminierteSEPALastschriftEinreichenV2 implements UnterstuetzteSEPADatenformate { + use UnterstuetzteSEPADatenformateTrait; + public int $maximaleAnzahlDirectDebitTransferTransactionInformation; public bool $summenfeldBenoetigt; public bool $einzelbuchungErlaubt; diff --git a/lib/Fhp/Segment/DSE/HIDSESv1.php b/lib/Fhp/Segment/DSE/HIDSESv1.php index 9e77934c..9ee8b5b5 100644 --- a/lib/Fhp/Segment/DSE/HIDSESv1.php +++ b/lib/Fhp/Segment/DSE/HIDSESv1.php @@ -15,7 +15,7 @@ class HIDSESv1 extends BaseGeschaeftsvorfallparameter implements HIDXES { public ParameterTerminierteSEPAEinzellastschriftEinreichenV1 $parameter; - public function getParameter(): SEPADirectDebitMinimalLeadTimeProvider + public function getParameter(): ParameterTerminierteSEPAEinzellastschriftEinreichenV1 { return $this->parameter; } diff --git a/lib/Fhp/Segment/DSE/HIDSESv2.php b/lib/Fhp/Segment/DSE/HIDSESv2.php index 17f45000..680d12f4 100644 --- a/lib/Fhp/Segment/DSE/HIDSESv2.php +++ b/lib/Fhp/Segment/DSE/HIDSESv2.php @@ -15,7 +15,7 @@ class HIDSESv2 extends BaseGeschaeftsvorfallparameter implements HIDXES { public ParameterTerminierteSEPAEinzellastschriftEinreichenV2 $parameter; - public function getParameter(): SEPADirectDebitMinimalLeadTimeProvider + public function getParameter(): ParameterTerminierteSEPAEinzellastschriftEinreichenV2 { return $this->parameter; } diff --git a/lib/Fhp/Segment/DSE/ParameterTerminierteSEPAEinzellastschriftEinreichenV2.php b/lib/Fhp/Segment/DSE/ParameterTerminierteSEPAEinzellastschriftEinreichenV2.php index f6a6f629..1123b5f1 100644 --- a/lib/Fhp/Segment/DSE/ParameterTerminierteSEPAEinzellastschriftEinreichenV2.php +++ b/lib/Fhp/Segment/DSE/ParameterTerminierteSEPAEinzellastschriftEinreichenV2.php @@ -2,8 +2,13 @@ namespace Fhp\Segment\DSE; -class ParameterTerminierteSEPAEinzellastschriftEinreichenV2 extends ParameterTerminierteSEPALastschriftEinreichenV2 +use Fhp\Segment\UnterstuetzteSEPADatenformate; +use Fhp\Segment\UnterstuetzteSEPADatenformateTrait; + +class ParameterTerminierteSEPAEinzellastschriftEinreichenV2 extends ParameterTerminierteSEPALastschriftEinreichenV2 implements UnterstuetzteSEPADatenformate { + use UnterstuetzteSEPADatenformateTrait; + /** Max Length: 4096 */ public ?string $zulaessigePurposecodes = null; diff --git a/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV1.php b/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV1.php index a323cd94..9ae83ab2 100644 --- a/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV1.php +++ b/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV1.php @@ -3,6 +3,8 @@ namespace Fhp\Segment\IPZ; use Fhp\Segment\BaseDeg; +use Fhp\Segment\UnterstuetzteSEPADatenformate; +use Fhp\Segment\UnterstuetzteSEPADatenformateTrait; /** * Parameter SEPA-Instant Payment Zahlung (Version 1) @@ -10,8 +12,10 @@ * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2022-04-15_final_version.pdf * Section D */ -class ParameterSEPAInstantPaymentZahlungV1 extends BaseDeg +class ParameterSEPAInstantPaymentZahlungV1 extends BaseDeg implements UnterstuetzteSEPADatenformate { + use UnterstuetzteSEPADatenformateTrait; + /** Max Length: 4096 */ public ?string $zulaessigePurposecodes = null; diff --git a/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV2.php b/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV2.php index 70a290b5..6675d306 100644 --- a/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV2.php +++ b/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV2.php @@ -3,6 +3,8 @@ namespace Fhp\lib\Fhp\Segment\IPZ; use Fhp\Segment\BaseDeg; +use Fhp\Segment\UnterstuetzteSEPADatenformate; +use Fhp\Segment\UnterstuetzteSEPADatenformateTrait; /** * Parameter SEPA-Instant Payment Zahlung (Version 2) @@ -10,8 +12,10 @@ * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2022-04-15_final_version.pdf * Section D */ -class ParameterSEPAInstantPaymentZahlungV2 extends BaseDeg +class ParameterSEPAInstantPaymentZahlungV2 extends BaseDeg implements UnterstuetzteSEPADatenformate { + use UnterstuetzteSEPADatenformateTrait; + public bool $umwandlungNachSEPAUeberweisungZulaessigErlaubt; /** Max Length: 4096 */ diff --git a/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordern.php b/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordern.php index 1f2a5039..d0b09eb7 100644 --- a/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordern.php +++ b/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordern.php @@ -3,11 +3,12 @@ namespace Fhp\Segment\SPA; +use Fhp\Segment\UnterstuetzteSEPADatenformate; + /** * Data Element Group: Parameter SEPA-Kontoverbindung anfordern */ -interface ParameterSepaKontoverbindungAnfordern +interface ParameterSepaKontoverbindungAnfordern extends UnterstuetzteSEPADatenformate { - /** @return string[] */ - public function getUnterstuetzteSepaDatenformate(): array; + } diff --git a/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV1.php b/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV1.php index cc7586df..49e51bd6 100644 --- a/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV1.php +++ b/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV1.php @@ -4,6 +4,7 @@ namespace Fhp\Segment\SPA; use Fhp\Segment\BaseDeg; +use Fhp\Segment\UnterstuetzteSEPADatenformateTrait; /** * Data Element Group: Parameter SEPA-Kontoverbindung anfordern (Version 1) @@ -13,7 +14,7 @@ */ class ParameterSepaKontoverbindungAnfordernV1 extends BaseDeg implements ParameterSepaKontoverbindungAnfordern { - use GetUnterstuetzteSepaDatenformateTrait; + use UnterstuetzteSEPADatenformateTrait; public bool $einzelkontenabrufErlaubt; public bool $nationaleKontoverbindungErlaubt; diff --git a/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV2.php b/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV2.php index e2ed1ea1..f82a95ec 100644 --- a/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV2.php +++ b/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV2.php @@ -4,6 +4,7 @@ namespace Fhp\Segment\SPA; use Fhp\Segment\BaseDeg; +use Fhp\Segment\UnterstuetzteSEPADatenformateTrait; /** * Data Element Group: Parameter SEPA-Kontoverbindung anfordern (Version 2) @@ -13,7 +14,7 @@ */ class ParameterSepaKontoverbindungAnfordernV2 extends BaseDeg implements ParameterSepaKontoverbindungAnfordern { - use GetUnterstuetzteSepaDatenformateTrait; + use UnterstuetzteSEPADatenformateTrait; public bool $einzelkontenabrufErlaubt; public bool $nationaleKontoverbindungErlaubt; diff --git a/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV3.php b/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV3.php index 8dc1bbec..901a10a6 100644 --- a/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV3.php +++ b/lib/Fhp/Segment/SPA/ParameterSepaKontoverbindungAnfordernV3.php @@ -4,6 +4,7 @@ namespace Fhp\Segment\SPA; use Fhp\Segment\BaseDeg; +use Fhp\Segment\UnterstuetzteSEPADatenformateTrait; /** * Data Element Group: Parameter SEPA-Kontoverbindung anfordern (Version 3) @@ -13,7 +14,7 @@ */ class ParameterSepaKontoverbindungAnfordernV3 extends BaseDeg implements ParameterSepaKontoverbindungAnfordern { - use GetUnterstuetzteSepaDatenformateTrait; + use UnterstuetzteSEPADatenformateTrait; public bool $einzelkontenabrufErlaubt; public bool $nationaleKontoverbindungErlaubt; diff --git a/lib/Fhp/Segment/UnterstuetzteSEPADatenformate.php b/lib/Fhp/Segment/UnterstuetzteSEPADatenformate.php new file mode 100644 index 00000000..e3e5d1af --- /dev/null +++ b/lib/Fhp/Segment/UnterstuetzteSEPADatenformate.php @@ -0,0 +1,10 @@ + 'urn:iso:std:iso:20022:tech:xsd:', '.xsd' => '', ]); - }, $this->unterstuetzteSepaDatenformate); + }, $this->unterstuetzteSepaDatenformate ?? $this->unterstuetzteSEPADatenformate ?? []); } } From b0c999136a849883185d8927e8e6b83440ef242e Mon Sep 17 00:00:00 2001 From: ampaze Date: Thu, 18 Sep 2025 09:04:02 +0200 Subject: [PATCH 16/38] Check if VoP is supported and required by the bank --- lib/Fhp/Action/SendSEPATransferVoP.php | 28 +++++++++++++++++++ lib/Fhp/Segment/VPP/HIVPPSv1.php | 16 +++++++++++ .../ParameterNamensabgleichPruefauftrag.php | 22 +++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 lib/Fhp/Action/SendSEPATransferVoP.php create mode 100644 lib/Fhp/Segment/VPP/HIVPPSv1.php create mode 100644 lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php diff --git a/lib/Fhp/Action/SendSEPATransferVoP.php b/lib/Fhp/Action/SendSEPATransferVoP.php new file mode 100644 index 00000000..ea3bbf00 --- /dev/null +++ b/lib/Fhp/Action/SendSEPATransferVoP.php @@ -0,0 +1,28 @@ +getLatestSupportedParameters('HIVPPS')) { + // Check if the request segment is in the list of VoP-supported segments + if (in_array($requestSegment->getName(), $hivpps->parameter->VoPPflichtigerZahlungsverkehrsauftrag)) { + throw new UnsupportedException('The bank requires VoP for this request, this is not implemented yet.'); + } + } + + return $requestSegments; + } +} \ No newline at end of file diff --git a/lib/Fhp/Segment/VPP/HIVPPSv1.php b/lib/Fhp/Segment/VPP/HIVPPSv1.php new file mode 100644 index 00000000..05db0d90 --- /dev/null +++ b/lib/Fhp/Segment/VPP/HIVPPSv1.php @@ -0,0 +1,16 @@ + Date: Thu, 18 Sep 2025 12:55:33 +0200 Subject: [PATCH 17/38] Send the VoP reqeust along with the SEPA transfer request --- lib/Fhp/Action/SendSEPATransferVoP.php | 11 ++++++++++- lib/Fhp/Segment/VPP/HKVPPv1.php | 19 +++++++++++++++++++ .../VPP/UnterstuetztePaymentStatusReports.php | 11 +++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 lib/Fhp/Segment/VPP/HKVPPv1.php create mode 100644 lib/Fhp/Segment/VPP/UnterstuetztePaymentStatusReports.php diff --git a/lib/Fhp/Action/SendSEPATransferVoP.php b/lib/Fhp/Action/SendSEPATransferVoP.php index ea3bbf00..019bf8c7 100644 --- a/lib/Fhp/Action/SendSEPATransferVoP.php +++ b/lib/Fhp/Action/SendSEPATransferVoP.php @@ -5,6 +5,7 @@ use Fhp\Protocol\BPD; use Fhp\Protocol\UPD; use Fhp\Segment\VPP\HIVPPSv1; +use Fhp\Segment\VPP\HKVPPv1; use Fhp\UnsupportedException; class SendSEPATransferVoP extends SendSEPATransfer @@ -19,7 +20,15 @@ protected function createRequest(BPD $bpd, ?UPD $upd) 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)) { - throw new UnsupportedException('The bank requires VoP for this request, this is not implemented yet.'); + + $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]; } } diff --git a/lib/Fhp/Segment/VPP/HKVPPv1.php b/lib/Fhp/Segment/VPP/HKVPPv1.php new file mode 100644 index 00000000..557d807c --- /dev/null +++ b/lib/Fhp/Segment/VPP/HKVPPv1.php @@ -0,0 +1,19 @@ + Date: Fri, 19 Sep 2025 09:30:02 +0200 Subject: [PATCH 18/38] Coding style and documentation --- lib/Fhp/Action/SendSEPATransferVoP.php | 10 +++++++--- lib/Fhp/Segment/VPP/HKVPPv1.php | 6 ++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/Fhp/Action/SendSEPATransferVoP.php b/lib/Fhp/Action/SendSEPATransferVoP.php index 019bf8c7..265f96e2 100644 --- a/lib/Fhp/Action/SendSEPATransferVoP.php +++ b/lib/Fhp/Action/SendSEPATransferVoP.php @@ -6,8 +6,11 @@ use Fhp\Protocol\UPD; use Fhp\Segment\VPP\HIVPPSv1; 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 function createRequest(BPD $bpd, ?UPD $upd) @@ -15,15 +18,16 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $requestSegment = parent::createRequest($bpd, $upd); $requestSegments = [$requestSegment]; - /** @var HIVPPSv1 $hivpps */ // 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)) { $hkvpp = HKVPPv1::createEmpty(); - # For now just pretend we support all formats + // For now just pretend we support all formats $supportedFormats = explode(';', $hivpps->parameter->unterstuetztePaymentStatusReportDatenformate); $hkvpp->unterstuetztePaymentStatusReports->paymentStatusReportDescriptor = $supportedFormats; diff --git a/lib/Fhp/Segment/VPP/HKVPPv1.php b/lib/Fhp/Segment/VPP/HKVPPv1.php index 557d807c..986d6c01 100644 --- a/lib/Fhp/Segment/VPP/HKVPPv1.php +++ b/lib/Fhp/Segment/VPP/HKVPPv1.php @@ -5,6 +5,12 @@ 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; From 868560f435666c1116c909fb6ddc631b7d1d3fda Mon Sep 17 00:00:00 2001 From: ampaze Date: Fri, 19 Sep 2025 11:53:58 +0200 Subject: [PATCH 19/38] Handling the VoP response WIP --- lib/Fhp/Action/SendSEPATransferVoP.php | 24 +++++++++++++- lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php | 6 ++++ .../ErgebnisVopPruefungEinzeltransaktion.php | 19 +++++++++++ lib/Fhp/Segment/VPP/HIVPPv1.php | 33 +++++++++++++++++++ .../ParameterNamensabgleichPruefauftrag.php | 2 +- 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 lib/Fhp/Segment/VPP/ErgebnisVopPruefungEinzeltransaktion.php create mode 100644 lib/Fhp/Segment/VPP/HIVPPv1.php diff --git a/lib/Fhp/Action/SendSEPATransferVoP.php b/lib/Fhp/Action/SendSEPATransferVoP.php index 265f96e2..d433131c 100644 --- a/lib/Fhp/Action/SendSEPATransferVoP.php +++ b/lib/Fhp/Action/SendSEPATransferVoP.php @@ -3,9 +3,13 @@ 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\HKVPPv1; +use Fhp\UnsupportedException; /** * Initiates an outgoing wire transfer in SEPA format (PAIN XML) with VoP. @@ -23,7 +27,7 @@ protected function createRequest(BPD $bpd, ?UPD $upd) /** @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)) { + if (in_array($requestSegment->getName(), $hivpps->parameter->vopPflichtigerZahlungsverkehrsauftrag)) { $hkvpp = HKVPPv1::createEmpty(); @@ -38,4 +42,22 @@ protected function createRequest(BPD $bpd, ?UPD $upd) return $requestSegments; } + + public function processResponse(Message $response) + { + // The Bank does not want a separate HKVPA ("VoP Ausführungsauftrag"). + if ($response->findRueckmeldung(Rueckmeldungscode::VOP_AUSFUEHRUNGSAUFTRAG_NICHT_BENOETIGT) !== null) { + parent::processResponse($response); + return; + } + + // The user needs to check the result of the name check. + if ($response->findRueckmeldung(Rueckmeldungscode::VOP_ERGEBNIS_NAMENSABGLEICH_PRUEFEN) !== null) { + + /** @var HIVPPv1 $hivpp */ + $hivpp = $response->findSegment(HIVPPv1::class); + + throw new UnsupportedException('The user needs to check the result of the name check. This is not implemented yet.'); + } + } } \ No newline at end of file diff --git a/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php b/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php index 7a53a5e2..256fc922 100644 --- a/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php +++ b/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php @@ -79,6 +79,12 @@ public static function isError(int $code): bool */ public const PAGINATION = 3040; + public const VOP_KEINE_NAMENSABWEICHUNG = 0025; + + public const VOP_ERGEBNIS_NAMENSABGLEICH_PRUEFEN = 3090; + + public const VOP_AUSFUEHRUNGSAUFTRAG_NICHT_BENOETIGT = 3091; + /** * Zugelassene Ein- und Zwei-Schritt-Verfahren für den Benutzer (+ Rückmeldungsparameter). * The parameters reference the VerfahrensparameterZweiSchrittVerfahren.sicherheitsfunktion values (900..997) from diff --git a/lib/Fhp/Segment/VPP/ErgebnisVopPruefungEinzeltransaktion.php b/lib/Fhp/Segment/VPP/ErgebnisVopPruefungEinzeltransaktion.php new file mode 100644 index 00000000..1b33e14d --- /dev/null +++ b/lib/Fhp/Segment/VPP/ErgebnisVopPruefungEinzeltransaktion.php @@ -0,0 +1,19 @@ + Date: Fri, 19 Sep 2025 12:37:22 +0200 Subject: [PATCH 20/38] Handling the VoP response WIP --- lib/Fhp/Action/SendSEPATransferVoP.php | 29 +++++++++++++++++++++ lib/Fhp/BaseAction.php | 10 +++++++ lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php | 4 +++ 3 files changed, 43 insertions(+) diff --git a/lib/Fhp/Action/SendSEPATransferVoP.php b/lib/Fhp/Action/SendSEPATransferVoP.php index d433131c..e9c7def1 100644 --- a/lib/Fhp/Action/SendSEPATransferVoP.php +++ b/lib/Fhp/Action/SendSEPATransferVoP.php @@ -17,6 +17,10 @@ */ class SendSEPATransferVoP extends SendSEPATransfer { + protected $vopRequired = false; + protected $vopIsPending = false; + protected $vopNeedsConfirmation = false; + protected function createRequest(BPD $bpd, ?UPD $upd) { $requestSegment = parent::createRequest($bpd, $upd); @@ -29,6 +33,8 @@ protected function createRequest(BPD $bpd, ?UPD $upd) // Check if the request segment is in the list of VoP-supported segments if (in_array($requestSegment->getName(), $hivpps->parameter->vopPflichtigerZahlungsverkehrsauftrag)) { + $this->vopRequired = true; + $hkvpp = HKVPPv1::createEmpty(); // For now just pretend we support all formats @@ -45,19 +51,42 @@ protected function createRequest(BPD $bpd, ?UPD $upd) public function processResponse(Message $response) { + // The bank accepted the request as is. + if ($response->findRueckmeldung(Rueckmeldungscode::ENTGEGENGENOMMEN) !== null || $response->findRueckmeldung(Rueckmeldungscode::AUSGEFUEHRT) !== null) { + parent::processResponse($response); + return; + } + // The Bank does not want a separate HKVPA ("VoP Ausführungsauftrag"). if ($response->findRueckmeldung(Rueckmeldungscode::VOP_AUSFUEHRUNGSAUFTRAG_NICHT_BENOETIGT) !== null) { parent::processResponse($response); return; } + if ($response->findRueckmeldung(Rueckmeldungscode::VOP_NAMENSABGLEICH_IST_NOCH_IN_BEARBEITUNG) !== null) { + $this->vopIsPending = true; + return; + } + // The user needs to check the result of the name check. if ($response->findRueckmeldung(Rueckmeldungscode::VOP_ERGEBNIS_NAMENSABGLEICH_PRUEFEN) !== null) { + $this->vopNeedsConfirmation = true; /** @var HIVPPv1 $hivpp */ $hivpp = $response->findSegment(HIVPPv1::class); throw new UnsupportedException('The user needs to check the result of the name check. This is not implemented yet.'); } } + + public function needsTime() + { + return $this->vopIsPending; + } + + public function needsConfirmation() + { + return $this->vopNeedsConfirmation; + } + } \ No newline at end of file diff --git a/lib/Fhp/BaseAction.php b/lib/Fhp/BaseAction.php index d36dec6a..93662fa0 100644 --- a/lib/Fhp/BaseAction.php +++ b/lib/Fhp/BaseAction.php @@ -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 diff --git a/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php b/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php index 256fc922..d3a88c31 100644 --- a/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php +++ b/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php @@ -85,6 +85,10 @@ public static function isError(int $code): bool 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 From 9b8c0d3c7f3e56f5e6b6ea296e5ac0a9b7a4e30b Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Wed, 8 Oct 2025 10:55:09 +0200 Subject: [PATCH 21/38] Update ParameterSEPAInstantPaymentZahlungV2.php --- lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV2.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV2.php b/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV2.php index 6675d306..4e40e3e4 100644 --- a/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV2.php +++ b/lib/Fhp/Segment/IPZ/ParameterSEPAInstantPaymentZahlungV2.php @@ -1,6 +1,6 @@ Date: Wed, 8 Oct 2025 10:55:23 +0200 Subject: [PATCH 22/38] Update HIIPZSv2.php --- lib/Fhp/Segment/IPZ/HIIPZSv2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Fhp/Segment/IPZ/HIIPZSv2.php b/lib/Fhp/Segment/IPZ/HIIPZSv2.php index 18a55f34..e78c92ba 100644 --- a/lib/Fhp/Segment/IPZ/HIIPZSv2.php +++ b/lib/Fhp/Segment/IPZ/HIIPZSv2.php @@ -2,7 +2,7 @@ namespace Fhp\Segment\IPZ; -use Fhp\lib\Fhp\Segment\IPZ\ParameterSEPAInstantPaymentZahlungV2; +use Fhp\Segment\IPZ\ParameterSEPAInstantPaymentZahlungV2; use Fhp\Segment\BaseGeschaeftsvorfallparameter; use Fhp\Segment\BaseSegment; From eb781da8817de91410bb889ef9318b3a5f09c5e1 Mon Sep 17 00:00:00 2001 From: ampaze Date: Thu, 9 Oct 2025 09:05:44 +0200 Subject: [PATCH 23/38] Typo --- lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php b/lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php index 17ff0618..23cc6a6c 100644 --- a/lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php +++ b/lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php @@ -2,7 +2,9 @@ namespace Fhp\Segment\VPP; -class ParameterNamensabgleichPruefauftrag +use Fhp\Segment\BaseDeg; + +class ParameterNamensabgleichPruefauftrag extends BaseDeg { public int $maximaleAnzahlCreditTransferTransactionInformationOptIn; From 55d127baa4144689a6eb7d5f7e697508688a0fdc Mon Sep 17 00:00:00 2001 From: ampaze Date: Thu, 9 Oct 2025 12:29:04 +0200 Subject: [PATCH 24/38] Handling the VoP response WIP --- lib/Fhp/Action/SendSEPATransfer.php | 4 +- lib/Fhp/Action/SendSEPATransferVoP.php | 58 +++++++++++++++---- lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php | 8 +++ lib/Fhp/Segment/VPP/HKVPAv1.php | 17 ++++++ lib/Fhp/Segment/VPP/HKVPPv1.php | 7 +++ .../ParameterNamensabgleichPruefauftrag.php | 2 +- 6 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 lib/Fhp/Segment/VPP/HKVPAv1.php diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 68de23bf..7c35160a 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 index e9c7def1..5cc6e908 100644 --- a/lib/Fhp/Action/SendSEPATransferVoP.php +++ b/lib/Fhp/Action/SendSEPATransferVoP.php @@ -21,8 +21,17 @@ class SendSEPATransferVoP extends SendSEPATransfer protected $vopIsPending = false; protected $vopNeedsConfirmation = false; + public ?HKVPPv1 $hkvpp = null; + public ?HIVPPv1 $hivpp = null; + protected function createRequest(BPD $bpd, ?UPD $upd) { + // Do we need to ask vor the VoP Result? + if ($this->vopIsPending) { + $this->hkvpp->pollingId = $this->hivpp->pollingId; + return $this->hkvpp; + } + $requestSegment = parent::createRequest($bpd, $upd); $requestSegments = [$requestSegment]; @@ -35,14 +44,22 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $this->vopRequired = true; - $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]; + // 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]; + } } } @@ -59,24 +76,43 @@ public function processResponse(Message $response) // The Bank does not want a separate HKVPA ("VoP Ausführungsauftrag"). if ($response->findRueckmeldung(Rueckmeldungscode::VOP_AUSFUEHRUNGSAUFTRAG_NICHT_BENOETIGT) !== null) { + $this->vopRequired = false; + $this->vopIsPending = 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; + } + + $this->hivpp = $response->findSegment(HIVPPv1::class); + + // The bank has discarded the request, and wants us to resend it with a HKVPA + // This can happen even if the name matches. + if ($response->findRueckmeldung(Rueckmeldungscode::FREIGABE_KANN_NICHT_ERTEILT_WERDEN) !== null) { + // Result is available + if ($this->hivpp->vopId) { + $this->vopNeedsConfirmation = true; + } else { + $this->vopIsPending = true; + } return; } // The user needs to check the result of the name check. + // This can be sent by the bank even if the name matches. if ($response->findRueckmeldung(Rueckmeldungscode::VOP_ERGEBNIS_NAMENSABGLEICH_PRUEFEN) !== null) { + $this->vopIsPending = false; $this->vopNeedsConfirmation = true; - /** @var HIVPPv1 $hivpp */ - $hivpp = $response->findSegment(HIVPPv1::class); - throw new UnsupportedException('The user needs to check the result of the name check. This is not implemented yet.'); + return; } + throw new UnsupportedException('Unexpected state in VoP process'); } public function needsTime() diff --git a/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php b/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php index d3a88c31..f0fc16ef 100644 --- a/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php +++ b/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php @@ -109,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. diff --git a/lib/Fhp/Segment/VPP/HKVPAv1.php b/lib/Fhp/Segment/VPP/HKVPAv1.php new file mode 100644 index 00000000..111294ca --- /dev/null +++ b/lib/Fhp/Segment/VPP/HKVPAv1.php @@ -0,0 +1,17 @@ +unterstuetztePaymentStatusReports = new UnterstuetztePaymentStatusReports(); + return $hkvpp; + } } \ No newline at end of file diff --git a/lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php b/lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php index 23cc6a6c..8eb0aeea 100644 --- a/lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php +++ b/lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftrag.php @@ -19,6 +19,6 @@ class ParameterNamensabgleichPruefauftrag extends BaseDeg public string $unterstuetztePaymentStatusReportDatenformate; - /** @var string[] @Max(N) Max length each: 6 */ + /** @var string[] @Max(999999) Max length each: 6 */ public array $vopPflichtigerZahlungsverkehrsauftrag; } \ No newline at end of file From a2a474ab00a02a804b3ee70a6b54896f139851a5 Mon Sep 17 00:00:00 2001 From: ampaze Date: Fri, 10 Oct 2025 13:12:22 +0200 Subject: [PATCH 25/38] Handling the VoP response --- lib/Fhp/Action/SendSEPATransferVoP.php | 61 ++++++++++++++------- lib/Fhp/FinTs.php | 10 +++- lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php | 2 +- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/lib/Fhp/Action/SendSEPATransferVoP.php b/lib/Fhp/Action/SendSEPATransferVoP.php index 5cc6e908..539be2cc 100644 --- a/lib/Fhp/Action/SendSEPATransferVoP.php +++ b/lib/Fhp/Action/SendSEPATransferVoP.php @@ -8,6 +8,7 @@ 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; @@ -21,20 +22,37 @@ class SendSEPATransferVoP extends SendSEPATransfer 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 vor the VoP Result? + // 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) { + + $hkvpa = HKVPAv1::createEmpty(); + $hkvpa->vopId = $this->hivpp->vopId; + return [$hkvpa, $requestSegment]; + } + // Check if VoP is supported by the bank /** @var HIVPPSv1 $hivpps */ @@ -68,8 +86,12 @@ protected function createRequest(BPD $bpd, ?UPD $upd) public function processResponse(Message $response) { + $this->vopIsPending = false; + $this->hivpp = $response->findSegment(HIVPPv1::class); + // 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; } @@ -77,7 +99,6 @@ public function processResponse(Message $response) // The Bank does not want a separate HKVPA ("VoP Ausführungsauftrag"). if ($response->findRueckmeldung(Rueckmeldungscode::VOP_AUSFUEHRUNGSAUFTRAG_NICHT_BENOETIGT) !== null) { $this->vopRequired = false; - $this->vopIsPending = false; $this->vopNeedsConfirmation = false; parent::processResponse($response); return; @@ -89,27 +110,23 @@ public function processResponse(Message $response) return; } - $this->hivpp = $response->findSegment(HIVPPv1::class); - - // The bank has discarded the request, and wants us to resend it with a HKVPA - // This can happen even if the name matches. - if ($response->findRueckmeldung(Rueckmeldungscode::FREIGABE_KANN_NICHT_ERTEILT_WERDEN) !== null) { - // Result is available - if ($this->hivpp->vopId) { - $this->vopNeedsConfirmation = true; - } else { - $this->vopIsPending = true; - } - return; + if (($pagination = $response->findRueckmeldung(Rueckmeldungscode::PAGINATION)) !== null) { + $this->paginationToken = $pagination->rueckmeldungsparameter[0]; } - // The user needs to check the result of the name check. - // This can be sent by the bank even if the name matches. - if ($response->findRueckmeldung(Rueckmeldungscode::VOP_ERGEBNIS_NAMENSABGLEICH_PRUEFEN) !== null) { - - $this->vopIsPending = false; + if ( + // 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; } throw new UnsupportedException('Unexpected state in VoP process'); @@ -125,4 +142,8 @@ public function needsConfirmation() return $this->vopNeedsConfirmation; } + public function setConfirmed() + { + $this->vopConfirmed = true; + } } \ No newline at end of file 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/HIRMS/Rueckmeldungscode.php b/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php index f0fc16ef..de648f04 100644 --- a/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php +++ b/lib/Fhp/Segment/HIRMS/Rueckmeldungscode.php @@ -79,7 +79,7 @@ public static function isError(int $code): bool */ public const PAGINATION = 3040; - public const VOP_KEINE_NAMENSABWEICHUNG = 0025; + public const VOP_KEINE_NAMENSABWEICHUNG = 25; public const VOP_ERGEBNIS_NAMENSABGLEICH_PRUEFEN = 3090; From 7e4bd723dc8afd5d9702d2fc48c511766f255ae6 Mon Sep 17 00:00:00 2001 From: ampaze Date: Mon, 13 Oct 2025 11:27:54 +0200 Subject: [PATCH 26/38] Integrationtest for VoP SEPA transfer --- .../Atruvia/AtruviaIntegrationTestBase.php | 90 +++++++++++++++++++ .../Atruvia/SendTransferVoPTest.php | 81 +++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 lib/Tests/Fhp/Integration/Atruvia/AtruviaIntegrationTestBase.php create mode 100644 lib/Tests/Fhp/Integration/Atruvia/SendTransferVoPTest.php 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..33976f41 --- /dev/null +++ b/lib/Tests/Fhp/Integration/Atruvia/SendTransferVoPTest.php @@ -0,0 +1,81 @@ +' . "\n" . 'M12345678902025-10-10T12:52:56+02:00110.00PRIVATE__________________P12345678TRF110.00SEPA
1999-01-01
PRIVATE__________________DE00ABCDEFGH1234567890ABCDEFGHIJKSLEVNOTPROVIDED10.00EmpfängerDE00ABCDEFGH1234567890Testüberweisung
'; + + public const SEND_TRANSFER_REQUEST = + "HKVPP:3:1+urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.002.001.10'HKCCS:4:1+DE00ABCDEFGH1234567890:ABCDEFGHIJK:1234567890::280:11223344+urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.09+@1161@" . + self::XML_PAYLOAD . + "'HKTAN:5:7+4+HKCCS'" + ; + public const SEND_TRANSFER_RESPONSE = "HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.+3905::Es wurde keine Challenge erzeugt.'HIRMS:4:2:3+3040::Es liegen weitere Informationen vor.:staticscrollref'HIRMS:5:2:5+3945::Freigabe ohne VOP-Bestätigung nicht möglich.'HIVPP:6:1:3+++@36@c0f5c2a4-ebb7-4e72-be44-c68742177a2b+++++2'"; + + public const REQUEST_VOP_REPORT_REQUEST = "HKVPP:3:1+urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.002.001.10+@36@c0f5c2a4-ebb7-4e72-be44-c68742177a2b++staticscrollref'"; + public const REQUEST_VOP_REPORT_RESPONSE = "HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.'HIRMS:4:2:3+3090::Ergebnis des Namensabgleichs prüfen.'HIVPP:5:1:3+@36@5e3b5c99-df27-4d42-835b-18b35d0c66ff+++urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.002.001.10+@3600@UTF8XMLPAYLOAD++Bei mindestens einem Zahlungsempfänger stimmt der Name mit dem für diese IBAN bei der Zahlungsempfängerbank hinterlegten Namen nicht oder nur nahezu überein.
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 REQUEST_VOP_REPORT_RESPONSE_XML_PAYLOAD = "ATRUVIA-20251010-125258-X2025-10-10T12:52:58.283+02:00ABCDEFGHIJKM1234567890pain.001.001.09110.00RVCMRCVC Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt mit dem für diese IBANRCVC hinterlegten Namen bei der Zahlungsempfängerbank überein.RVMC Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt nahezu mit dem für diese IBANRVMC hinterlegten Namen bei der Zahlungsempfängerbank überein. Die Autorisierung der ZahlungRVMC kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen Inhaber nichtRVMC der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nicht fürRVMC die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.RVNM Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt nicht mit dem für diese IBAN hinter-RVNM legten Namen bei der Zahlungsempfängerbank überein. Bitte prüfen Sie den Empfängernamen. Die Autori-RVNM sierung der Zahlung kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen InhaberRVNM nicht der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nichtRVNM für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.RVNA Der von Ihnen eingegebene Name des Zahlungsempfängers konnte nicht mit dem für diese IBAN hinter-RVNA legten Namen bei der Zahlungsempfängerbank abgeglichen werden (z.B. technischer Fehler). Die Autori-RVNA sierung der Zahlung kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen InhaberRVNA nicht der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nichtRVNA für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.0RCVC0RVMC1RVNM0RVNA176009357610RCVC0RVMC1RVNM0RVNANOTPROVIDEDRVNMTestempfängerDE00ABCDEFGH1234567890"; + + public const CONFIRM_VOP_REQUEST = + "HKVPA:3:1+@36@5e3b5c99-df27-4d42-835b-18b35d0c66ff'HKCCS:4:1+DE00ABCDEFGH1234567890:ABCDEFGHIJK:1234567890::280:11223344+urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.09+@1161@" . + self::XML_PAYLOAD . + "'HKTAN:5:7+4+HKCCS'" + ; + public const CONFIRM_VOP_RESPONSE = "HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.'HIRMS:4:2:3+0020::Ausführungsbestätigung nach Namensabgleich erhalten.'HIRMS:5:2:5+3955::Sicherheitsfreigabe erfolgt über anderen Kanal.'HITAN:6:7:5+4++1234567890123456789012345678+Bitte bestätigen Sie den Vorgang in Ihrer SecureGo plus App'"; + + public const CHECK_DECOUPLED_SUBMISSION_REQUEST = "HKTAN:3:7+S++++1234567890123456789012345678+N'"; + public const CHECK_DECOUPLED_SUBMISSION_RESPONSE = "HIRMG:3:2+0010::Nachricht entgegengenommen.'HIRMS:4:2:3+0020::*SEPA-Einzelüberweisung erfolgreich+0900::Freigabe erfolgreich'HITAN:5:7:3+S++1234567890123456789012345678'"; + + /** + * @throws \Throwable + */ + public function test() + { + $this->initDialog(); + + $transferAction = $this->getTransferAction(); + + $this->expectMessage(static::SEND_TRANSFER_REQUEST, mb_convert_encoding(static::SEND_TRANSFER_RESPONSE, 'ISO-8859-1', 'UTF-8')); + + $this->fints->execute($transferAction); + + while ($transferAction->needsTime()) { + # As this is a test, we don't need to actually wait. + #$wait = $transferAction->hivpp->wartezeitVorNaechsterAbfrage; + #sleep($wait); + + $this->expectMessage(static::REQUEST_VOP_REPORT_REQUEST, str_replace('UTF8XMLPAYLOAD', self::REQUEST_VOP_REPORT_RESPONSE_XML_PAYLOAD, mb_convert_encoding(static::REQUEST_VOP_REPORT_RESPONSE, 'ISO-8859-1', 'UTF-8'))); + + $this->fints->execute($transferAction); + } + + if ($transferAction->needsConfirmation()) { + $transferAction->setConfirmed(); + $this->expectMessage(static::CONFIRM_VOP_REQUEST, mb_convert_encoding(static::CONFIRM_VOP_RESPONSE, 'ISO-8859-1', 'UTF-8')); + $this->fints->execute($transferAction); + } + + if ($transferAction->needsTan()) { + $tanMode = $this->fints->getSelectedTanMode(); + + if ($tanMode->isDecoupled()) { + $this->expectMessage(static::CHECK_DECOUPLED_SUBMISSION_REQUEST, mb_convert_encoding(static::CHECK_DECOUPLED_SUBMISSION_RESPONSE, 'ISO-8859-1', 'UTF-8')); + $this->fints->checkDecoupledSubmission($transferAction); + } + } + + $transferAction->ensureDone(); + + } + + protected function getTransferAction(): SendSEPATransferVoP + { + $account = $this->getTestAccount(); + return SendSEPATransferVoP::create($account, self::XML_PAYLOAD); + } +} From 60a62eea2f775a3bd5a0f3dc2d25205a0a90e7b1 Mon Sep 17 00:00:00 2001 From: ampaze Date: Mon, 13 Oct 2025 13:29:42 +0200 Subject: [PATCH 27/38] Fixes for VoP case when the name matches --- lib/Fhp/Action/SendSEPATransferVoP.php | 18 +++++----- .../ErgebnisVopPruefungEinzeltransaktion.php | 6 ++-- .../Atruvia/SendTransferVoPTest.php | 33 ++++++++++++++----- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/lib/Fhp/Action/SendSEPATransferVoP.php b/lib/Fhp/Action/SendSEPATransferVoP.php index 539be2cc..4624efe9 100644 --- a/lib/Fhp/Action/SendSEPATransferVoP.php +++ b/lib/Fhp/Action/SendSEPATransferVoP.php @@ -89,13 +89,6 @@ public function processResponse(Message $response) $this->vopIsPending = false; $this->hivpp = $response->findSegment(HIVPPv1::class); - // 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; - } - // The Bank does not want a separate HKVPA ("VoP Ausführungsauftrag"). if ($response->findRueckmeldung(Rueckmeldungscode::VOP_AUSFUEHRUNGSAUFTRAG_NICHT_BENOETIGT) !== null) { $this->vopRequired = false; @@ -115,9 +108,10 @@ public function processResponse(Message $response) } 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 + || $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 @@ -129,6 +123,14 @@ public function processResponse(Message $response) } 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'); } diff --git a/lib/Fhp/Segment/VPP/ErgebnisVopPruefungEinzeltransaktion.php b/lib/Fhp/Segment/VPP/ErgebnisVopPruefungEinzeltransaktion.php index 1b33e14d..41783426 100644 --- a/lib/Fhp/Segment/VPP/ErgebnisVopPruefungEinzeltransaktion.php +++ b/lib/Fhp/Segment/VPP/ErgebnisVopPruefungEinzeltransaktion.php @@ -2,13 +2,15 @@ namespace Fhp\Segment\VPP; -class ErgebnisVopPruefungEinzeltransaktion +use Fhp\Segment\BaseDeg; + +class ErgebnisVopPruefungEinzeltransaktion extends BaseDeg { public string $ibanEmpfaenger; public ?string $ibanZusatzinformationen = null; - public ?string $abweichenderEmpfängername = null; + public ?string $abweichenderEmpfaengername = null; public ?string $anderesIdentifikationmerkmal = null; diff --git a/lib/Tests/Fhp/Integration/Atruvia/SendTransferVoPTest.php b/lib/Tests/Fhp/Integration/Atruvia/SendTransferVoPTest.php index 33976f41..43972043 100644 --- a/lib/Tests/Fhp/Integration/Atruvia/SendTransferVoPTest.php +++ b/lib/Tests/Fhp/Integration/Atruvia/SendTransferVoPTest.php @@ -3,7 +3,6 @@ namespace Tests\Fhp\Integration\Atruvia; use Fhp\Action\SendSEPATransferVoP; -use Tests\Fhp\FinTsPeer; class SendTransferVoPTest extends AtruviaIntegrationTestBase { @@ -16,10 +15,15 @@ class SendTransferVoPTest extends AtruviaIntegrationTestBase ; public const SEND_TRANSFER_RESPONSE = "HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.+3905::Es wurde keine Challenge erzeugt.'HIRMS:4:2:3+3040::Es liegen weitere Informationen vor.:staticscrollref'HIRMS:5:2:5+3945::Freigabe ohne VOP-Bestätigung nicht möglich.'HIVPP:6:1:3+++@36@c0f5c2a4-ebb7-4e72-be44-c68742177a2b+++++2'"; - public const REQUEST_VOP_REPORT_REQUEST = "HKVPP:3:1+urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.002.001.10+@36@c0f5c2a4-ebb7-4e72-be44-c68742177a2b++staticscrollref'"; - public const REQUEST_VOP_REPORT_RESPONSE = "HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.'HIRMS:4:2:3+3090::Ergebnis des Namensabgleichs prüfen.'HIVPP:5:1:3+@36@5e3b5c99-df27-4d42-835b-18b35d0c66ff+++urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.002.001.10+@3600@UTF8XMLPAYLOAD++Bei mindestens einem Zahlungsempfänger stimmt der Name mit dem für diese IBAN bei der Zahlungsempfängerbank hinterlegten Namen nicht oder nur nahezu überein.
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_REQUEST = "HKVPP:3:1+urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.002.001.10+@36@c0f5c2a4-ebb7-4e72-be44-c68742177a2b++staticscrollref'"; - public const REQUEST_VOP_REPORT_RESPONSE_XML_PAYLOAD = "ATRUVIA-20251010-125258-X2025-10-10T12:52:58.283+02:00ABCDEFGHIJKM1234567890pain.001.001.09110.00RVCMRCVC Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt mit dem für diese IBANRCVC hinterlegten Namen bei der Zahlungsempfängerbank überein.RVMC Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt nahezu mit dem für diese IBANRVMC hinterlegten Namen bei der Zahlungsempfängerbank überein. Die Autorisierung der ZahlungRVMC kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen Inhaber nichtRVMC der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nicht fürRVMC die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.RVNM Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt nicht mit dem für diese IBAN hinter-RVNM legten Namen bei der Zahlungsempfängerbank überein. Bitte prüfen Sie den Empfängernamen. Die Autori-RVNM sierung der Zahlung kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen InhaberRVNM nicht der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nichtRVNM für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.RVNA Der von Ihnen eingegebene Name des Zahlungsempfängers konnte nicht mit dem für diese IBAN hinter-RVNA legten Namen bei der Zahlungsempfängerbank abgeglichen werden (z.B. technischer Fehler). Die Autori-RVNA sierung der Zahlung kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen InhaberRVNA nicht der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nichtRVNA für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.0RCVC0RVMC1RVNM0RVNA176009357610RCVC0RVMC1RVNM0RVNANOTPROVIDEDRVNMTestempfängerDE00ABCDEFGH1234567890"; + public const POLL_VOP_REPORT_MATCH_RESPONSE = "HIRMG:3:2+0010::Nachricht entgegengenommen.'HIRMS:4:2:3+0020::Auftrag ausgeführt.+0025::Keine Namensabweichung.'HIVPP:5:1:3+@36@5e3b5c99-df27-4d42-835b-18b35d0c66ff+++urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.002.001.10+@3616@UTF8XMLPAYLOAD'"; + + public const POLL_VOP_REPORT_MATCH_RESPONSE_XML_PAYLOAD = "ATRUVIA-20251013-125258-XXXXXXXXXXXXXXXX2025-10-13T11:36:04.201+02:00ABCDEFGHIJKM1234567890pain.001.001.091100.00RCVCRCVC Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt mit dem für diese IBANRCVC hinterlegten Namen bei der Zahlungsempfängerbank überein.RVMC Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt nahezu mit dem für diese IBANRVMC hinterlegten Namen bei der Zahlungsempfängerbank überein. Die Autorisierung der ZahlungRVMC kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen Inhaber nichtRVMC der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nicht fürRVMC die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.RVNM Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt nicht mit dem für diese IBAN hinter-RVNM legten Namen bei der Zahlungsempfängerbank überein. Bitte prüfen Sie den Empfängernamen. Die Autori-RVNM sierung der Zahlung kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen InhaberRVNM nicht der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nichtRVNM für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.RVNA Der von Ihnen eingegebene Name des Zahlungsempfängers konnte nicht mit dem für diese IBAN hinter-RVNA legten Namen bei der Zahlungsempfängerbank abgeglichen werden (z.B. technischer Fehler). Die Autori-RVNA sierung der Zahlung kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen InhaberRVNA nicht der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nichtRVNA für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.1RCVC0RVMC0RVNM0RVNA176034816211RCVC0RVMC0RVNM0RVNANOTPROVIDEDRCVCTestempfängerDE00ABCDEFGH1234567890"; + + public const POLL_VOP_REPORT_NO_MATCH_RESPONSE = "HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.'HIRMS:4:2:3+3090::Ergebnis des Namensabgleichs prüfen.'HIVPP:5:1:3+@36@5e3b5c99-df27-4d42-835b-18b35d0c66ff+++urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.002.001.10+@3600@UTF8XMLPAYLOAD++Bei mindestens einem Zahlungsempfänger stimmt der Name mit dem für diese IBAN bei der Zahlungsempfängerbank hinterlegten Namen nicht oder nur nahezu überein.
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 = "ATRUVIA-20251010-125258-X2025-10-10T12:52:58.283+02:00ABCDEFGHIJKM1234567890pain.001.001.09110.00RVCMRCVC Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt mit dem für diese IBANRCVC hinterlegten Namen bei der Zahlungsempfängerbank überein.RVMC Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt nahezu mit dem für diese IBANRVMC hinterlegten Namen bei der Zahlungsempfängerbank überein. Die Autorisierung der ZahlungRVMC kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen Inhaber nichtRVMC der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nicht fürRVMC die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.RVNM Der von Ihnen eingegebene Name des Zahlungsempfängers stimmt nicht mit dem für diese IBAN hinter-RVNM legten Namen bei der Zahlungsempfängerbank überein. Bitte prüfen Sie den Empfängernamen. Die Autori-RVNM sierung der Zahlung kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen InhaberRVNM nicht der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nichtRVNM für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.RVNA Der von Ihnen eingegebene Name des Zahlungsempfängers konnte nicht mit dem für diese IBAN hinter-RVNA legten Namen bei der Zahlungsempfängerbank abgeglichen werden (z.B. technischer Fehler). Die Autori-RVNA sierung der Zahlung kann dazu führen, dass das Geld auf ein Konto überwiesen wird, dessen InhaberRVNA nicht der von Ihnen angegebene Empfänger ist. In diesem Fall haften die Zahlungsdienstleister nichtRVNA für die Folgen der fehlenden Übereinstimmung, insbesondere besteht kein Anspruch auf Rückerstattung.0RCVC0RVMC1RVNM0RVNA176009357610RCVC0RVMC1RVNM0RVNANOTPROVIDEDRVNMTestempfängerDE00ABCDEFGH1234567890"; public const CONFIRM_VOP_REQUEST = "HKVPA:3:1+@36@5e3b5c99-df27-4d42-835b-18b35d0c66ff'HKCCS:4:1+DE00ABCDEFGH1234567890:ABCDEFGHIJK:1234567890::280:11223344+urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.09+@1161@" . @@ -34,7 +38,7 @@ class SendTransferVoPTest extends AtruviaIntegrationTestBase /** * @throws \Throwable */ - public function test() + protected function testVop(string $requestVopReportResponse) { $this->initDialog(); @@ -49,7 +53,7 @@ public function test() #$wait = $transferAction->hivpp->wartezeitVorNaechsterAbfrage; #sleep($wait); - $this->expectMessage(static::REQUEST_VOP_REPORT_REQUEST, str_replace('UTF8XMLPAYLOAD', self::REQUEST_VOP_REPORT_RESPONSE_XML_PAYLOAD, mb_convert_encoding(static::REQUEST_VOP_REPORT_RESPONSE, 'ISO-8859-1', 'UTF-8'))); + $this->expectMessage(static::POLL_VOP_REPORT_REQUEST, $requestVopReportResponse); $this->fints->execute($transferAction); } @@ -60,8 +64,8 @@ public function test() $this->fints->execute($transferAction); } - if ($transferAction->needsTan()) { - $tanMode = $this->fints->getSelectedTanMode(); + $tanMode = $this->fints->getSelectedTanMode(); + while ($transferAction->needsTan()) { if ($tanMode->isDecoupled()) { $this->expectMessage(static::CHECK_DECOUPLED_SUBMISSION_REQUEST, mb_convert_encoding(static::CHECK_DECOUPLED_SUBMISSION_RESPONSE, 'ISO-8859-1', 'UTF-8')); @@ -70,7 +74,20 @@ public function test() } $transferAction->ensureDone(); + } + + public function testVopNoMatch() + { + $requestVopReportResponse = str_replace('UTF8XMLPAYLOAD', self::POLL_VOP_REPORT_NO_MATCH_RESPONSE_XML_PAYLOAD, mb_convert_encoding(static::POLL_VOP_REPORT_NO_MATCH_RESPONSE, 'ISO-8859-1', 'UTF-8')); + + $this->testVop($requestVopReportResponse); + } + + public function testVopMatch() + { + $requestVopReportResponse = str_replace('UTF8XMLPAYLOAD', self::POLL_VOP_REPORT_MATCH_RESPONSE_XML_PAYLOAD, mb_convert_encoding(static::POLL_VOP_REPORT_MATCH_RESPONSE, 'ISO-8859-1', 'UTF-8')); + $this->testVop($requestVopReportResponse); } protected function getTransferAction(): SendSEPATransferVoP From ae95c3fac41244c4cef9f80820c039aad850a514 Mon Sep 17 00:00:00 2001 From: Steve Date: Thu, 8 Aug 2024 07:35:59 +0200 Subject: [PATCH 28/38] Update SendSEPATransfer.php --- lib/Fhp/Action/SendSEPATransfer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 7c35160a..2474df74 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -72,8 +72,8 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $segment = \Fhp\Segment\CSE\HKCSEv1::createEmpty(); } elseif ($numberOfTransactions > 1 && !$hasReqdExDates) { - // SEPA-Sammelüberweisungen (Segment HKCCM / Kennung HICSES) - $segmentID = 'HICSES'; + // SEPA-Sammelüberweisungen (Segment HKCCM / Kennung HICCMS) + $segmentID = 'HICCMS'; $segment = \Fhp\Segment\CCM\HKCCMv1::createEmpty(); } else { From 68313f18842d2e66b10c71a8eb8366db2cd01fdf Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 19 Aug 2024 07:47:11 +0200 Subject: [PATCH 29/38] Update composer.json --- composer.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 25d2c7fb..a975c6d4 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { - "name": "nemiah/php-fints", + "name": "seem2810/php-fints", "description": "PHP Library for the protocols fints and hbci", - "homepage": "https://github.com/nemiah/phpFinTS", - "version": "3.6.0", + "homepage": "https://github.com/seem2810/phpFinTS", + "version": "3.5.0", "license": "MIT", "autoload": { "psr-0": { @@ -17,14 +17,14 @@ "ext-mbstring": "*" }, "require-dev": { - "phpunit/phpunit": "^9.5", - "php-mock/php-mock-phpunit": "^2.6", - "friendsofphp/php-cs-fixer": "^3.0" + "phpunit/phpunit": "9.5.*", + "php-mock/php-mock-phpunit": "2.6.*", + "friendsofphp/php-cs-fixer": "3.*" }, "suggest": { "monolog/monolog": "Allow sending log messages to a variety of different handlers", "abcaeffchen/sephpa": "1.*", - "nemiah/php-sepa-xml": "dev-master" + "seem2810/php-sepa-xml": "dev-master" }, "scripts": { "test": [ From 2b09aeaef81e5dbd07587ff05e48f11b691d0a52 Mon Sep 17 00:00:00 2001 From: "steve.mueller" Date: Thu, 22 Aug 2024 08:09:28 +0200 Subject: [PATCH 30/38] fix Summenfeld --- lib/Fhp/Action/SendSEPATransfer.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 2474df74..30867e89 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -8,6 +8,7 @@ use Fhp\Protocol\Message; use Fhp\Protocol\UnexpectedResponseException; use Fhp\Protocol\UPD; +use Fhp\Segment\Common\Btg; use Fhp\Segment\Common\Kti; use Fhp\Segment\HIRMS\Rueckmeldungscode; use Fhp\Segment\SPA\HISPAS; @@ -51,7 +52,9 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $xmlAsObject = simplexml_load_string($this->painMessage, "SimpleXMLElement", LIBXML_NOCDATA); $numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs; $hasReqdExDates = false; + $CtrlSum = 0.00; foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) { + $CtrlSum += (float)$pmtInfo->CtrlSum; // Checks for both, 1999-01-01 and

1999-01-01
if (isset($pmtInfo->ReqdExctnDt) && ($pmtInfo->ReqdExctnDt->Dt ?? $pmtInfo->ReqdExctnDt) != '1999-01-01') { $hasReqdExDates = true; @@ -65,6 +68,7 @@ protected function createRequest(BPD $bpd, ?UPD $upd) // Terminierte SEPA-Sammelüberweisung (Segment HKCME / Kennung HICMES) $segmentID = 'HICMES'; $segment = \Fhp\Segment\CME\HKCMEv1::createEmpty(); + $segment->summenfeld = Btg::create($CtrlSum); } elseif ($numberOfTransactions == 1 && $hasReqdExDates) { // Terminierte SEPA-Überweisung (Segment HKCSE / Kennung HICSES) @@ -75,6 +79,7 @@ protected function createRequest(BPD $bpd, ?UPD $upd) // SEPA-Sammelüberweisungen (Segment HKCCM / Kennung HICCMS) $segmentID = 'HICCMS'; $segment = \Fhp\Segment\CCM\HKCCMv1::createEmpty(); + $segment->summenfeld = Btg::create($CtrlSum); } else { //SEPA Einzelüberweisung (Segment HKCCS / Kennung HICCSS). From 6caa0c1019aa5ddfedeeeb73252af73b1174e870 Mon Sep 17 00:00:00 2001 From: "steve.mueller" Date: Mon, 26 Aug 2024 09:36:03 +0200 Subject: [PATCH 31/38] fix Summenfeld --- lib/Fhp/Action/SendSEPATransfer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 30867e89..893d1091 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -51,8 +51,8 @@ protected function createRequest(BPD $bpd, ?UPD $upd) //ANALYSE XML FOR RECEIPTS AND PAYMENT DATE $xmlAsObject = simplexml_load_string($this->painMessage, "SimpleXMLElement", LIBXML_NOCDATA); $numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs; + $CtrlSum = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->CtrlSum; $hasReqdExDates = false; - $CtrlSum = 0.00; foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) { $CtrlSum += (float)$pmtInfo->CtrlSum; // Checks for both, 1999-01-01 and
1999-01-01
From 0e332d728bcfe2c85fcad0386e9a0bbb85d85252 Mon Sep 17 00:00:00 2001 From: "steve.mueller" Date: Wed, 4 Sep 2024 12:47:35 +0200 Subject: [PATCH 32/38] - add round for control sum --- lib/Fhp/Action/SendSEPATransfer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 893d1091..4a9dbdbc 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -51,7 +51,7 @@ protected function createRequest(BPD $bpd, ?UPD $upd) //ANALYSE XML FOR RECEIPTS AND PAYMENT DATE $xmlAsObject = simplexml_load_string($this->painMessage, "SimpleXMLElement", LIBXML_NOCDATA); $numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs; - $CtrlSum = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->CtrlSum; + $CtrlSum = round($xmlAsObject->CstmrCdtTrfInitn->GrpHdr->CtrlSum, 2); $hasReqdExDates = false; foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) { $CtrlSum += (float)$pmtInfo->CtrlSum; From 7d3269d2ddda192e86190faac77ff1863ef1f78b Mon Sep 17 00:00:00 2001 From: "steve.mueller" Date: Fri, 6 Sep 2024 12:48:52 +0200 Subject: [PATCH 33/38] - add round for control sum --- lib/Fhp/Action/SendSEPATransfer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 4a9dbdbc..26b0ed2b 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -51,7 +51,7 @@ protected function createRequest(BPD $bpd, ?UPD $upd) //ANALYSE XML FOR RECEIPTS AND PAYMENT DATE $xmlAsObject = simplexml_load_string($this->painMessage, "SimpleXMLElement", LIBXML_NOCDATA); $numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs; - $CtrlSum = round($xmlAsObject->CstmrCdtTrfInitn->GrpHdr->CtrlSum, 2); + $CtrlSum = round((float)$xmlAsObject->CstmrCdtTrfInitn->GrpHdr->CtrlSum, 2); $hasReqdExDates = false; foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) { $CtrlSum += (float)$pmtInfo->CtrlSum; From a3d100db33b9bc572f68e4137cf36875b8b63473 Mon Sep 17 00:00:00 2001 From: "steve.mueller" Date: Mon, 16 Sep 2024 12:42:34 +0200 Subject: [PATCH 34/38] - add batchbooking for segmnet --- lib/Fhp/Action/SendSEPATransfer.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index 26b0ed2b..b46aceac 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -53,6 +53,7 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs; $CtrlSum = round((float)$xmlAsObject->CstmrCdtTrfInitn->GrpHdr->CtrlSum, 2); $hasReqdExDates = false; + $batchBooking = false; foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) { $CtrlSum += (float)$pmtInfo->CtrlSum; // Checks for both, 1999-01-01 and
1999-01-01
@@ -60,6 +61,7 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $hasReqdExDates = true; break; } + if (isset($pmtInfo->BtchBookg)) $batchBooking = (string)$pmtInfo->BtchBookg == 'true'; } //NOW READ OUT, WICH SEGMENT SHOULD BE USED: @@ -69,6 +71,7 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $segmentID = 'HICMES'; $segment = \Fhp\Segment\CME\HKCMEv1::createEmpty(); $segment->summenfeld = Btg::create($CtrlSum); + $segment->einzelbuchungGewuenscht = $batchBooking; } elseif ($numberOfTransactions == 1 && $hasReqdExDates) { // Terminierte SEPA-Überweisung (Segment HKCSE / Kennung HICSES) @@ -80,6 +83,7 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $segmentID = 'HICCMS'; $segment = \Fhp\Segment\CCM\HKCCMv1::createEmpty(); $segment->summenfeld = Btg::create($CtrlSum); + $segment->einzelbuchungGewuenscht = $batchBooking; } else { //SEPA Einzelüberweisung (Segment HKCCS / Kennung HICCSS). @@ -112,6 +116,7 @@ protected function createRequest(BPD $bpd, ?UPD $upd) $segment->kontoverbindungInternational = Kti::fromAccount($this->account); $segment->sepaDescriptor = $this->xmlSchema; $segment->sepaPainMessage = new Bin($this->painMessage); + return $segment; } From 17479206eb166a80cd28e031621aa69d4ba55695 Mon Sep 17 00:00:00 2001 From: "steve.mueller" Date: Mon, 16 Sep 2024 15:24:12 +0200 Subject: [PATCH 35/38] fix --- lib/Fhp/FinTs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Fhp/FinTs.php b/lib/Fhp/FinTs.php index defaf6d7..30710e33 100644 --- a/lib/Fhp/FinTs.php +++ b/lib/Fhp/FinTs.php @@ -310,7 +310,7 @@ public function execute(BaseAction $action) throw new \RuntimeException('Need to login (DialogInitialization) before executing other actions'); } - $requestSegments = $action->getNextRequest($this->bpd, $this->upd); + $requestSegments = $action->getNextRequest($this->getBpd(), $this->upd); if (count($requestSegments) === 0) { return; // No request needed. From 118794946c538710fd7aa2e8325971787472eb25 Mon Sep 17 00:00:00 2001 From: "steve.mueller" Date: Wed, 4 Dec 2024 15:10:54 +0100 Subject: [PATCH 36/38] - fix send sepa Transfer return segments --- lib/Fhp/Action/SendSEPATransfer.php | 10 ++++++++++ lib/Fhp/Segment/CCM/HICCMSv1.php | 4 ++-- lib/Fhp/Segment/CCS/HICCSSv1.php | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/Fhp/Action/SendSEPATransfer.php b/lib/Fhp/Action/SendSEPATransfer.php index b46aceac..72fbda51 100644 --- a/lib/Fhp/Action/SendSEPATransfer.php +++ b/lib/Fhp/Action/SendSEPATransfer.php @@ -64,6 +64,16 @@ protected function createRequest(BPD $bpd, ?UPD $upd) if (isset($pmtInfo->BtchBookg)) $batchBooking = (string)$pmtInfo->BtchBookg == 'true'; } + //CHECK IF $hasReqdExDates and set other to tomorrow + if ($hasReqdExDates) { + foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) { + if (isset($pmtInfo->ReqdExctnDt) && $pmtInfo->ReqdExctnDt == '1999-01-01') { + throw new UnsupportedException('Terminierte SEPA-Sammelüberweisung (Segment HKCME / Kennung HICMES) requires all entries to be in future'); + } + } + } + + //NOW READ OUT, WICH SEGMENT SHOULD BE USED: if ($numberOfTransactions > 1 && $hasReqdExDates) { diff --git a/lib/Fhp/Segment/CCM/HICCMSv1.php b/lib/Fhp/Segment/CCM/HICCMSv1.php index f3d23be9..82a1d20c 100644 --- a/lib/Fhp/Segment/CCM/HICCMSv1.php +++ b/lib/Fhp/Segment/CCM/HICCMSv1.php @@ -2,7 +2,7 @@ namespace Fhp\Segment\CCM; -use Fhp\Segment\BaseGeschaeftsvorfallparameter; +use Fhp\Segment\BaseSegment; /** * Segment: SEPA Einzelüberweisung Parameter (Version 1) @@ -10,7 +10,7 @@ * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2015-08-07_final_version.pdf * Section: C.10.2.1 c) */ -class HICCMSv1 extends BaseGeschaeftsvorfallparameter +class HICCMSv1 extends BaseSegment { public ParameterSEPASammelueberweisungV1 $parameter; diff --git a/lib/Fhp/Segment/CCS/HICCSSv1.php b/lib/Fhp/Segment/CCS/HICCSSv1.php index b7b1fc1f..b84e155d 100644 --- a/lib/Fhp/Segment/CCS/HICCSSv1.php +++ b/lib/Fhp/Segment/CCS/HICCSSv1.php @@ -2,7 +2,7 @@ namespace Fhp\Segment\CCS; -use Fhp\Segment\BaseGeschaeftsvorfallparameter; +use Fhp\Segment\BaseSegment; /** * Segment: SEPA Einzelüberweisung Parameter (Version 1) @@ -10,7 +10,7 @@ * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2015-08-07_final_version.pdf * Section: C.10.2.1 c) */ -class HICCSSv1 extends BaseGeschaeftsvorfallparameter +class HICCSSv1 extends BaseSegment { // No parameters. } From cff1e16eebb43b7419901d304049b7c65058410c Mon Sep 17 00:00:00 2001 From: "steve.mueller" Date: Wed, 4 Dec 2024 15:15:45 +0100 Subject: [PATCH 37/38] - fix send sepa Transfer return segments --- lib/Fhp/Segment/CCM/HICCMSv1.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Fhp/Segment/CCM/HICCMSv1.php b/lib/Fhp/Segment/CCM/HICCMSv1.php index 82a1d20c..f3d23be9 100644 --- a/lib/Fhp/Segment/CCM/HICCMSv1.php +++ b/lib/Fhp/Segment/CCM/HICCMSv1.php @@ -2,7 +2,7 @@ namespace Fhp\Segment\CCM; -use Fhp\Segment\BaseSegment; +use Fhp\Segment\BaseGeschaeftsvorfallparameter; /** * Segment: SEPA Einzelüberweisung Parameter (Version 1) @@ -10,7 +10,7 @@ * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2015-08-07_final_version.pdf * Section: C.10.2.1 c) */ -class HICCMSv1 extends BaseSegment +class HICCMSv1 extends BaseGeschaeftsvorfallparameter { public ParameterSEPASammelueberweisungV1 $parameter; From 870e53a470c2bcb0b37d4e1f85dd84285f4bd667 Mon Sep 17 00:00:00 2001 From: "steve.mueller" Date: Wed, 4 Dec 2024 15:20:49 +0100 Subject: [PATCH 38/38] - fix send sepa Transfer return segments --- lib/Fhp/Segment/CCS/HICCSSv1.php | 4 ++-- lib/Fhp/Segment/CME/HICMEv1.php | 2 +- lib/Fhp/Segment/CSE/HICSEv1.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Fhp/Segment/CCS/HICCSSv1.php b/lib/Fhp/Segment/CCS/HICCSSv1.php index b84e155d..b7b1fc1f 100644 --- a/lib/Fhp/Segment/CCS/HICCSSv1.php +++ b/lib/Fhp/Segment/CCS/HICCSSv1.php @@ -2,7 +2,7 @@ namespace Fhp\Segment\CCS; -use Fhp\Segment\BaseSegment; +use Fhp\Segment\BaseGeschaeftsvorfallparameter; /** * Segment: SEPA Einzelüberweisung Parameter (Version 1) @@ -10,7 +10,7 @@ * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2015-08-07_final_version.pdf * Section: C.10.2.1 c) */ -class HICCSSv1 extends BaseSegment +class HICCSSv1 extends BaseGeschaeftsvorfallparameter { // No parameters. } diff --git a/lib/Fhp/Segment/CME/HICMEv1.php b/lib/Fhp/Segment/CME/HICMEv1.php index 4fb50613..b7c6a3b9 100644 --- a/lib/Fhp/Segment/CME/HICMEv1.php +++ b/lib/Fhp/Segment/CME/HICMEv1.php @@ -5,7 +5,7 @@ use Fhp\Segment\BaseSegment; /** - * Segment: SEPA Einzelüberweisung Parameter (Version 1) + * Segment: Einreichung terminierter SEPA-Sammelüberweisung bestätigen (Version 1) * * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2015-08-07_final_version.pdf * Section: C.10.2.1 c) diff --git a/lib/Fhp/Segment/CSE/HICSEv1.php b/lib/Fhp/Segment/CSE/HICSEv1.php index 4bedf9fe..bc749c4e 100644 --- a/lib/Fhp/Segment/CSE/HICSEv1.php +++ b/lib/Fhp/Segment/CSE/HICSEv1.php @@ -5,7 +5,7 @@ use Fhp\Segment\BaseSegment; /** - * Segment: SEPA Einzelüberweisung Parameter (Version 1) + * Segment: Einreichung terminierter SEPA-Überweisung bestätigen (Version 1) * * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2015-08-07_final_version.pdf * Section: C.10.2.1 c)