From 8a33729b2047abc7edb5a6b9d933f07d3d25f976 Mon Sep 17 00:00:00 2001
From: ekes <ekes@iskra.net>
Date: Mon, 9 Jun 2025 18:20:54 +0200
Subject: [PATCH 1/2] Refactor to display different return page on: success,
 failure and error.

---
 .../govuk_pay_webform.module                  |  29 +-
 .../govuk_pay_webform.routing.yml             |   2 +-
 .../Controller/GovPayWebformController.php    | 414 ++++++++++++------
 .../GovPayWebformControllerException.php      |  10 +
 .../src/GovUkPayWebformService.php            |  13 +-
 .../Plugin/WebformHandler/GovPayHandler.php   |  10 +-
 ...webform--govuk-confirmation-page.html.twig |  36 --
 ...-pay-webform--payment-error-page.html.twig |   9 +
 ...ay-webform--payment-failure-page.html.twig |  13 +
 ...ay-webform--payment-success-page.html.twig |  16 +
 10 files changed, 362 insertions(+), 190 deletions(-)
 create mode 100644 modules/govuk_pay_webform/src/Controller/GovPayWebformControllerException.php
 delete mode 100644 modules/govuk_pay_webform/templates/govuk-pay-webform--govuk-confirmation-page.html.twig
 create mode 100644 modules/govuk_pay_webform/templates/govuk-pay-webform--payment-error-page.html.twig
 create mode 100644 modules/govuk_pay_webform/templates/govuk-pay-webform--payment-failure-page.html.twig
 create mode 100644 modules/govuk_pay_webform/templates/govuk-pay-webform--payment-success-page.html.twig

diff --git a/modules/govuk_pay_webform/govuk_pay_webform.module b/modules/govuk_pay_webform/govuk_pay_webform.module
index e15bcc8..31f271b 100644
--- a/modules/govuk_pay_webform/govuk_pay_webform.module
+++ b/modules/govuk_pay_webform/govuk_pay_webform.module
@@ -16,15 +16,34 @@ require_once __DIR__ . '/includes/views.inc';
  */
 function govuk_pay_webform_theme($existing, $type, $theme, $path) {
   return [
-    'govuk_pay_webform__govuk_confirmation_page' => [
+    'govuk_pay_webform__payment_success_page' => [
       'variables' => [
         'payment_id' => NULL,
-        'payment_amount' => NULL,
-        'payment_status' => NULL,
-        'payment_message' => NULL,
+        'amount' => NULL,
+        'status' => NULL,
+        'message' => NULL,
         'payment_for' => NULL,
         'payment_reference' => NULL,
-        'confirmation_message' => NULL,
+        'message' => NULL,
+      ],
+    ],
+    // @todo check how many of these are filled in.
+    // And how many are useful. Status and Payment ID
+    // should be completed.
+    'govuk_pay_webform__payment_failure_page' => [
+      'variables' => [
+        'payment_id' => NULL,
+        'amount' => NULL,
+        'status' => NULL,
+        'message' => NULL,
+        'payment_for' => NULL,
+        'payment_reference' => NULL,
+        'message' => NULL,
+      ],
+    ],
+    'govuk_pay_webform__payment_error_page' => [
+      'variables' => [
+        'error' => NULL,
       ],
     ],
     'govuk_payment_view__webform' => [
diff --git a/modules/govuk_pay_webform/govuk_pay_webform.routing.yml b/modules/govuk_pay_webform/govuk_pay_webform.routing.yml
index 0de4e35..24afdb4 100644
--- a/modules/govuk_pay_webform/govuk_pay_webform.routing.yml
+++ b/modules/govuk_pay_webform/govuk_pay_webform.routing.yml
@@ -2,6 +2,6 @@ govuk_pay_webform.confirmation_page:
   path: 'gov-pay-confirmation'
   defaults:
     _controller: '\Drupal\govuk_pay_webform\Controller\GovPayWebformController::confirmationPage'
-    _title: 'GOV.UK Pay Confirmation'
+    _title_callback: '\Drupal\govuk_pay_webform\Controller\GovPayWebformController::confirmationPageTitle'
   requirements:
     _govuk_pay_webform_access_check: 'TRUE'
diff --git a/modules/govuk_pay_webform/src/Controller/GovPayWebformController.php b/modules/govuk_pay_webform/src/Controller/GovPayWebformController.php
index e528b9a..b6527c7 100644
--- a/modules/govuk_pay_webform/src/Controller/GovPayWebformController.php
+++ b/modules/govuk_pay_webform/src/Controller/GovPayWebformController.php
@@ -2,18 +2,23 @@
 
 namespace Drupal\govuk_pay_webform\Controller;
 
-use Symfony\Component\HttpFoundation\RequestStack;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Psr\Log\LoggerInterface;
-use Drupal\webform\Entity\Webform;
-use Drupal\govuk_pay_webform\GovUkPayWebformService;
-use Drupal\govuk_pay\PaymentEventService;
-use Drupal\Core\Utility\Token;
-use Drupal\Core\Messenger\MessengerInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Controller\ControllerBase;
 use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Component\Render\MarkupInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Utility\Error;
+use Drupal\Core\Utility\Token;
+use Drupal\govuk_pay\Entity\GovUkPayment;
+use Drupal\govuk_pay\PaymentEventService;
+use Drupal\govuk_pay_webform\GovUkPayWebformService;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\WebformInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Page controller for displaying GOV.UK Pay content on behalf of Webform.
@@ -140,165 +145,285 @@ class GovPayWebformController extends ControllerBase {
     );
   }
 
+  /**
+   * Creates the Confirmation page title.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+   */
+  public function confirmationPageTitle(): TranslatableMarkup {
+    $session_payment_data = $this->getPaymentSession();
+    if (isset($session_payment_data['webform_id'])) {
+      $webform = Webform::load($session_payment_data['webform_id']);
+      return new TranslatableMarkup('@title', ['@title' => $webform->label()]);
+    }
+
+    return new TranslatableMarkup('Your payment');
+  }
+
   /**
    * Builds the Confirmation page.
    *
    * @return array
    *   Render array.
    */
-  public function confirmationPage() {
-    // Default render array with empty values.
-    $data = [
-      '#theme' => 'govuk_pay_webform__govuk_confirmation_page',
-      '#payment_id' => NULL,
-      '#payment_amount' => NULL,
-      '#payment_status' => NULL,
-      '#payment_message' => NULL,
-      '#payment_for' => NULL,
-      '#payment_reference' => NULL,
-      '#confirmation_message' => NULL,
-    ];
+  public function confirmationPage(): array {
+    try {
+      $session_payment_data = $this->getPaymentSession();
+      $payment_details = $this->getPaymentDetails(
+        $session_payment_data['uuid'],
+        $session_payment_data['webform_id'],
+        $session_payment_data['submission_id']
+      );
+      $this->updatePaymentEntity($session_payment_data['uuid'], $payment_details);
+    }
+    catch (GovPayWebformControllerException $e) {
+      if (isset($session_payment_data['webform_id'])) {
+      }
+      return [
+        '#theme' => 'govuk_pay_webform__payment_error_page',
+        '#error' => $e->getMessage(),
+        '#cache' => ['max-age' => 0],
+      ];
+    }
 
-    // Get payment details from the session.
-    $payment_data = $this->paymentService->getPaymentData();
+    $page = [];
+    $page['#payment_id'] = $payment_details['payment_id'];
+    $page['#status'] = $payment_details['status'];
+    $page['#amount'] = $payment_details['amount'];
+    $page['#payment_for'] = $payment_details['payment_for'];
+    $page['#payment_reference'] = $payment_details['payment_reference'];
+    if (in_array($payment_details['status'], ['failed', 'cancelled', 'error'])) {
+      $page['#theme'] = 'govuk_pay_webform__payment_failure_page';
+      $page['#message'] = $this->getFailureMessage(
+        $session_payment_data['webform_id'],
+        $session_payment_data['submission_id'],
+        $payment_details,
+      );
+    }
+    else {
+      $page['#theme'] = 'govuk_pay_webform__payment_success_page';
+      $page['#message'] = $this->getSuccessMessage(
+        $session_payment_data['webform_id'],
+        $session_payment_data['submission_id'],
+        $payment_details,
+      );
+    }
+    $page['#cache']['contexts'][] = 'session';
+    $page['#cache']['max-age'] = 0;
 
-    if (empty($payment_data)) {
-      $this->messenger->addError($this->t('Payment information not found. The payment session may have expired.'));
-      return $data;
+    return $page;
+  }
+
+  /**
+   * Retrieve the payment data from the session.
+   *
+   * @return array
+   *   Session payment data.
+   *
+   * @throws \Drupal\govuk_pay_webform\Controller\GovPayWebformControllerException
+   */
+  protected function getPaymentSession(): array {
+    // Get payment details from the session.
+    $session_payment_data = $this->paymentService->getPaymentData();
+    if (is_null($session_payment_data)) {
+      throw new GovPayWebformControllerException($this->t('Payment information not found. The payment session may have expired.'));
+    }
+    if (!isset($session_payment_data['uuid'],
+      $session_payment_data['webform_id'],
+      $session_payment_data['submission_id'])
+    ) {
+      throw new GovPayWebformControllerException($this->t('Invalid payment data. Please try again or contact support.'));
     }
 
+    return $session_payment_data;
+  }
+
+  /**
+   * Retreive latest payment details using service.
+   *
+   * @param string $uuid
+   *   Payment UUID.
+   * @param string $webform_id
+   *   The webform starting the payment.
+   * @param int $submission_id
+   *   The webform submission ID.
+   *
+   * @return array
+   *   Payment details array.
+   *
+   * @throws \Drupal\govuk_pay_webform\Controller\GovPayWebformControllerException
+   */
+  protected function getPaymentDetails(string $uuid, string $webform_id, $submission_id): array {
     try {
-      if (!isset($payment_data['uuid']) || !isset($payment_data['webform_id']) || !isset($payment_data['submission_id'])) {
-        $this->messenger->addError($this->t('Invalid payment data. Please try again or contact support.'));
-        return $data;
-      }
+      $payment_details = $this->paymentService->getPaymentDetails($uuid, $webform_id, $submission_id);
+    }
+    catch (\Exception $e) {
+      Error::logException($this->logger, $e);
+      throw new GovPayWebformControllerException(
+        message: 'An error occurred while processing your payment information. Please contact support.',
+        previous: $e,
+      );
+    }
+    if (is_null($payment_details['payment_id']) || is_null($payment_details['status'])) {
+      // @todo in what cases is this?
+      $this->logger->error('Error processing payment missing webform: @webform_id', ['@webform_id' => $webform_id]);
+      throw new GovPayWebformControllerException($this->t('An error occurred while processing your payment information. Please contact support.'));
+    }
 
-      $uuid = $payment_data['uuid'];
-      $webform_id = $payment_data['webform_id'];
-      $submission_id = $payment_data['submission_id'];
+    return $payment_details;
+  }
 
-      // Get payment details from the session.
-      $payment_data = $this->paymentService->getPaymentData();
+  /**
+   * Update the Gov UK Paymente Entity.
+   *
+   * @param string $uuid
+   *   Payment UUID.
+   * @param array $payment_details
+   *   Retrieved payment details.
+   */
+  protected function updatePaymentEntity($uuid, $payment_details): void {
+     // Load the payment entity by UUID.
+    $payment_storage = $this->entityTypeManager->getStorage('govukpayment');
+    $payment_entities = $payment_storage->loadByProperties(['uuid' => $uuid]);
+    if (empty($payment_entities)) {
+      // @todo again what case is this?
+      // This isn't a payment, but recording error.
+      $this->messenger->addError($this->t('An error occurred while processing your payment information. Please contact support.'));
+      $this->logger->error('Error processing payment missing payment record: @uuid', ['@uuid' => $uuid]);
+      return;
+    }
 
-      if (empty($payment_data)) {
-        $this->messenger->addError($this->t('Payment information not found. The payment session may have expired.'));
-        return $data;
+    $payment_entity = reset($payment_entities);
+    assert($payment_entity instanceof GovUkPayment);
+    // Only update if the status has changed.
+    if ($payment_entity->get('status')->value !== $payment_details['status']) {
+      $payment_entity->setNewRevision(TRUE);
+      $payment_entity->setRevisionCreationTime(time());
+      $payment_entity->setRevisionLogMessage('Payment status updated from ' . $payment_entity->get('status')->value . ' to ' . $payment_details['status']);
+      // Set the revision owner to the current user if available.
+      $current_user = $this->currentUser();
+      if ($current_user) {
+        $payment_entity->setRevisionUserId($current_user->id());
+      }
+      $payment_entity->set('status', $payment_details['status']);
+      $payment_entity->save();
+
+      // Log the update if verbose logging is enabled.
+      if ($this->verboseLogging) {
+        $this->logger->info('Payment status updated for UUID: @uuid from @old_status to @new_status', [
+          '@uuid' => $uuid,
+          '@old_status' => $payment_entity->get('status')->value,
+          '@new_status' => $payment_details['status'],
+        ]);
       }
 
-      // Get the confirmation message from the GovPayHandler configuration.
-      $webform = Webform::load($webform_id);
-      $webform_submission = NULL;
+      // Create a payment event for this status update.
+      $this->paymentEventService->recordPaymentEvent(
+        $payment_details['payment_id'],
+        $payment_details['status'],
+        'payment.status_updated',
+        'redirect',
+        [
+          'payment_id' => $payment_details['payment_id'],
+          'status' => $payment_details['status'],
+          'previous_status' => $payment_entity->get('status')->value,
+          'amount' => $this->getRawAmount($payment_details['amount']),
+          'description' => $payment_details['payment_for'],
+          'reference' => $payment_details['payment_reference'],
+        ],
+        time()
+      );
+    }
+  }
 
-      // Load the submission if available for token replacement.
-      if ($submission_id) {
-        $webform_submission = $this->entityTypeManager->getStorage('webform_submission')->load($submission_id);
-      }
+  /**
+   * Create the failure message from the Webform Plugin template.
+   *
+   * @param string $webform_id
+   *   Webform ID.
+   * @param int $submission_id
+   *   Webform submission ID.
+   * @param array $payment_details
+   *   Payment details for token replacement.
+   */
+  protected function getFailureMessage($webform_id, $submission_id, $payment_details): MarkupInterface {
+    $message = $this->getWebformMessage($webform_id, $submission_id, $payment_details, 'failure_message');
+    // Provide default confirmation message if empty.
+    if ($message === '') {
+      $message = $this->t('Sorry your payment via GOV.UK Pay was not completed.<br/>'
+        . 'Please try again, or contact support.'
+      );
+    }
 
-      if ($webform) {
-        foreach ($webform->getHandlers() as $handler) {
-          if ($handler->getPluginId() === 'govuk_pay') {
-            $configuration = $handler->getConfiguration();
-            $confirmation_message = $configuration['settings']['confirmation_message'] ? $configuration['settings']['confirmation_message'] : NULL;
-            if ($confirmation_message && $webform_submission) {
-              // Replace tokens in the confirmation message.
-              $token_data = [
-                'webform' => $webform,
-                'webform_submission' => $webform_submission,
-              ];
-              $confirmation_message = $this->token->replace($confirmation_message, $token_data);
-            }
-            if ($confirmation_message) {
-              $data['#confirmation_message'] = new FormattableMarkup($confirmation_message, []);
-            }
-            break;
-          }
-        }
+    return new FormattableMarkup($message, []);
+  }
 
-        // Provide default confirmation message if empty.
-        if (empty($data['#confirmation_message'])) {
-          $default_message = $this->t('Thank you for making a payment via GOV.UK Pay.<br/>
-          If your payment has not shown as complete for over 1 day, 
-          please contact us with your payment ID.
-        ');
-          $data['#confirmation_message'] = new FormattableMarkup($default_message, []);
-        }
+  /**
+   * Create the success message from the Webform Plugin template.
+   *
+   * @param string $webform_id
+   *   Webform ID.
+   * @param int $submission_id
+   *   Webform submission ID.
+   * @param array $payment_details
+   *   Payment details for token replacement.
+   */
+  protected function getSuccessMessage($webform_id, $submission_id, $payment_details): MarkupInterface {
+    $message = $this->getWebformMessage($webform_id, $submission_id, $payment_details, 'confirmation_message');
+    // Provide default confirmation message if empty.
+    if ($message === '') {
+      $message = $this->t('Thank you for making a payment via GOV.UK Pay.<br/>'
+        . 'If your payment has not shown as complete for over 1 day, '
+        . 'please contact us with your payment ID.'
+      );
+    }
 
-        // Fetch payment details using the service.
-        $payment_details = $this->paymentService->getPaymentDetails($uuid, $webform_id, $submission_id);
-
-        // Set payment details in the render array.
-        $data['#payment_id'] = $payment_details['payment_id'];
-        $data['#payment_amount'] = $payment_details['amount'];
-        $data['#payment_status'] = $payment_details['status'];
-        $data['#payment_message'] = $payment_details['message'];
-        $data['#payment_for'] = $payment_details['payment_for'];
-        $data['#payment_reference'] = $payment_details['payment_reference'];
-        $data['#cache']['contexts'][] = 'session';
-
-        // Update Payment entity with status from payment details.
-        if (!empty($payment_details['payment_id']) && !empty($payment_details['status'])) {
-          try {
-            // Load the payment entity by UUID.
-            $payment_storage = $this->entityTypeManager->getStorage('govukpayment');
-            $payment_entities = $payment_storage->loadByProperties(['uuid' => $uuid]);
-
-            if (!empty($payment_entities)) {
-              /** @var \Drupal\govuk_pay\Entity\GovUkPayment $payment_entity */
-              $payment_entity = reset($payment_entities);
-
-              // Only update if the status has changed.
-              if ($payment_entity->get('status')->value !== $payment_details['status']) {
-                $payment_entity->setNewRevision(TRUE);
-                $payment_entity->setRevisionCreationTime(time());
-                $payment_entity->setRevisionLogMessage('Payment status updated from ' . $payment_entity->get('status')->value . ' to ' . $payment_details['status']);
-                // Set the revision owner to the current user if available.
-                $current_user = $this->currentUser();
-                if ($current_user) {
-                  $payment_entity->setRevisionUserId($current_user->id());
-                }
-                $payment_entity->set('status', $payment_details['status']);
-                $payment_entity->save();
-
-                // Log the update if verbose logging is enabled.
-                if ($this->verboseLogging) {
-                  $this->logger->info('Payment status updated for UUID: @uuid from @old_status to @new_status', [
-                    '@uuid' => $uuid,
-                    '@old_status' => $payment_entity->get('status')->value,
-                    '@new_status' => $payment_details['status'],
-                  ]);
-                }
-
-                // Create a payment event for this status update.
-                $this->paymentEventService->recordPaymentEvent(
-                  $payment_details['payment_id'],
-                  $payment_details['status'],
-                  'payment.status_updated',
-                  'redirect',
-                  [
-                    'payment_id' => $payment_details['payment_id'],
-                    'status' => $payment_details['status'],
-                    'previous_status' => $payment_entity->get('status')->value,
-                    'amount' => $this->getRawAmount($payment_details['amount']),
-                    'description' => $payment_details['payment_for'],
-                    'reference' => $payment_details['payment_reference'],
-                  ],
-                  time()
-                );
-              }
-            }
-          }
-          catch (\Exception $e) {
-            $this->logger->error('Error updating payment entity status: @error', ['@error' => $e->getMessage()]);
+    return new FormattableMarkup($message, []);
+  }
+
+  /**
+   * Retrieve webform template and replace tokens.
+   *
+   * @param string $webform_id
+   *   Webform ID.
+   * @param int $submission_id
+   *   Webform submission ID.
+   * @param array $payment_details
+   *   Payment details for token replacement.
+   * @param string $message_id
+   *   The configuration setting that contains the message template.
+   */
+  protected function getWebformMessage($webform_id, $submission_id, $payment_details, $message_id): string {
+    $message = '';
+
+    // Load the submission if available for token replacement.
+    $webform_submission = NULL;
+    if ($submission_id) {
+      $webform_submission = $this->entityTypeManager->getStorage('webform_submission')->load($submission_id);
+    }
+
+    // Get the confirmation message from the GovPayHandler configuration.
+    $webform = Webform::load($webform_id);
+    if ($webform instanceof WebformInterface) {
+      // Load confirmation message.
+      foreach ($webform->getHandlers() as $handler) {
+        if ($handler->getPluginId() === 'govuk_pay') {
+          $configuration = $handler->getConfiguration();
+          $message_template = $configuration['settings'][$message_id] ? $configuration['settings'][$message_id] : NULL;
+          if ($message_template && $webform_submission) {
+            // Replace tokens in the confirmation message.
+            $token_data = [
+              'webform' => $webform,
+              'webform_submission' => $webform_submission,
+            ];
+            $message = $this->token->replace($message_template, $token_data);
           }
+          break;
         }
       }
     }
-    catch (\Exception $e) {
-      $this->messenger->addError($this->t('An error occurred while processing your payment information. Please contact support.'));
-      $this->logger->error('Error processing payment confirmation: @error', ['@error' => $e->getMessage()]);
-    }
 
-    return $data;
+    return $message;
   }
 
   /**
@@ -322,3 +447,4 @@ class GovPayWebformController extends ControllerBase {
   }
 
 }
+
diff --git a/modules/govuk_pay_webform/src/Controller/GovPayWebformControllerException.php b/modules/govuk_pay_webform/src/Controller/GovPayWebformControllerException.php
new file mode 100644
index 0000000..130a879
--- /dev/null
+++ b/modules/govuk_pay_webform/src/Controller/GovPayWebformControllerException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\govuk_pay_webform\Controller;
+
+/**
+ * Exception for when constructing data in the Webform Controller.
+ *
+ * @see \Drupal\govuk_pay_webform\Controller\GovPayWebformController
+ */
+class GovPayWebformControllerException extends \RuntimeException {}
diff --git a/modules/govuk_pay_webform/src/GovUkPayWebformService.php b/modules/govuk_pay_webform/src/GovUkPayWebformService.php
index a1e411c..97f04c3 100644
--- a/modules/govuk_pay_webform/src/GovUkPayWebformService.php
+++ b/modules/govuk_pay_webform/src/GovUkPayWebformService.php
@@ -556,6 +556,8 @@ class GovUkPayWebformService {
   /**
    * Get payment details for the confirmation page.
    *
+   * Uncaught exceptions passed through from ApiService::getPayment().
+   *
    * @param string $uuid
    *   UUID of the payment.
    * @param string $webform_id
@@ -567,8 +569,13 @@ class GovUkPayWebformService {
    *   Payment details array with keys for
    *   payment_id,
    *   amount, status, and message.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the payment ID is empty.
+   * @throws \RuntimeException
+   *   Thrown when the payment retrieval fails.
    */
-  public function getPaymentDetails($uuid, $webform_id, $submission_id) {
+  public function getPaymentDetails($uuid, $webform_id, $submission_id): array {
     $details = [
       'payment_id' => NULL,
       'amount' => NULL,
@@ -621,7 +628,7 @@ class GovUkPayWebformService {
    * @param array $payment_data
    *   The payment data to store.
    */
-  public function setPaymentData(array $payment_data) {
+  public function setPaymentData(array $payment_data): void {
     try {
       // Store data in the private temp store with
       // a 1-hour expiration (default).
@@ -641,7 +648,7 @@ class GovUkPayWebformService {
    * @return array|null
    *   The payment data array, or NULL if not found.
    */
-  public function getPaymentData() {
+  public function getPaymentData(): ?array {
     try {
       $data = $this->tempStore->get('payment_data');
       if (empty($data)) {
diff --git a/modules/govuk_pay_webform/src/Plugin/WebformHandler/GovPayHandler.php b/modules/govuk_pay_webform/src/Plugin/WebformHandler/GovPayHandler.php
index 6af7450..6cbdcc4 100644
--- a/modules/govuk_pay_webform/src/Plugin/WebformHandler/GovPayHandler.php
+++ b/modules/govuk_pay_webform/src/Plugin/WebformHandler/GovPayHandler.php
@@ -111,6 +111,7 @@ class GovPayHandler extends WebformHandlerBase {
       'payment_for' => '',
       'payment_reference' => '',
       'confirmation_message' => '',
+      'failure_message' => '',
       'metadata' => [],
     ] + parent::defaultConfiguration();
   }
@@ -247,10 +248,17 @@ class GovPayHandler extends WebformHandlerBase {
     $form['messages']['confirmation_message'] = [
       '#type' => 'webform_html_editor',
       '#title' => $this->t('Confirmation message'),
-      '#description' => $this->t('Additional text to display to the user once they return to the site from GOV.UK Pay.'),
+      '#description' => $this->t('Text to display to the user once they return to the site from GOV.UK Pay after a successful payment.'),
       '#default_value' => $this->configuration['confirmation_message'],
     ];
 
+    $form['messages']['failure_message'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('Failure message'),
+      '#description' => $this->t('Text to display to the user once they return to the site from GOV.UK Pay after a failed payment.'),
+      '#default_value' => $this->configuration['failure_message'],
+    ];
+
     // Add metadata field for key/value pairs.
     $form['metadata_container'] = [
       '#type' => 'details',
diff --git a/modules/govuk_pay_webform/templates/govuk-pay-webform--govuk-confirmation-page.html.twig b/modules/govuk_pay_webform/templates/govuk-pay-webform--govuk-confirmation-page.html.twig
deleted file mode 100644
index e888677..0000000
--- a/modules/govuk_pay_webform/templates/govuk-pay-webform--govuk-confirmation-page.html.twig
+++ /dev/null
@@ -1,36 +0,0 @@
-{{ attach_library('govuk_pay_webform/confirmation') }}
-<div>
-    <div id="gov_uk_header">
-        <div class="logo"></div>
-    </div>
-    <div id="default_message">
-        <p>
-          GOV.UK Payment ID: <strong>{{ payment_id }}</strong>
-        </p>
-        <p>
-          Payment Reference: <strong>{{ payment_reference }}</strong>
-        </p>
-        <p>
-          Payment For: <strong>{{ payment_for }}</strong>
-        </p>
-        <p>
-            Your payment of {{ payment_amount }} currently has
-            a status of <span class="payment-status">{{ payment_status }}</span>.
-        </p>
-        <p>
-            This status may update as your payment is processed, so please
-            reload this page if your payment is not yet complete.
-        </p>
-        <p>
-            Please note that some payment providers can take up to 1 hour
-            to process a payment, so feel free to come back to this page at a later time.
-        </p>
-        <p>
-            Additionally, the email address you provided during your payment
-            will be notified once your payment has been processed.
-        </p>
-    </div>
-    <div id="confirmation-message">
-        {{ confirmation_message }}
-    </div>
-</div>
diff --git a/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-error-page.html.twig b/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-error-page.html.twig
new file mode 100644
index 0000000..b1adb02
--- /dev/null
+++ b/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-error-page.html.twig
@@ -0,0 +1,9 @@
+{{ attach_library('govuk_pay_webform/confirmation') }}
+<div>
+    <div id="default_message">
+        <h2>Payment error</h1>
+    </div>
+    <div id="error-message">
+        <p>{{ error }}</p>
+    </div>
+</div>
diff --git a/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-failure-page.html.twig b/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-failure-page.html.twig
new file mode 100644
index 0000000..b19a7d0
--- /dev/null
+++ b/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-failure-page.html.twig
@@ -0,0 +1,13 @@
+{{ attach_library('govuk_pay_webform/confirmation') }}
+<div>
+    <div id="default_message">
+        <h2>Payment failure</h1>
+        <dl>
+            <dt>ID</dt><dd>{{ payment_id }}</dd></dt>
+            <dt>Status</dt><dd>{{ status }}</dd>
+        </dl>
+    </div>
+    <div id="confirmation-message">
+        {{ message }}
+    </div>
+</div>
diff --git a/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-success-page.html.twig b/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-success-page.html.twig
new file mode 100644
index 0000000..a6dca51
--- /dev/null
+++ b/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-success-page.html.twig
@@ -0,0 +1,16 @@
+{{ attach_library('govuk_pay_webform/confirmation') }}
+<div>
+    <div id="default_message">
+        <h2>Payment complete</h1>
+        <dl>
+            <dt>ID</dt><dd>{{ payment_id }}</dd></dt>
+            <dt>Reference</dt><dd>{{ payment_reference }}</dd>
+            <dt>Payment for</dt><dd>{{ payment_for }}</dd>
+            <dt>Amount</dt><dd>{{ amount }}</dd>
+            <dt>Current status</dt><dd>{{ status }}</dd>
+        </dl>
+    </div>
+    <div id="confirmation-message">
+        {{ message }}
+    </div>
+</div>
-- 
GitLab


From 69d101b3e26847a480efa53dc4445b87f6202a84 Mon Sep 17 00:00:00 2001
From: ekes <ekes@iskra.net>
Date: Mon, 16 Jun 2025 13:59:50 +0200
Subject: [PATCH 2/2] Enhanced failure pages.

---
 .../govuk_pay_webform.module                  |  18 +++
 .../govuk_pay_webform.routing.yml             |   6 +
 .../Controller/GovPayWebformController.php    | 103 ++++++++++++++----
 .../src/GovUkPayWebformService.php            |   1 +
 ...ay-webform--payment-failure-page.html.twig |  11 +-
 ...ay-webform--payment-success-page.html.twig |  12 +-
 6 files changed, 119 insertions(+), 32 deletions(-)

diff --git a/modules/govuk_pay_webform/govuk_pay_webform.module b/modules/govuk_pay_webform/govuk_pay_webform.module
index 31f271b..3702351 100644
--- a/modules/govuk_pay_webform/govuk_pay_webform.module
+++ b/modules/govuk_pay_webform/govuk_pay_webform.module
@@ -21,6 +21,7 @@ function govuk_pay_webform_theme($existing, $type, $theme, $path) {
         'payment_id' => NULL,
         'amount' => NULL,
         'status' => NULL,
+        'code' => NULL,
         'message' => NULL,
         'payment_for' => NULL,
         'payment_reference' => NULL,
@@ -35,10 +36,12 @@ function govuk_pay_webform_theme($existing, $type, $theme, $path) {
         'payment_id' => NULL,
         'amount' => NULL,
         'status' => NULL,
+        'code' => NULL,
         'message' => NULL,
         'payment_for' => NULL,
         'payment_reference' => NULL,
         'message' => NULL,
+        'retry_url' => NULL,
       ],
     ],
     'govuk_pay_webform__payment_error_page' => [
@@ -122,6 +125,21 @@ function govuk_pay_webform_theme_suggestions_govuk_payment_view_alter(array &$su
   $suggestions[] = 'govuk_payment_view__webform';
 }
 
+/**
+ * Implements hook_preprocess_HOOK().
+ */
+function govuk_pay_webform_preprocess_govuk_pay_webform__payment_failure_page(array &$variables) {
+  $status = $variables['status'];
+  $code = $variables['code'];
+  $variables['status_description'] = match($code) {
+    'P0010' => \t('Your payment could not be processed'),
+    'P0030', 'P0040' => \t('Your payment has been cancelled'),
+    'P0020' => \t('Your payment has expired'),
+    'P0050' => \t('There was an error processing your payment'),
+    default => \t('Your payment has not been processed'),
+  };
+}
+
 /**
  * Implements hook_preprocess_HOOK().
  */
diff --git a/modules/govuk_pay_webform/govuk_pay_webform.routing.yml b/modules/govuk_pay_webform/govuk_pay_webform.routing.yml
index 24afdb4..911217a 100644
--- a/modules/govuk_pay_webform/govuk_pay_webform.routing.yml
+++ b/modules/govuk_pay_webform/govuk_pay_webform.routing.yml
@@ -5,3 +5,9 @@ govuk_pay_webform.confirmation_page:
     _title_callback: '\Drupal\govuk_pay_webform\Controller\GovPayWebformController::confirmationPageTitle'
   requirements:
     _govuk_pay_webform_access_check: 'TRUE'
+govuk_pay_webform.retry_page:
+  path: 'gov-pay-webform-retry'
+  defaults:
+    _controller: '\Drupal\govuk_pay_webform\Controller\GovPayWebformController::retryPaymentRedirect'
+  requirements:
+    _govuk_pay_webform_access_check: 'TRUE'
diff --git a/modules/govuk_pay_webform/src/Controller/GovPayWebformController.php b/modules/govuk_pay_webform/src/Controller/GovPayWebformController.php
index b6527c7..ecf7410 100644
--- a/modules/govuk_pay_webform/src/Controller/GovPayWebformController.php
+++ b/modules/govuk_pay_webform/src/Controller/GovPayWebformController.php
@@ -8,13 +8,17 @@ use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Routing\TrustedRedirectResponse;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TempStore\PrivateTempStoreFactory;
+use Drupal\Core\Url;
 use Drupal\Core\Utility\Error;
 use Drupal\Core\Utility\Token;
 use Drupal\govuk_pay\Entity\GovUkPayment;
 use Drupal\govuk_pay\PaymentEventService;
 use Drupal\govuk_pay_webform\GovUkPayWebformService;
 use Drupal\webform\Entity\Webform;
+use Drupal\webform\Plugin\WebformHandlerInterface;
 use Drupal\webform\WebformInterface;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
@@ -88,6 +92,13 @@ class GovPayWebformController extends ControllerBase {
    */
   protected $verboseLogging;
 
+  /**
+   * The tempstore service.
+   *
+   * @var \Drupal\Core\TempStore\PrivateTempStore
+   */
+  protected $tempStore;
+
   /**
    * Constructs a new GovPayWebformController object.
    *
@@ -107,6 +118,8 @@ class GovPayWebformController extends ControllerBase {
    *   The token service.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory.
+   * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
+   *   The tempstore factory.
    */
   public function __construct(
     EntityTypeManagerInterface $entity_type_manager,
@@ -117,6 +130,7 @@ class GovPayWebformController extends ControllerBase {
     LoggerInterface $logger,
     Token $token,
     ConfigFactoryInterface $config_factory,
+    PrivateTempStoreFactory $temp_store_factory
   ) {
     $this->entityTypeManager = $entity_type_manager;
     $this->paymentService = $payment_service;
@@ -127,6 +141,7 @@ class GovPayWebformController extends ControllerBase {
     $this->token = $token;
     $this->configFactory = $config_factory;
     $this->verboseLogging = (bool) $this->configFactory->get('govuk_pay.settings')->get('verbose_logging');
+    $this->tempStore = $temp_store_factory->get('govuk_pay_webform');
   }
 
   /**
@@ -134,14 +149,15 @@ class GovPayWebformController extends ControllerBase {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-    $container->get('entity_type.manager'),
-    $container->get('govuk_pay_webform.payment_service'),
-    $container->get('govuk_pay.payment_event_service'),
-    $container->get('request_stack'),
-    $container->get('messenger'),
-    $container->get('logger.factory')->get('govuk_pay_webform'),
-    $container->get('token'),
-    $container->get('config.factory'),
+      $container->get('entity_type.manager'),
+      $container->get('govuk_pay_webform.payment_service'),
+      $container->get('govuk_pay.payment_event_service'),
+      $container->get('request_stack'),
+      $container->get('messenger'),
+      $container->get('logger.factory')->get('govuk_pay_webform'),
+      $container->get('token'),
+      $container->get('config.factory'),
+      $container->get('tempstore.private')
     );
   }
 
@@ -189,6 +205,7 @@ class GovPayWebformController extends ControllerBase {
     $page = [];
     $page['#payment_id'] = $payment_details['payment_id'];
     $page['#status'] = $payment_details['status'];
+    $page['#code'] = $payment_details['code'];
     $page['#amount'] = $payment_details['amount'];
     $page['#payment_for'] = $payment_details['payment_for'];
     $page['#payment_reference'] = $payment_details['payment_reference'];
@@ -199,6 +216,7 @@ class GovPayWebformController extends ControllerBase {
         $session_payment_data['submission_id'],
         $payment_details,
       );
+      $page['#retry_url'] = Url::fromRoute('govuk_pay_webform.retry_page')->toString();
     }
     else {
       $page['#theme'] = 'govuk_pay_webform__payment_success_page';
@@ -214,6 +232,41 @@ class GovPayWebformController extends ControllerBase {
     return $page;
   }
 
+  public function retryPaymentRedirect() {
+    try {
+      $session_payment_data = $this->getPaymentSession();
+      if (
+        $session_payment_data['webform_id'] &&
+        $session_payment_data['submission_id']
+      ) {
+        $handler = $this->getWebformHandler($session_payment_data['webform_id']);
+        $configuration = $handler->getConfiguration();
+        $webform_submission = $this
+          ->entityTypeManager
+          ->getStorage('webform_submission')
+          ->load($session_payment_data['submission_id']);
+
+        $this->paymentService->createPayment($webform_submission, $configuration['settings']);
+        // The service puts the redirect URL in the tempStore.
+        // Could be nice to refactor.
+        $redirect_url = $this->tempStore->get('redirect_url');
+        if (!empty($redirect_url)) {
+          // Clear the stored redirect URL to prevent repeated redirects.
+          $this->tempStore->delete('redirect_url');
+          // Create and prepare the redirect response.
+          return new TrustedRedirectResponse($redirect_url, 302);
+        }
+      }
+    }
+    catch (GovPayWebformControllerException $e) {
+    }
+    return [
+      '#theme' => 'govuk_pay_webform__payment_error_page',
+      '#error' => $e->getMessage(),
+      '#cache' => ['max-age' => 0],
+    ];
+  }
+
   /**
    * Retrieve the payment data from the session.
    *
@@ -401,29 +454,33 @@ class GovPayWebformController extends ControllerBase {
     if ($submission_id) {
       $webform_submission = $this->entityTypeManager->getStorage('webform_submission')->load($submission_id);
     }
+    if ($handler = $this->getWebformHandler($webform_id)) {
+      $webform = Webform::load($webform_id);
+      $configuration = $handler->getConfiguration();
+      $message_template = $configuration['settings'][$message_id] ? $configuration['settings'][$message_id] : NULL;
+      if ($message_template && $webform_submission) {
+        // Replace tokens in the confirmation message.
+        $token_data = [
+          'webform' => $webform,
+          'webform_submission' => $webform_submission,
+        ];
+        $message = $this->token->replace($message_template, $token_data);
+      }
+    }
 
-    // Get the confirmation message from the GovPayHandler configuration.
+    return $message;
+  }
+
+  protected function getWebformHandler(int|string $webform_id): ?WebformHandlerInterface {
     $webform = Webform::load($webform_id);
     if ($webform instanceof WebformInterface) {
-      // Load confirmation message.
       foreach ($webform->getHandlers() as $handler) {
         if ($handler->getPluginId() === 'govuk_pay') {
-          $configuration = $handler->getConfiguration();
-          $message_template = $configuration['settings'][$message_id] ? $configuration['settings'][$message_id] : NULL;
-          if ($message_template && $webform_submission) {
-            // Replace tokens in the confirmation message.
-            $token_data = [
-              'webform' => $webform,
-              'webform_submission' => $webform_submission,
-            ];
-            $message = $this->token->replace($message_template, $token_data);
-          }
-          break;
+          return $handler;
         }
       }
     }
-
-    return $message;
+    return NULL;
   }
 
   /**
diff --git a/modules/govuk_pay_webform/src/GovUkPayWebformService.php b/modules/govuk_pay_webform/src/GovUkPayWebformService.php
index 97f04c3..3fa100d 100644
--- a/modules/govuk_pay_webform/src/GovUkPayWebformService.php
+++ b/modules/govuk_pay_webform/src/GovUkPayWebformService.php
@@ -595,6 +595,7 @@ class GovUkPayWebformService {
       $state = $api_record->getState();
       $details['status'] = $state ? $state->getStatus() : 'Status not found.';
       $details['message'] = $state ? $state->getMessage() : '';
+      $details['code'] = $state ? $state->getCode() : '';
 
       if ($api_record->getAmount()) {
         $details['amount'] = '£' . number_format(floatval($api_record->getAmount()) / 100, 2);
diff --git a/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-failure-page.html.twig b/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-failure-page.html.twig
index b19a7d0..a0896af 100644
--- a/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-failure-page.html.twig
+++ b/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-failure-page.html.twig
@@ -1,13 +1,18 @@
 {{ attach_library('govuk_pay_webform/confirmation') }}
 <div>
     <div id="default_message">
-        <h2>Payment failure</h1>
+        <h2>{{ 'Payment failure'|t }}</h1>
         <dl>
-            <dt>ID</dt><dd>{{ payment_id }}</dd></dt>
-            <dt>Status</dt><dd>{{ status }}</dd>
+            <dt>{{ 'ID'|t }}</dt><dd>{{ payment_id }}</dd></dt>
+            <dt>{{ 'Reference'|t }}</dt><dd>{{ payment_reference }}</dd></dt>
+            <dt>{{ 'Status'|t }}</dt><dd>{{ code }} {{ status }}</dd>
+            <dt>{{ 'Description'|t }}</dt><dd>{{ status_description }}</dd>
         </dl>
     </div>
     <div id="confirmation-message">
         {{ message }}
     </div>
+    <div id="retry">
+      <a href="{{ retry_url }}">{{ 'Retry'|t }}</a>
+    </div>
 </div>
diff --git a/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-success-page.html.twig b/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-success-page.html.twig
index a6dca51..11da358 100644
--- a/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-success-page.html.twig
+++ b/modules/govuk_pay_webform/templates/govuk-pay-webform--payment-success-page.html.twig
@@ -1,13 +1,13 @@
 {{ attach_library('govuk_pay_webform/confirmation') }}
 <div>
     <div id="default_message">
-        <h2>Payment complete</h1>
+        <h2>{{ 'Payment complete'|t }}</h1>
         <dl>
-            <dt>ID</dt><dd>{{ payment_id }}</dd></dt>
-            <dt>Reference</dt><dd>{{ payment_reference }}</dd>
-            <dt>Payment for</dt><dd>{{ payment_for }}</dd>
-            <dt>Amount</dt><dd>{{ amount }}</dd>
-            <dt>Current status</dt><dd>{{ status }}</dd>
+            <dt>{{ 'ID'|t }}</dt><dd>{{ payment_id }}</dd></dt>
+            <dt>{{ 'Reference'|t }}</dt><dd>{{ payment_reference }}</dd>
+            <dt>{{ 'Payment for'|t }}</dt><dd>{{ payment_for }}</dd>
+            <dt>{{ 'Amount'|t }}</dt><dd>{{ amount }}</dd>
+            <dt>{{ 'Current status'|t }}</dt><dd>{{ status }}</dd>
         </dl>
     </div>
     <div id="confirmation-message">
-- 
GitLab

