2626use Fhp \Segment \TAN \HKTAN ;
2727use Fhp \Segment \TAN \HKTANFactory ;
2828use Fhp \Segment \TAN \HKTANv6 ;
29+ use Fhp \Segment \VPP \HKVPPv1 ;
30+ use Fhp \Segment \VPP \VopHelper ;
31+ use Fhp \Segment \VPP \VopPollingToken ;
2932use Fhp \Syntax \InvalidResponseException ;
3033use Psr \Log \LoggerInterface ;
3134use Psr \Log \NullLogger ;
@@ -285,17 +288,32 @@ public function login(): DialogInitialization
285288
286289 /**
287290 * Executes an action. Be sure to {@link login()} first. See the `\Fhp\Action` package for actions that can be
288- * executed with this function. Note that, after this function returns, the action can be in two possible states:
291+ * executed with this function. Note that, after this function returns, the action can be in the following states:
289292 * 1. If {@link BaseAction::needsTan()} returns true, the action isn't completed yet because needs a TAN or other
290293 * kind of two-factor authentication (2FA). In this case, use {@link BaseAction::getTanRequest()} to get more
291294 * information about the TAN/2FA that is needed. Your application then needs to interact with the user to obtain
292295 * the TAN (which should be passed into {@link submitTan()}) or to have them complete the 2FA check (which can
293296 * be verified with {@link checkDecoupledSubmission()}). Both of those functions require passing the same
294297 * {@link BaseAction} argument as an argument, and once they succeed, the action will be in the same completed
295298 * state as if it had been completed right away.
296- * 2. If {@link BaseAction::needsTan()} returns false, the action was completed right away. Use the respective
297- * getters on the action instance to retrieve the result. In case the action fails, the corresponding exception
298- * will be thrown from this function.
299+ * 2. If {@link BaseAction::needsPollingWait()} returns true, the action isn't completed yet because the server is
300+ * still running some slow operation. Importantly, the server has not necessarily accepted the action yet, so it
301+ * is absolutely required that the client keeps polling if they don't want the action to be abandoned.
302+ * In this case, use {@link BaseAction::getPollingToken()} to get more information on how frequently to poll, and
303+ * do the polling through {@link pollAction()}.
304+ * 3. If {@link BaseAction::needsVopConfirmation()} returns true, the action isn't completed yet because the payee
305+ * information couldn't be matched automatically, so an explicit confirmation from the user is required. In this
306+ * case, use TODO.
307+ * 4. If none of the above return true, the action was completed right away.
308+ * Use the respective getters on the action instance to retrieve the result. In case the action fails, the
309+ * corresponding exception will be thrown from this function.
310+ *
311+ * Note that all conditions above that leave the action in an incomplete state require some action from the client
312+ * application. These actions then change the state of the action again, but they don't necessarily complete it.
313+ * In practice, the typical sequence is: Maybe polling, maybe VoP confirmation, maybe TAN, done. That said, you
314+ * should ideally implement your application to deal with any sequence of states. Just execute the action, check
315+ * what's state it's in, resolve that state as appropriate, and then check again (using the same code as before). Do
316+ * this repeatedly until none of the special conditions above happen anymore, at which point the action is done.
299317 *
300318 * @param BaseAction $action The action to be executed. Its {@link BaseAction::isDone()} status will be updated when
301319 * this function returns successfully.
@@ -324,7 +342,13 @@ public function execute(BaseAction $action): void
324342 $ this ->requireTanMode (), $ this ->selectedTanMedium , $ needTanForSegment ));
325343 }
326344 }
327- $ request = $ this ->buildMessage ($ message , $ this ->getSelectedTanMode ());
345+
346+ // Add HKVPP for VoP verification if necessary.
347+ $ hkvpp = null ;
348+ if ($ this ->bpd ?->vopRequiredForRequest($ requestSegments ) !== null ) {
349+ $ hkvpp = VopHelper::createHKVPPForInitialRequest ($ this ->bpd );
350+ $ message ->add ($ hkvpp );
351+ }
328352
329353 // Construct the request and tell the action about the segment numbers that were assigned.
330354 $ request = $ this ->buildMessage ($ message , $ this ->getSelectedTanMode ()); // This fills in the segment numbers.
@@ -372,7 +396,15 @@ private function processServerResponse(BaseAction $action, Message $response, ?H
372396 return ;
373397 }
374398
375- // If no TAN is needed, process the response normally, and maybe keep going for more pages.
399+ // Detect if the bank needs us to do something for Verification of Payee.
400+ if ($ hkvpp != null ) {
401+ if ($ pollingToken = VopHelper::checkPollingRequired ($ response , $ hkvpp ->getSegmentNumber ())) {
402+ $ action ->setPollingToken ($ pollingToken );
403+ return ;
404+ }
405+ }
406+
407+ // If no TAN or VOP is needed, process the response normally, and maybe keep going for more pages.
376408 $ this ->processActionResponse ($ action , $ response ->filterByReferenceSegments ($ action ->getRequestSegmentNumbers ()));
377409 if ($ action instanceof PaginateableAction && $ action ->hasMorePages ()) {
378410 $ this ->execute ($ action );
@@ -384,9 +416,9 @@ private function processServerResponse(BaseAction $action, Message $response, ?H
384416 * `false`, this function sends the given $tan to the server to complete the action. By using {@link persist()},
385417 * this can be done asynchronously, i.e., not in the same PHP process as the original {@link execute()} call.
386418 *
387- * After this function returns, the `$action` is completed. That is, its result is available through its getters
388- * just as if it had been completed by the original call to {@link execute()} right away. In case the action fails,
389- * the corresponding exception will be thrown from this function.
419+ * After this function returns, the `$action` is in any of the same states as after {@link execute()}, see there.
420+ * In practice, the action is fully completed after completing the decoupled submission.
421+ * In case the action fails, the corresponding exception will be thrown from this function.
390422 *
391423 * @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Security_Sicherheitsverfahren_PINTAN_2020-07-10_final_version.pdf
392424 * Section B.4.2.1.1
@@ -452,7 +484,9 @@ public function submitTan(BaseAction $action, string $tan): void
452484 * For an action where {@link BaseAction::needsTan()} returns `true` and {@link TanMode::isDecoupled()} returns
453485 * `true`, this function checks with the server whether the second factor authentication has been completed yet on
454486 * the secondary device of the user.
455- * - If so, this completes the given action and returns `true`.
487+ * - If so, this function returns `true` and the `$action` is then in any of the same states as after
488+ * {@link execute()} (except {@link BaseAction::needsTan()} won't happen again). See there for documentation.
489+ * In practice, the action is fully completed after completing the decoupled submission.
456490 * - In case the action fails, the corresponding exception will be thrown from this function.
457491 * - If the authentication has not been completed yet, this returns `false` and the action remains in its
458492 * previous, uncompleted state.
@@ -468,9 +502,10 @@ public function submitTan(BaseAction $action, string $tan): void
468502 * Section B.4.2.2
469503 *
470504 * @param BaseAction $action The action to be completed.
471- * @return bool True if the decoupled authentication is done and the $action was completed. If false, the
472- * {@link TanRequest} inside the action has been updated, which *may* provide new/more instructions to the user,
473- * though probably it rarely does in practice.
505+ * @return bool True if the decoupled authentication is done and the $action was completed or entered one of the
506+ * other states documented on {@link execute()}.
507+ * If false, the {@link TanRequest} inside the action has been updated, which *may* provide new/more
508+ * instructions to the user, though probably it rarely does in practice.
474509 * @throws CurlException When the connection fails in a layer below the FinTS protocol.
475510 * @throws UnexpectedResponseException When the server responds with a valid but unexpected message.
476511 * @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things
@@ -549,6 +584,52 @@ public function checkDecoupledSubmission(BaseAction $action): bool
549584 return true ;
550585 }
551586
587+ /**
588+ * For an action where {@link BaseAction::needsPollingWait()} returns `true`, this function polls the server.
589+ * By using {@link persist()}, this can be done asynchronously, i.e., not in the same PHP process as the original
590+ * {@link execute()} call or the previous {@link pollAction()} call.
591+ *
592+ * After this function returns, the `$action` is in any of the same states as after {@link execute()}, see there. In
593+ * particular, it's possible that the long-running operation on the server has not completed yet and thus
594+ * {@link BaseAction::needsPollingWait()} still returns `true`. In practice, actions often require VoP confirmation
595+ * or a TAN after the polling is over, though they can also complete right away.
596+ * In case the action fails, the corresponding exception will be thrown from this function.
597+ *
598+ * @param BaseAction $action The action to be completed.
599+ * @throws CurlException When the connection fails in a layer below the FinTS protocol.
600+ * @throws UnexpectedResponseException When the server responds with a valid but unexpected message.
601+ * @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things
602+ * that can go wrong with the action itself, like wrong credentials, invalid IBANs, locked accounts, etc.
603+ * @link FinTS_3.0_Messages_Geschaeftsvorfaelle_VOP_1.01_2025_06_27_FV.pdf
604+ * Section C.10.7.1.1 a)
605+ *
606+ */
607+ public function pollAction (BaseAction $ action ): void
608+ {
609+ $ pollingToken = $ action ->getPollingToken ();
610+ if ($ pollingToken === null ) {
611+ throw new \InvalidArgumentException ('This action is not awaiting polling for a long-running operation ' );
612+ } elseif ($ pollingToken instanceof VopPollingToken) {
613+ // Only send a new HKVPP.
614+ $ hkvpp = VopHelper::createHKVPPForPollingRequest ($ this ->bpd , $ pollingToken );
615+ $ message = MessageBuilder::create ()->add ($ hkvpp );
616+
617+ // Add HKTAN for authentication if necessary.
618+ if (!($ this ->getSelectedTanMode () instanceof NoPsd2TanMode)) {
619+ if (($ needTanForSegment = $ action ->getNeedTanForSegment ()) !== null ) {
620+ $ message ->add (HKTANFactory::createProzessvariante2Step1 (
621+ $ this ->requireTanMode (), $ this ->selectedTanMedium , $ needTanForSegment ));
622+ }
623+ }
624+
625+ // Execute the request and process the response.
626+ $ response = $ this ->sendMessage ($ this ->buildMessage ($ message , $ this ->getSelectedTanMode ()));
627+ $ this ->processServerResponse ($ action , $ response , $ hkvpp );
628+ } else {
629+ throw new \InvalidArgumentException ('Unexpected polling token type: ' . gettype ($ pollingToken ));
630+ }
631+ }
632+
552633 /**
553634 * Closes the session/dialog/connection, if open. This is equivalent to logging out. You should call this function
554635 * when you're done with all the actions, but NOT when you're persisting the instance to fulfill the TAN request of
0 commit comments