Skip to content
Snippets Groups Projects
Commit fd980596 authored by Alex Pott's avatar Alex Pott Committed by Jonathan Sacksick
Browse files

Issue #3405381 by jsacksick, lisastreeter, Jing Qian, mglaman, Nigel...

Issue #3405381 by jsacksick, lisastreeter, Jing Qian, mglaman, Nigel Cunningham, tBKoT: Add an event for payment failures during checkout.
parent 84254393
No related branches found
Tags 8.x-1.0-alpha2
7 merge requests!379Issue #3491248 Validation is breaking the amount number format decimal point,!375Issue #3413020 by czigor: Using a translatable string as a category for field...,!357Issue #2914933: Add service tags to order-related interfaces,!344Resolve #3107602 "Product attributes do not update visually when switching variations",!343Resolve #3107602 "Product attributes do not update visually when switching variations",!342Issue #3476581 by josephr5000: add OrderItemLabelEvent,!197Resolve #3405381 "Add an event"
Pipeline #57899 passed
Showing
with 338 additions and 128 deletions
......@@ -5,6 +5,8 @@ namespace Drupal\commerce_payment\Controller;
use Drupal\commerce\Response\NeedsRedirectException;
use Drupal\commerce_checkout\CheckoutOrderManagerInterface;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\Event\FailedPaymentEvent;
use Drupal\commerce_payment\Event\PaymentEvents;
use Drupal\commerce_payment\Exception\PaymentGatewayException;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface;
use Drupal\Core\Access\AccessException;
......@@ -15,6 +17,7 @@ use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
/**
......@@ -50,6 +53,13 @@ class PaymentCheckoutController implements ContainerInjectionInterface {
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected EventDispatcherInterface $eventDispatcher;
/**
* Constructs a new PaymentCheckoutController object.
*
......@@ -61,12 +71,15 @@ class PaymentCheckoutController implements ContainerInjectionInterface {
* The logger.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*/
public function __construct(CheckoutOrderManagerInterface $checkout_order_manager, MessengerInterface $messenger, LoggerInterface $logger, EntityTypeManagerInterface $entity_type_manager) {
public function __construct(CheckoutOrderManagerInterface $checkout_order_manager, MessengerInterface $messenger, LoggerInterface $logger, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher) {
$this->checkoutOrderManager = $checkout_order_manager;
$this->messenger = $messenger;
$this->logger = $logger;
$this->entityTypeManager = $entity_type_manager;
$this->eventDispatcher = $event_dispatcher;
}
/**
......@@ -77,7 +90,8 @@ class PaymentCheckoutController implements ContainerInjectionInterface {
$container->get('commerce_checkout.checkout_order_manager'),
$container->get('messenger'),
$container->get('logger.channel.commerce_payment'),
$container->get('entity_type.manager')
$container->get('entity_type.manager'),
$container->get('event_dispatcher')
);
}
......@@ -123,6 +137,8 @@ class PaymentCheckoutController implements ContainerInjectionInterface {
$redirect_step_id = $checkout_flow_plugin->getNextStepId($step_id);
}
catch (PaymentGatewayException $e) {
$event = new FailedPaymentEvent($order, $payment_gateway, $e);
$this->eventDispatcher->dispatch($event, PaymentEvents::PAYMENT_FAILURE);
$this->logger->error($e->getMessage());
$this->messenger->addError(t('Payment failed at the payment server. Please review your information and try again.'));
$redirect_step_id = $checkout_flow_plugin->getPreviousStepId($step_id);
......
<?php
namespace Drupal\commerce_payment\Event;
use Drupal\commerce\EventBase;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\Entity\PaymentGatewayInterface;
use Drupal\commerce_payment\Entity\PaymentInterface;
use Drupal\commerce_payment\Entity\PaymentMethodInterface;
use Drupal\commerce_payment\Exception\PaymentGatewayException;
/**
* Defines the event for failed payment.
*
* @see \Drupal\commerce_payment\Event\PaymentEvents
*/
class FailedPaymentEvent extends EventBase {
/**
* @var \Drupal\commerce_payment\Entity\PaymentMethodInterface|null
*/
protected ?PaymentMethodInterface $paymentMethod = NULL;
/**
* Constructs a new FailedPaymentEvent.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order entity.
* @param \Drupal\commerce_payment\Entity\PaymentGatewayInterface $paymentGateway
* The payment gateway.
* @param \Drupal\commerce_payment\Exception\PaymentGatewayException $gatewayException
* The payment gateway exception.
* @param \Drupal\commerce_payment\Entity\PaymentInterface|null $payment
* The payment.
*/
public function __construct(
protected OrderInterface $order,
protected PaymentGatewayInterface $paymentGateway,
protected PaymentGatewayException $gatewayException,
protected ?PaymentInterface $payment = NULL
) {
}
/**
* Gets the order.
*/
public function getOrder(): OrderInterface {
return $this->order;
}
/**
* Gets the payment gateway.
*/
public function getPaymentGateway(): PaymentGatewayInterface {
return $this->paymentGateway;
}
/**
* Gets the payment gateway exception.
*/
public function getGatewayException(): PaymentGatewayException {
return $this->gatewayException;
}
/**
* Gets the payment.
*/
public function getPayment(): ?PaymentInterface {
return $this->payment;
}
/**
* Gets the payment method.
*/
public function getPaymentMethod(): ?PaymentMethodInterface {
return $this->paymentMethod ?? $this->payment?->getPaymentMethod();
}
/**
* Sets the payment method.
*/
public function setPaymentMethod(PaymentMethodInterface $payment_method): self {
$this->paymentMethod = $payment_method;
return $this;
}
}
......@@ -78,4 +78,13 @@ final class PaymentEvents {
*/
const PAYMENT_DELETE = 'commerce_payment.commerce_payment.delete';
/**
* Name of the event fired when payment is failed.
*
* @Event
*
* @see \Drupal\commerce_payment\Event\FailedPaymentEvent
*/
const PAYMENT_FAILURE = 'commerce_payment.commerce_payment.failure';
}
......@@ -6,17 +6,19 @@ use Drupal\commerce\InlineFormManager;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase;
use Drupal\commerce_payment\Entity\PaymentGatewayInterface;
use Drupal\commerce_payment\Event\FailedPaymentEvent;
use Drupal\commerce_payment\Event\PaymentEvents;
use Drupal\commerce_payment\Exception\DeclineException;
use Drupal\commerce_payment\Exception\PaymentGatewayException;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\ManualPaymentGatewayInterface;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Url;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Provides the payment process pane.
......@@ -32,56 +34,28 @@ class PaymentProcess extends CheckoutPaneBase {
/**
* The inline form manager.
*
* @var \Drupal\commerce\InlineFormManager
*/
protected $inlineFormManager;
protected InlineFormManager $inlineFormManager;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
protected LoggerChannelInterface $logger;
/**
* Constructs a new PaymentProcess object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $checkout_flow
* The parent checkout flow.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\commerce\InlineFormManager $inline_form_manager
* The inline form manager.
* @param \Psr\Log\LoggerInterface $logger
* The logger.
* The event dispatcher.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow, EntityTypeManagerInterface $entity_type_manager, InlineFormManager $inline_form_manager, LoggerInterface $logger) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $checkout_flow, $entity_type_manager);
$this->inlineFormManager = $inline_form_manager;
$this->logger = $logger;
}
protected EventDispatcherInterface $eventDispatcher;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$checkout_flow,
$container->get('entity_type.manager'),
$container->get('plugin.manager.commerce_inline_form'),
$container->get('logger.channel.commerce_payment')
);
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow = NULL): self {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition, $checkout_flow);
$instance->inlineFormManager = $container->get('plugin.manager.commerce_inline_form');
$instance->logger = $container->get('logger.channel.commerce_payment');
$instance->eventDispatcher = $container->get('event_dispatcher');
return $instance;
}
/**
......@@ -173,79 +147,66 @@ class PaymentProcess extends CheckoutPaneBase {
$payment = $this->createPayment($payment_gateway);
$next_step_id = $this->checkoutFlow->getNextStepId($this->getStepId());
if ($payment_gateway_plugin instanceof SupportsStoredPaymentMethodsInterface && !$this->order->get('payment_method')->isEmpty()) {
try {
try {
if ($payment_gateway_plugin instanceof SupportsStoredPaymentMethodsInterface && !$this->order->get('payment_method')->isEmpty()) {
$payment->payment_method = $this->order->get('payment_method')->entity;
$payment_gateway_plugin->createPayment($payment, $this->configuration['capture']);
$this->checkoutFlow->redirectToStep($next_step_id);
}
catch (DeclineException $e) {
$message = $this->t('We encountered an error processing your payment method. Please verify your details and try again.');
$this->messenger()->addError($message);
$this->checkoutFlow->redirectToStep($error_step_id);
}
catch (PaymentGatewayException $e) {
$this->logger->error($e->getMessage());
$message = $this->t('We encountered an unexpected error processing your payment method. Please try again later.');
$this->messenger()->addError($message);
$this->checkoutFlow->redirectToStep($error_step_id);
}
}
elseif ($payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) {
$complete_form['actions']['next']['#value'] = $this->t('Proceed to @gateway', [
'@gateway' => $payment_gateway_plugin->getDisplayLabel(),
]);
// Make sure that the payment gateway's onCancel() method is invoked,
// by pointing the "Go back" link to the cancel URL.
$complete_form['actions']['next']['#suffix'] = Link::fromTextAndUrl($this->t('Go back'), $this->buildCancelUrl())->toString();
// Actions are not needed by gateways that embed iframes or redirect
// via GET. The inline form can show them when needed (redirect via POST).
$complete_form['actions']['#access'] = FALSE;
$inline_form = $this->inlineFormManager->createInstance('payment_gateway_form', [
'operation' => 'offsite-payment',
'catch_build_exceptions' => FALSE,
], $payment);
$pane_form['offsite_payment'] = [
'#parents' => array_merge($pane_form['#parents'], ['offsite_payment']),
'#inline_form' => $inline_form,
'#return_url' => $this->buildReturnUrl()->toString(),
'#cancel_url' => $this->buildCancelUrl()->toString(),
'#capture' => $this->configuration['capture'],
];
try {
elseif ($payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) {
$complete_form['actions']['next']['#value'] = $this->t('Proceed to @gateway', [
'@gateway' => $payment_gateway_plugin->getDisplayLabel(),
]);
// Make sure that the payment gateway's onCancel() method is invoked,
// by pointing the "Go back" link to the cancel URL.
$complete_form['actions']['next']['#suffix'] = Link::fromTextAndUrl($this->t('Go back'), $this->buildCancelUrl())->toString();
// Actions are not needed by gateways that embed iframes or redirect
// via GET. The inline form can show them when needed (redirect via POST).
$complete_form['actions']['#access'] = FALSE;
$inline_form = $this->inlineFormManager->createInstance('payment_gateway_form', [
'operation' => 'offsite-payment',
'catch_build_exceptions' => FALSE,
], $payment);
$pane_form['offsite_payment'] = [
'#parents' => array_merge($pane_form['#parents'], ['offsite_payment']),
'#inline_form' => $inline_form,
'#return_url' => $this->buildReturnUrl()->toString(),
'#cancel_url' => $this->buildCancelUrl()->toString(),
'#capture' => $this->configuration['capture'],
];
$pane_form['offsite_payment'] = $inline_form->buildInlineForm($pane_form['offsite_payment'], $form_state);
return $pane_form;
}
catch (PaymentGatewayException $e) {
$this->logger->error($e->getMessage());
$message = $this->t('We encountered an unexpected error processing your payment. Please try again later.');
$this->messenger()->addError($message);
$this->checkoutFlow->redirectToStep($error_step_id);
}
return $pane_form;
}
elseif ($payment_gateway_plugin instanceof ManualPaymentGatewayInterface) {
try {
elseif ($payment_gateway_plugin instanceof ManualPaymentGatewayInterface) {
$payment_gateway_plugin->createPayment($payment);
$this->checkoutFlow->redirectToStep($next_step_id);
}
catch (PaymentGatewayException $e) {
$this->logger->error($e->getMessage());
$message = $this->t('We encountered an unexpected error processing your payment. Please try again later.');
$this->messenger()->addError($message);
$this->checkoutFlow->redirectToStep($error_step_id);
}
}
else {
$this->logger->error('Unable process payment with :plugin_id', [
':plugin_id' => $payment_gateway_plugin->getPluginId(),
]);
$message = $this->t('We encountered an unexpected error processing your payment. Please try again later.');
// Consistently log exceptions from any type of payment gateway.
catch (PaymentGatewayException $e) {
$this->logger->error($e->getMessage());
$message = $e instanceof DeclineException ?
$this->t('We encountered an error processing your payment method. Please verify your details and try again.') :
$this->t('We encountered an unexpected error processing your payment. Please try again later.');
$this->messenger()->addError($message);
$event = new FailedPaymentEvent($this->order, $payment_gateway, $e, $payment);
$this->eventDispatcher->dispatch($event, PaymentEvents::PAYMENT_FAILURE);
$this->checkoutFlow->redirectToStep($error_step_id);
}
// If we get to this point the payment gateway is not properly configured.
$this->logger->error('Unable process payment with :plugin_id', [
':plugin_id' => $payment_gateway_plugin->getPluginId(),
]);
$message = $this->t('We encountered an unexpected error processing your payment. Please try again later.');
$this->messenger()->addError($message);
$this->checkoutFlow->redirectToStep($error_step_id);
}
/**
......
......@@ -3,12 +3,19 @@
namespace Drupal\commerce_payment\Plugin\Commerce\InlineForm;
use Drupal\commerce\Plugin\Commerce\InlineForm\EntityInlineFormBase;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\Entity\EntityWithPaymentGatewayInterface;
use Drupal\commerce_payment\Entity\PaymentGatewayInterface;
use Drupal\commerce_payment\Entity\PaymentInterface;
use Drupal\commerce_payment\Entity\PaymentMethodInterface;
use Drupal\commerce_payment\Event\FailedPaymentEvent;
use Drupal\commerce_payment\Event\PaymentEvents;
use Drupal\commerce_payment\Exception\PaymentGatewayException;
use Drupal\commerce_payment\PluginForm\PaymentGatewayFormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Provides a form element for embedding payment gateway forms.
......@@ -44,33 +51,28 @@ class PaymentGatewayForm extends EntityInlineFormBase {
protected $pluginForm;
/**
* Constructs a new PaymentGatewayForm object.
* The route match.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_factory
* The plugin form factory.
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, PluginFormFactoryInterface $plugin_form_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
protected RouteMatchInterface $routeMatch;
$this->pluginFormFactory = $plugin_form_factory;
}
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected EventDispatcherInterface $eventDispatcher;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin_form.factory')
);
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->pluginFormFactory = $container->get('plugin_form.factory');
$instance->routeMatch = $container->get('current_route_match');
$instance->eventDispatcher = $container->get('event_dispatcher');
return $instance;
}
/**
......@@ -154,6 +156,7 @@ class PaymentGatewayForm extends EntityInlineFormBase {
$this->entity = $this->pluginForm->getEntity();
}
catch (PaymentGatewayException $e) {
$this->dispatchFailedPaymentEvent($e);
$error_element = $this->pluginForm->getErrorElement($inline_form, $form_state);
$form_state->setError($error_element, $e->getMessage());
}
......@@ -170,9 +173,33 @@ class PaymentGatewayForm extends EntityInlineFormBase {
$this->entity = $this->pluginForm->getEntity();
}
catch (PaymentGatewayException $e) {
$this->dispatchFailedPaymentEvent($e);
$error_element = $this->pluginForm->getErrorElement($inline_form, $form_state);
$form_state->setError($error_element, $e->getMessage());
}
}
/**
* Dispatches a FailedPaymentEvent.
*
* @param \Drupal\commerce_payment\Exception\PaymentGatewayException $e
* The payment gateway exception.
*/
private function dispatchFailedPaymentEvent(PaymentGatewayException $e): void {
$payment_gateway = $this->entity->getPaymentGateway();
$order = $this->routeMatch->getParameter('commerce_order');
$payment = $this->entity instanceof PaymentInterface ? $this->entity : NULL;
$payment_method = $this->entity instanceof PaymentMethodInterface ? $this->entity : NULL;
if (
$order instanceof OrderInterface &&
$payment_gateway instanceof PaymentGatewayInterface
) {
$event = new FailedPaymentEvent($order, $payment_gateway, $e, $payment);
if ($payment_method) {
$event->setPaymentMethod($payment_method);
}
$this->eventDispatcher->dispatch($event, PaymentEvents::PAYMENT_FAILURE);
}
}
}
......@@ -2,14 +2,40 @@
namespace Drupal\commerce_payment\PluginForm;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\CreditCard;
use Drupal\commerce_payment\Entity\PaymentGatewayInterface;
use Drupal\commerce_payment\Entity\PaymentMethodInterface;
use Drupal\commerce_payment\Event\FailedPaymentEvent;
use Drupal\commerce_payment\Event\PaymentEvents;
use Drupal\commerce_payment\Exception\DeclineException;
use Drupal\commerce_payment\Exception\PaymentGatewayException;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class PaymentMethodEditForm extends PaymentMethodFormBase {
/**
* The event dispatcher.
*/
protected EventDispatcherInterface $eventDispatcher;
/**
* The order.
*/
protected ?OrderInterface $order = NULL;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->eventDispatcher = $container->get('event_dispatcher');
$instance->order = $container->get('current_route_match')->getParameter('commerce_order');
return $instance;
}
/**
* {@inheritdoc}
*/
......@@ -71,13 +97,23 @@ class PaymentMethodEditForm extends PaymentMethodFormBase {
$payment_gateway_plugin->updatePaymentMethod($payment_method);
$payment_method->save();
}
catch (DeclineException $e) {
$this->logger->warning($e->getMessage());
throw new DeclineException(t('We encountered an error processing your payment method. Please verify your details and try again.'));
}
catch (PaymentGatewayException $e) {
$this->logger->error($e->getMessage());
throw new PaymentGatewayException(t('We encountered an unexpected error processing your payment method. Please try again later.'));
if (
$this->order instanceof OrderInterface &&
$payment_method->getPaymentGateway() instanceof PaymentGatewayInterface
) {
$event = new FailedPaymentEvent($this->order, $payment_method->getPaymentGateway(), $e);
$event->setPaymentMethod($payment_method);
$this->eventDispatcher->dispatch($event, PaymentEvents::PAYMENT_FAILURE);
}
$this->logger->warning($e->getMessage());
$message = $e instanceof DeclineException ?
$this->t('We encountered an error processing your payment method. Please verify your details and try again.') :
$this->t('We encountered an unexpected error processing your payment. Please try again later.');
// Rethrow the original exception with a new message for security reasons.
throw new (get_class($e))($message);
}
}
......
......@@ -3,6 +3,7 @@
namespace Drupal\Tests\commerce_payment\FunctionalJavascript;
use Drupal\commerce_checkout\Entity\CheckoutFlow;
use Drupal\commerce_event_recorder_test\CommerceEventRecorder;
use Drupal\commerce_order\Adjustment;
use Drupal\commerce_order\Entity\Order;
use Drupal\commerce_payment\Entity\Payment;
......@@ -80,6 +81,7 @@ class PaymentCheckoutTest extends CommerceWebDriverTestBase {
'commerce_checkout',
'commerce_payment',
'commerce_payment_example',
'commerce_event_recorder_test',
];
/**
......@@ -643,11 +645,24 @@ class PaymentCheckoutTest extends CommerceWebDriverTestBase {
$this->assertSession()->pageTextContains('Example');
$this->assertSession()->pageTextContains('Bryan FAIL');
$this->assertSession()->pageTextContains('9 Drupal Ave');
$this->assertNull(\Drupal::state()->get(CommerceEventRecorder::STATE_KEY_PREFIX . 'onPaymentFailure'), 'No payment failure events have been recorded');
$this->submitForm([], 'Pay and complete purchase');
$this->assertSession()->pageTextNotContains('Your order number is 1. You can view your order on your account page when logged in.');
$this->assertSession()->pageTextContains('We encountered an unexpected error processing your payment. Please try again later.');
$this->assertSession()->addressEquals('checkout/1/order_information');
// Ensure the expected payment failure event has been recorded.
$expected = [
[
'order_id' => '1',
'payment_type' => 'Default',
'payment_gateway' => 'Off-site',
'payment_method' => '',
],
];
$this->assertSame($expected, \Drupal::state()->get(CommerceEventRecorder::STATE_KEY_PREFIX . 'onPaymentFailure'));
$order = Order::load(1);
$this->assertFalse($order->isLocked());
// Verify a payment was not created.
......
name: Commerce Test
type: module
description: Contains various non-specific things needed in tests.
package: Testing
dependencies:
- commerce:commerce_store
services:
commerce_test.referenceable_plugin_types_subscriber:
class: \Drupal\commerce_event_recorder_test\CommerceEventRecorder
arguments: [ '@state' ]
tags:
- { name: event_subscriber }
<?php
namespace Drupal\commerce_event_recorder_test;
use Drupal\commerce_payment\Event\FailedPaymentEvent;
use Drupal\Core\State\StateInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class CommerceEventRecorder implements EventSubscriberInterface {
public const STATE_KEY_PREFIX = 'CommerceEventRecorder.';
/**
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(private StateInterface $state) {
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Using strings here rather than class constants so this module has no
// code dependencies.
$events['commerce_payment.commerce_payment.failure'][] = ['onPaymentFailure'];
return $events;
}
/**
* @param \Drupal\commerce_payment\Event\FailedPaymentEvent $event
* The failed payment event.
*/
public function onPaymentFailure(FailedPaymentEvent $event): void {
$key = self::STATE_KEY_PREFIX . 'onPaymentFailure';
$records = $this->state->get($key, []);
$records[] = [
'order_id' => $event->getOrder()->id(),
'payment_type' => (string) $event->getPayment()?->getType()->getLabel(),
'payment_gateway' => (string) $event->getPaymentGateway()->label(),
'payment_method' => (string) $event->getPaymentMethod()?->label(),
];
$this->state->set($key, $records);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment