diff --git a/modules/order/src/OrderStorage.php b/modules/order/src/OrderStorage.php index dd2389b6e55213fc6a6c6383b2ebfd00c6524e51..38f36e56161bbacb109dad78826fd33dbc848575 100644 --- a/modules/order/src/OrderStorage.php +++ b/modules/order/src/OrderStorage.php @@ -170,10 +170,7 @@ class OrderStorage extends CommerceContentEntityStorage implements OrderStorageI } finally { // Release the update lock if it was acquired for this entity. - if (isset($this->updateLocks[$entity->id()])) { - $this->lockBackend->release($this->getLockId($entity->id())); - unset($this->updateLocks[$entity->id()]); - } + $this->releaseLock($entity->id()); } } @@ -209,4 +206,14 @@ class OrderStorage extends CommerceContentEntityStorage implements OrderStorageI return 'commerce_order_update:' . $order_id; } + /** + * {@inheritDoc} + */ + public function releaseLock(int $order_id): void { + if (isset($this->updateLocks[$order_id])) { + $this->lockBackend->release($this->getLockId($order_id)); + unset($this->updateLocks[$order_id]); + } + } + } diff --git a/modules/order/src/OrderStorageInterface.php b/modules/order/src/OrderStorageInterface.php index 34fabb7eebcdec2d54459f2a6e9c0a7429f10e78..9efc59818587290a14a37722a20cba78692c80fa 100644 --- a/modules/order/src/OrderStorageInterface.php +++ b/modules/order/src/OrderStorageInterface.php @@ -31,4 +31,20 @@ interface OrderStorageInterface extends ContentEntityStorageInterface { */ public function loadForUpdate(int $order_id): ?OrderInterface; + /** + * Release the order lock. + * + * In the normal scenario, the lock will be released automatically when the + * order is saved. There may be times, however, when we want to release the + * lock without a save. One is if we waited for the lock and then determined + * the order is not in a state we can't process. e.g. We wanted to process + * a draft order, but another process has completed or cancelled the order. + * Another scenario is if an exception occurs, in which case, we want to + * release the lock. + * + * @param int $order_id + * The order ID. + */ + public function releaseLock(int $order_id): void; + } diff --git a/modules/payment/src/Controller/PaymentCheckoutController.php b/modules/payment/src/Controller/PaymentCheckoutController.php index f4bd26aaf90e5035882b02e8eb652741accea872..16df5413dde0d85250ebedaa2bf98abf630b8185 100644 --- a/modules/payment/src/Controller/PaymentCheckoutController.php +++ b/modules/payment/src/Controller/PaymentCheckoutController.php @@ -112,47 +112,61 @@ class PaymentCheckoutController implements ContainerInjectionInterface { $step_id = $route_match->getParameter('step'); $this->validateStepId($step_id, $order); - // Reload the order and mark it for updating, redirecting to step below - // will save it and free the lock. This must be done before the checkout - // flow plugin is initiated to make sure that it has the reloaded order - // object. Additionally, the checkout flow plugin gets the order from - // the route match object, so update the order there as well with. The - // passed in route match object is created on-demand in - // \Drupal\Core\Controller\ArgumentResolver\RouteMatchValueResolver and is - // not the same object as the current route match service. - $order = $this->entityTypeManager->getStorage('commerce_order')->loadForUpdate($order->id()); - \Drupal::routeMatch()->getParameters()->set('commerce_order', $order); - - /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ - $payment_gateway = $order->get('payment_gateway')->entity; - $payment_gateway_plugin = $payment_gateway->getPlugin(); - if (!$payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) { - throw new AccessException('The payment gateway for the order does not implement ' . OffsitePaymentGatewayInterface::class); - } - /** @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface $checkout_flow */ - $checkout_flow = $order->get('checkout_flow')->entity; - $checkout_flow_plugin = $checkout_flow->getPlugin(); - + /** @var \Drupal\commerce_order\OrderStorageInterface $order_storage */ + $order_storage = $this->entityTypeManager->getStorage('commerce_order'); try { - $payment_gateway_plugin->onReturn($order, $request); - $redirect_step_id = $checkout_flow_plugin->getNextStepId($step_id); - } - catch (NeedsRedirectException $e) { - throw $e; - } - catch (PaymentGatewayException $e) { - $event = new FailedPaymentEvent($order, $payment_gateway, $e); - $this->eventDispatcher->dispatch($event, PaymentEvents::PAYMENT_FAILURE); - Error::logException($this->logger, $e); - $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); + // Reload the order and mark it for updating, redirecting to step below + // will save it and free the lock. This must be done before the checkout + // flow plugin is initiated to make sure that it has the reloaded order + // object. Additionally, the checkout flow plugin gets the order from + // the route match object, so update the order there as well with. The + // passed in route match object is created on-demand in + // \Drupal\Core\Controller\ArgumentResolver\RouteMatchValueResolver and is + // not the same object as the current route match service. + /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ + $order = $order_storage->loadForUpdate($order->id()); + \Drupal::routeMatch()->getParameters()->set('commerce_order', $order); + if ($order->getState()->getId() !== 'draft') { + // While we waited for the lock, the order state changed. + // Release our lock, and revalidate the step. + $order_storage->releaseLock($order->id()); + $this->validateStepId($step_id, $order); + } + + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = $order->get('payment_gateway')->entity; + $payment_gateway_plugin = $payment_gateway->getPlugin(); + if (!$payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) { + throw new AccessException('The payment gateway for the order does not implement ' . OffsitePaymentGatewayInterface::class); + } + /** @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface $checkout_flow */ + $checkout_flow = $order->get('checkout_flow')->entity; + $checkout_flow_plugin = $checkout_flow->getPlugin(); + + try { + $payment_gateway_plugin->onReturn($order, $request); + $redirect_step_id = $checkout_flow_plugin->getNextStepId($step_id); + } + catch (NeedsRedirectException $e) { + throw $e; + } + catch (PaymentGatewayException $e) { + $event = new FailedPaymentEvent($order, $payment_gateway, $e); + $this->eventDispatcher->dispatch($event, PaymentEvents::PAYMENT_FAILURE); + Error::logException($this->logger, $e); + $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); + } + catch (\Exception $e) { + Error::logException($this->logger, $e); + $this->messenger->addError(t('We encountered an issue recording your payment. Please contact customer service to resolve the issue.')); + $redirect_step_id = $checkout_flow_plugin->getPreviousStepId($step_id); + } + $checkout_flow_plugin->redirectToStep($redirect_step_id); } - catch (\Exception $e) { - Error::logException($this->logger, $e); - $this->messenger->addError(t('We encountered an issue recording your payment. Please contact customer service to resolve the issue.')); - $redirect_step_id = $checkout_flow_plugin->getPreviousStepId($step_id); + finally { + $order_storage->releaseLock($order->id()); } - $checkout_flow_plugin->redirectToStep($redirect_step_id); } /**