woocommerce / woocommerce-gateway-stripe

The official Stripe Payment Gateway for WooCommerce
https://wordpress.org/plugins/woocommerce-gateway-stripe/
236 stars 206 forks source link

Fix duplicate order notes/emails on subscription renewal orders #3572

Closed mattallan closed 2 weeks ago

mattallan commented 3 weeks ago

Fixes #3568 Part of the on-going duplicate order notes/emails issue: #3501

Changes proposed in this Pull Request:

During the subscription renewal order payment process, the order payment is locked before initiating the first Stripe create/confirm intent request (code reference).

In the current develop branch, the order is being unlocked prematurely (code reference). Here's the current sequence:

1. process_subscription_payment( $amount, $renewal_order )
2. lock_order_payment( $renewal_order )
3. create_and_confirm_intent_for_off_session( ... )
4. unlock_order_payment( $renewal_order )
5. get_latest_charge_from_intent()                // sends a "GET charges/ID" request off to Stripe
6. process_response( $charge, $renewal_order )    // marks the order from pending -> processing/completed status

When there are delays in fetching the latest charge from Stripe or if the payment_intent.succeeded webhook is sent quickly, the current request and incoming webhook may be processed in parallel, leading to both requests calling $renewal_order->payment_complete() and adding the "Stripe charge completed." order note.

This results in duplicate emails being sent to customers and third-party code hooked to status transitions is fired multiple times.

Solution

This PR addresses the issue by moving the unlock step to occur after processing the response and handling any exceptions. This ensures the order remains locked until all necessary actions are complete, preventing parallel handling conflicts.

Testing instructions

To replicate this consistently, I had to add a small sleep( 2 ); between fetching the latest charge from Stripe and calling process_response() (here). While it seems hacky, this change simulates a slow Stripe request to fetch a charge object and assists with giving time for the payment_intent.succeeded webhook to be sent

$latest_charge = $this->get_latest_charge_from_intent( $response );
sleep( 2 );
$this->process_response( ( ! empty( $latest_charge ) ) ? $latest_charge : $response, $renewal_order );
  1. Activate WooCommerce Subscriptions and purchase a subscription product with any successful card 4242424242424242
  2. While on develop process a renewal order using the "Process renewal" action on the Edit Subscription page
  3. While visiting admin Edit Order page for new renewal order and the admin Edit Subscription page, notice the double order notes.
  4. Checkout this branch and add the same sleep( 2 ) in the code.
  5. Confirm no duplicate order notes.
develop this PR
image image

Post merge