Can't place the order error for Paypal Express (when Stripe is also installed) with Magento 2

This week I needed to debug an issue when Paypal Express was being used and it was throwing a “We can’t place the order”.

If you’ve ever looked into the Paypal Express controller within Magento then you’ll see that it’s effectively a catch all when something in the process fails.

If you take a look at vendor/magento/module-paypal/Controller/Express/AbstractExpress/PlaceOrder.php:

141        } catch (ApiProcessableException $e) {
142 $this->_processPaypalApiError($e);
143 } catch (LocalizedException $e) {
144 $this->processException($e, $e->getRawMessage());
145 } catch (\Exception $e) {
146 $this->processException($e, 'We can\'t place the order.');
147 }

This bug presents itself when the user follows this process:

  • Order an item that doesn’t have free shipping
  • Guest user
  • Using a PHP8.2 compatible version of Stripe stripe/stripe-payments extension, in this case it was v3.5.16

If you imagine the customer going through this flow:

  1. Add item to cart that doesn’t have free shipping, let’s say £10.00 inc VAT.
  2. User uses a shortcut button to go and pay via Paypal Express
  3. The user authorises £10.00 at Paypal and then returns to the order review page
  4. At this point, they need to select shipping which we’ll say is £5 inc VAT.
  5. When the user now tries to place the order, the total required is £15.00 - not the pre authorised £10.

This causes Paypal to reject the transaction:

Exception #0 (Exception): PayPal gateway has rejected request. This transaction couldn't be completed. Please redirect your customer to PayPal (#10486: This transaction couldn't be completed).

In normal working order, the Paypal extension then throws a ApiProcessableException exception, which is then caught by line 141 in the code example above.

Now, this is where it gets problematic. Stripe have a plugin located at vendor/stripe/module-payments/Plugin/Sales/Model/Service/OrderService.php which has an around on the place() method:

public function aroundPlace($subject, \Closure $proceed, $order)
{
try
{
if (!empty($order) && !empty($order->getQuoteId()))
{
$this->quoteHelper->quoteId = $order->getQuoteId();
}

$savedOrder = $proceed($order);

return $this->postProcess($savedOrder);
}
catch (\Exception $e)
{
$helper = $this->helperFactory->create();
$msg = $e->getMessage();

if ($helper->isAuthenticationRequiredMessage($msg))
throw $e;
else
$helper->dieWithError($e->getMessage(), $e);
}
}

The problem here is that Stripe are assuming they’re the only paypal in play that may throw an exception. They’re just blanket catching all exceptions and hoovering them up which then catches Paypal’s ApiProcessableException exception as well - meaning the user is never redirected to Paypal to authorise the new amount of £15.00.

For a company as big as Stripe, I’m genuinely surprised that they haven’t written this better or even checked that the current payment method in the process is actually a Stripe one. Defensive programming it is not.

The patch is fairly simple:

diff --git a/vendor/stripe/module-payments/Plugin/Sales/Model/Service/OrderService.php b/vendor/stripe/module-payments/Plugin/Sales/Model/Service/OrderService.php
index 1fe4d32e2..bdd6cb0f7 100644
--- a/vendor/stripe/module-payments/Plugin/Sales/Model/Service/OrderService.php
+++ b/vendor/stripe/module-payments/Plugin/Sales/Model/Service/OrderService.php
@@ -38,6 +38,10 @@ class OrderService

public function aroundPlace($subject, \Closure $proceed, $order)
{
+ if (!str_contains($order->getPayment()->getMethod(), "stripe_")) {
+ return $proceed($order);
+ }
+
try
{
if (!empty($order) && !empty($order->getQuoteId()))

Just a check to make sure that the payment method being used is actually a Stripe one. This prevents the hoovering up on non-Stripe payment methods.

There’s a few people who have bumped into similar issues on Github too https://github.com/magento/magento2/issues/34412.