Use Stripe's events and webhooks to prevent the scenario where a payment passes but the order never gets completed because user session gets killed before the end.

Created on 11 December 2023, 7 months ago
Updated 14 December 2023, 7 months ago

Hello,

We've had very few cases where a payment successfully passes to Stripe, but the order never gets placed / completed on Drupal side.

I called the client on the phone and she told me she was in an area with a quite poor internet connection.
This made me wonder: how does Stripe actually communicates with the Drupal site to let it know a transaction has been successful? Does Stripe call a return url directly on our server, using curl for example? Or does it rely on the user's session to do it?
In other words, if the user's internet connection gets unstable or killed right after the payment is successfully received by Stripe, what will happen? Will Stripe still be able to let our site know the payment has been made and the order can now be placed? Or will the information be lost and the client will have paid for nothing in return?

Since I've been able to test payments locally without my local site being accessible from the outside, I guess we make an API request for the payment to proceed and wait for its response. So this process does seem indeed to rely on the user's session and internet connection.

Am I right?

If yes, wouldn't it be safer to have Stripe communicate directly with our website / server without relying on the user session?

Also, what lead me to this bug is an error I received from Stripe in the logs:
Stripe\Exception\InvalidRequestException: This PaymentIntent's payment_method could not be updated because it has a status of succeeded. You may only update the payment_method of a PaymentIntent with one of the following statuses: requires_payment_method, requires_confirmation, requires_action. in Stripe\Exception\ApiErrorException::factory()

Maybe we could / should check if a Payment Intent already has the status succeeded on Stripe's end before trying to update it. This could help us recognize this kind of problem where there has been a connection issue right after the payment has been successfully received on Stripe's end and we could then finalize the completion of the order.
But maybe this should not be done automatically so nothing bad happens without us knowing about it. But we could have some kind of warning on a dashboard telling us an order is not completed although the Stripe Payment Intent is marked successful. From here, there could be an individual action that allows us to synchronize the Payment Intent and transition the order to complete…

In my case I've only been able to manually complete the order, which worked well, however it does not have any trace of the payment that has been received by Stripe.

💬 Support request
Status

Active

Version

1.1

Component

Miscellaneous

Created by

🇫🇷France Nicolas Bouteille

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @Nicolas Bouteille
  • 🇫🇷France Nicolas Bouteille

    After digging a little bit more into that topic, this is what I think I understand:

    Stripe.php (ie Card Element) executes the payment in ajax in commerce_stripe.review.js using stripe.handleCardPayment(). Only when this ajax call is over, form.submit() is called which I assume goes to the next checkout step PaymentProcess which calls $this->createPayment in order to create the Commerce Payment inside the database. A few lines below, $payment_gateway_plugin->createPayment($payment, $this->configuration['capture']) calls Stripe.php->createPayment() and eventually the order will be placed / completed.

    StripePaymentElement.php (Payment Element) executes the payment in ajax as well in commerce_stripe.payment_element.js using stripe.confirmPayment({}) when choosing a new payment method. stripe.confirmPayment uses a return_url parameter that will redirect to PaymentCheckoutController->returnPage() which will call StripePaymentElement->onReturn() to create the payment method in our database and then call $checkout_flow_plugin->redirectToStep($redirect_step_id); to make us reach the PaymentProcess step, etc.

    Back to commerce_stripe.payment_element.js, in case we've selected a stored credit card, we fire stripe.confirmCardPayment() that does not need a return_url since we don't need to create the payment method afterwards because it already exists. Instead $form.submit(); makes us go to the Payment Process step.

    Conclusion: in all these situations, if the customer's session gets killed after the ajax call of stripe.handleCardPayment(), stripe.confirmPayment({}) or stripe.confirmCardPayment(), and we never reach the PaymentProcess step, no information about the successful payment is stored inside the database, and the order never gets completed either.

    To prevent this type of scenario from happening, we can / should plug Stripe's events to custom webhooks / api endpoints / urls instead.
    https://stripe.com/docs/payments/accept-a-payment-deferred?platform=web&...
    https://stripe.com/docs/webhooks
    With this solution, even if the user session gets closed right after the ajax payment, Stripe would still communicate with our website and we would still be able to finalize the order no matter what.
    Disavantage: for local testing, I guess this requires to have a local url accessible from the outside using something like ngrok.

    What do you guys think?

  • 🇫🇷France Nicolas Bouteille

    Because this is issue is too long, I'll just keep it a support request and we could create a cleaner, shorter, separate feature request...

Production build 0.69.0 2024