Skip to content

Commit 9a258c5

Browse files
committed
Merge remote-tracking branch 'origin/release/8.8.2' into trunk
2 parents 6ae72f2 + 8c15da6 commit 9a258c5

10 files changed

+112
-49
lines changed

changelog.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
*** Changelog ***
22

3+
= 8.8.2 - 2024-11-07 =
4+
* Fix - Prevent marking renewal orders as processing/completed multiple times due to handling the Stripe webhook in parallel.
5+
* Dev - Refactor lock_order_payment() to use order meta instead of transients.
6+
* Update - Process successful payment intent webhooks asynchronously.
7+
38
= 8.8.1 - 2024-10-28 =
49
* Tweak - Disables APMs when using the legacy checkout experience due Stripe deprecation by October 29, 2024.
510
* Fix - Prevent marking orders on-hold with order note "Process order to take payment" when the payment has failed.

includes/abstracts/abstract-wc-stripe-payment-gateway.php

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,17 +1672,25 @@ private function get_intent( $intent_type, $intent_id ) {
16721672
* @return bool A flag that indicates whether the order is already locked.
16731673
*/
16741674
public function lock_order_payment( $order, $intent = null ) {
1675-
$order_id = $order->get_id();
1676-
$transient_name = 'wc_stripe_processing_intent_' . $order_id;
1677-
$processing = get_transient( $transient_name );
1675+
$order->read_meta_data( true );
16781676

1679-
// Block the process if the same intent is already being handled.
1680-
if ( '-1' === $processing || ( isset( $intent->id ) && $processing === $intent->id ) ) {
1681-
return true;
1677+
$existing_lock = $order->get_meta( '_stripe_lock_payment', true );
1678+
1679+
if ( $existing_lock ) {
1680+
$parts = explode( '|', $existing_lock ); // Format is: "{expiry_timestamp}" or "{expiry_timestamp}|{pi_xxxx}" if an intent is passed.
1681+
$expiration = (int) $parts[0];
1682+
$locked_intent = ! empty( $parts[1] ) ? $parts[1] : '';
1683+
1684+
// If the lock is still active, return true.
1685+
if ( time() <= $expiration && ( empty( $intent ) || empty( $locked_intent ) || ( $intent->id ?? '' ) === $locked_intent ) ) {
1686+
return true;
1687+
}
16821688
}
16831689

1684-
// Save the new intent as a transient, eventually overwriting another one.
1685-
set_transient( $transient_name, empty( $intent ) ? '-1' : $intent->id, 5 * MINUTE_IN_SECONDS );
1690+
$new_lock = ( time() + 5 * MINUTE_IN_SECONDS ) . ( isset( $intent->id ) ? '|' . $intent->id : '' );
1691+
1692+
$order->update_meta_data( '_stripe_lock_payment', $new_lock );
1693+
$order->save_meta_data();
16861694

16871695
return false;
16881696
}
@@ -1694,8 +1702,8 @@ public function lock_order_payment( $order, $intent = null ) {
16941702
* @param WC_Order $order The order that is being unlocked.
16951703
*/
16961704
public function unlock_order_payment( $order ) {
1697-
$order_id = $order->get_id();
1698-
delete_transient( 'wc_stripe_processing_intent_' . $order_id );
1705+
$order->delete_meta_data( '_stripe_lock_payment' );
1706+
$order->save_meta_data();
16991707
}
17001708

17011709
/**

includes/class-wc-stripe-webhook-handler.php

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -928,22 +928,20 @@ public function process_payment_intent_success( $notification ) {
928928
break;
929929
case 'payment_intent.succeeded':
930930
case 'payment_intent.amount_capturable_updated':
931-
$charge = $this->get_latest_charge_from_intent( $intent );
932-
933931
WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
934932

935-
/**
936-
* Check if the order is awaiting further action from the customer. If so, do not process the payment via the webhook, let the redirect handle it.
937-
*
938-
* This is a stop-gap to fix a critical issue, see https://github.com/woocommerce/woocommerce-gateway-stripe/issues/2536. It would
939-
* be better if we removed the need for additional meta data in favor of refactoring this part of the payment processing.
940-
*/
941-
$is_awaiting_action = $order->get_meta( '_stripe_upe_waiting_for_redirect' ) ?? false;
933+
$process_webhook_async = apply_filters( 'wc_stripe_process_payment_intent_webhook_async', true, $order, $intent, $notification );
934+
$is_awaiting_action = $order->get_meta( '_stripe_upe_waiting_for_redirect' ) ?? false;
935+
936+
// Process the webhook now if it's for a voucher or wallet payment , or if filtered to process immediately and order is not awaiting action.
937+
if ( $is_voucher_payment || $is_wallet_payment || ( ! $process_webhook_async && ! $is_awaiting_action ) ) {
938+
$charge = $this->get_latest_charge_from_intent( $intent );
942939

943-
// Voucher payments are only processed via the webhook so are excluded from the above check.
944-
// Wallets are also processed via the webhook, not redirection.
945-
if ( ! $is_voucher_payment && ! $is_wallet_payment && $is_awaiting_action ) {
946-
WC_Stripe_Logger::log( "Stripe UPE waiting for redirect. Scheduled deferred webhook processing. The status for order $order_id might need manual adjustment." );
940+
do_action( 'wc_gateway_stripe_process_payment', $charge, $order );
941+
942+
$this->process_response( $charge, $order );
943+
} else {
944+
WC_Stripe_Logger::log( "Processing $notification->type ($intent->id) asynchronously for order $order_id." );
947945

948946
// Schedule a job to check on the status of this intent.
949947
$this->defer_webhook_processing(
@@ -954,14 +952,11 @@ public function process_payment_intent_success( $notification ) {
954952
]
955953
);
956954

957-
do_action( 'wc_gateway_stripe_process_payment_intent_incomplete', $order );
958-
return;
955+
if ( $is_awaiting_action ) {
956+
do_action( 'wc_gateway_stripe_process_payment_intent_incomplete', $order );
957+
}
959958
}
960959

961-
do_action( 'wc_gateway_stripe_process_payment', $charge, $order );
962-
963-
// Process valid response.
964-
$this->process_response( $charge, $order );
965960
break;
966961
default:
967962
if ( $is_voucher_payment && 'payment_intent.payment_failed' === $notification->type ) {
@@ -1123,6 +1118,11 @@ protected function handle_deferred_payment_intent_succeeded( $order, $intent_id
11231118

11241119
$charge = $this->get_latest_charge_from_intent( $intent );
11251120

1121+
if ( ! $charge ) {
1122+
WC_Stripe_Logger::log( "Skipped processing deferred webhook for Stripe PaymentIntent {$intent_id} for order {$order->get_id()} - no charge found." );
1123+
return;
1124+
}
1125+
11261126
WC_Stripe_Logger::log( "Processing Stripe PaymentIntent {$intent_id} for order {$order->get_id()} via deferred webhook." );
11271127

11281128
do_action( 'wc_gateway_stripe_process_payment', $charge, $order );

includes/compat/trait-wc-stripe-subscriptions.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -499,18 +499,15 @@ public function process_subscription_payment( $amount, $renewal_order, $retry =
499499

500500
throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
501501
}
502-
503-
// TODO: Remove when SEPA is migrated to payment intents.
504-
if ( 'stripe_sepa' !== $this->id ) {
505-
$this->unlock_order_payment( $renewal_order );
506-
}
507502
} catch ( WC_Stripe_Exception $e ) {
508503
WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
509504

510505
do_action( 'wc_gateway_stripe_process_payment_error', $e, $renewal_order );
511506

512507
/* translators: error message */
513508
$renewal_order->update_status( 'failed' );
509+
$this->unlock_order_payment( $renewal_order );
510+
514511
return;
515512
}
516513

@@ -561,6 +558,8 @@ public function process_subscription_payment( $amount, $renewal_order, $retry =
561558

562559
do_action( 'wc_gateway_stripe_process_payment_error', $e, $renewal_order );
563560
}
561+
562+
$this->unlock_order_payment( $renewal_order );
564563
}
565564

566565
/**

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "woocommerce-gateway-stripe",
33
"title": "WooCommerce Gateway Stripe",
4-
"version": "8.8.1",
4+
"version": "8.8.2",
55
"license": "GPL-3.0",
66
"homepage": "http://wordpress.org/plugins/woocommerce-gateway-stripe/",
77
"repository": {

readme.txt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Tags: credit card, stripe, apple pay, payment request, google pay, sepa, bancont
44
Requires at least: 6.4
55
Tested up to: 6.6
66
Requires PHP: 7.4
7-
Stable tag: 8.8.1
7+
Stable tag: 8.8.2
88
License: GPLv3
99
License URI: https://www.gnu.org/licenses/gpl-3.0.html
1010
Attributions: thorsten-stripe
@@ -110,11 +110,9 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
110110

111111
== Changelog ==
112112

113-
= 8.8.1 - 2024-10-28 =
114-
* Tweak - Disables APMs when using the legacy checkout experience due Stripe deprecation by October 29, 2024.
115-
* Fix - Prevent marking orders on-hold with order note "Process order to take payment" when the payment has failed.
116-
* Fix - Prevent subscriptions from being marked as "Pending" when a customer attempts to change their payment method to a declining card.
117-
* Fix - Delay updating the subscription's payment method until after the intent is confirmed when using the new checkout experience.
118-
* Fix - Display a success notice to customers after successfully changing their subscription payment method to a card that required 3DS authentication.
113+
= 8.8.2 - 2024-11-07 =
114+
* Fix - Prevent marking renewal orders as processing/completed multiple times due to handling the Stripe webhook in parallel.
115+
* Dev - Refactor lock_order_payment() to use order meta instead of transients.
116+
* Update - Process successful payment intent webhooks asynchronously.
119117

120118
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).

tests/phpunit/test-class-wc-stripe-upe-payment-gateway.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1768,8 +1768,11 @@ public function test_subscription_renewal_is_successful() {
17681768

17691769
list( $amount, $description, $metadata ) = $this->get_order_details( $order );
17701770
$order->set_payment_method( WC_Stripe_UPE_Payment_Gateway::ID );
1771+
$order->update_meta_data( '_stripe_lock_payment', ( time() + MINUTE_IN_SECONDS ) ); // To assist with comparing expected order objects, set an existing lock.
17711772
$order->save();
17721773

1774+
$order = wc_get_order( $order_id );
1775+
17731776
$payment_method_mock = self::MOCK_CARD_PAYMENT_METHOD_TEMPLATE;
17741777
$payment_method_mock['id'] = $payment_method_id;
17751778
$payment_method_mock['customer'] = $customer_id;
@@ -1799,7 +1802,7 @@ public function test_subscription_renewal_is_successful() {
17991802
$this->mock_gateway->expects( $this->once() )
18001803
->method( 'create_and_confirm_intent_for_off_session' )
18011804
->with(
1802-
wc_get_order( $order_id ),
1805+
$order,
18031806
$prepared_source,
18041807
$amount
18051808
)
@@ -1820,7 +1823,7 @@ public function test_subscription_renewal_is_successful() {
18201823
->method( 'get_latest_charge_from_intent' )
18211824
->willReturn( $this->array_to_object( $charge ) );
18221825

1823-
$this->mock_gateway->process_subscription_payment( $amount, wc_get_order( $order_id ), false, false );
1826+
$this->mock_gateway->process_subscription_payment( $amount, $order, false, false );
18241827

18251828
$final_order = wc_get_order( $order_id );
18261829
$note = wc_get_order_notes(
@@ -1859,8 +1862,11 @@ public function test_subscription_renewal_checks_payment_method_authorization()
18591862

18601863
list( $amount, $description, $metadata ) = $this->get_order_details( $order );
18611864
$order->set_payment_method( WC_Stripe_UPE_Payment_Gateway::ID );
1865+
$order->update_meta_data( '_stripe_lock_payment', ( time() + MINUTE_IN_SECONDS ) ); // To assist with comparing expected order objects, set an existing lock.
18621866
$order->save();
18631867

1868+
$order = wc_get_order( $order_id );
1869+
18641870
$payment_method_mock = self::MOCK_CARD_PAYMENT_METHOD_TEMPLATE;
18651871
$payment_method_mock['id'] = $payment_method_id;
18661872
$payment_method_mock['customer'] = $customer_id;
@@ -1898,7 +1904,7 @@ public function test_subscription_renewal_checks_payment_method_authorization()
18981904
$this->mock_gateway->expects( $this->once() )
18991905
->method( 'create_and_confirm_intent_for_off_session' )
19001906
->with(
1901-
wc_get_order( $order_id ),
1907+
$order,
19021908
$prepared_source,
19031909
$amount
19041910
)
@@ -1919,7 +1925,7 @@ public function test_subscription_renewal_checks_payment_method_authorization()
19191925
->method( 'get_latest_charge_from_intent' )
19201926
->willReturn( $this->array_to_object( $charge ) );
19211927

1922-
$this->mock_gateway->process_subscription_payment( $amount, wc_get_order( $order_id ), false, false );
1928+
$this->mock_gateway->process_subscription_payment( $amount, $order, false, false );
19231929

19241930
$final_order = wc_get_order( $order_id );
19251931
$note = wc_get_order_notes(

tests/phpunit/test-wc-stripe-payment-gateway.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,4 +568,51 @@ public function test_get_balance_transaction_id_from_charge() {
568568

569569
$this->assertEquals( null, $this->gateway->get_balance_transaction_id_from_charge( null ) );
570570
}
571+
572+
/**
573+
* Tests for `lock_order_payment` method.
574+
*/
575+
public function test_lock_order_payment() {
576+
$order_1 = WC_Helper_Order::create_order();
577+
$locked = $this->gateway->lock_order_payment( $order_1 );
578+
579+
$this->assertFalse( $locked );
580+
$current_lock = $order_1->get_meta( '_stripe_lock_payment' );
581+
$this->assertEqualsWithDelta( (int) $current_lock, ( time() + 5 * MINUTE_IN_SECONDS ), 3 );
582+
583+
$locked = $this->gateway->lock_order_payment( $order_1 );
584+
$this->assertTrue( $locked );
585+
586+
// lock with an intent ID.
587+
$order_2 = WC_Helper_Order::create_order();
588+
$intent_id = 'pi_123intent';
589+
590+
$locked = $this->gateway->lock_order_payment( $order_2, $intent_id );
591+
$current_lock = $order_2->get_meta( '_stripe_lock_payment' );
592+
593+
$this->assertFalse( $locked );
594+
$locked = $this->gateway->lock_order_payment( $order_2, $intent_id );
595+
$this->assertTrue( $locked );
596+
$locked = $this->gateway->lock_order_payment( $order_2 ); // test that you don't need to pass the intent ID to check lock.
597+
$this->assertTrue( $locked );
598+
599+
// test expired locks.
600+
$order_3 = WC_Helper_Order::create_order();
601+
$order_3->update_meta_data( '_stripe_lock_payment', time() - 1 );
602+
$order_3->save_meta_data();
603+
604+
$locked = $this->gateway->lock_order_payment( $order_3, $intent_id );
605+
$current_lock = $order_3->get_meta( '_stripe_lock_payment' );
606+
607+
$this->assertFalse( $locked );
608+
$this->assertEqualsWithDelta( (int) $current_lock, ( time() + 5 * MINUTE_IN_SECONDS ), 3 );
609+
610+
// test two instances of the same order, one locked and one not.
611+
$order_4 = WC_Helper_Order::create_order();
612+
$dup_order = wc_get_order( $order_4->get_id() );
613+
614+
$this->gateway->lock_order_payment( $order_4 );
615+
$dup_locked = $this->gateway->lock_order_payment( $dup_order );
616+
$this->assertTrue( $dup_locked ); // Confirms lock from $order_4 prevents payment on $dup_order.
617+
}
571618
}

woocommerce-gateway-stripe.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Description: Take credit card payments on your store using Stripe.
66
* Author: Stripe
77
* Author URI: https://stripe.com/
8-
* Version: 8.8.1
8+
* Version: 8.8.2
99
* Requires Plugins: woocommerce
1010
* Requires at least: 6.4
1111
* Tested up to: 6.6
@@ -22,7 +22,7 @@
2222
/**
2323
* Required minimums and constants
2424
*/
25-
define( 'WC_STRIPE_VERSION', '8.8.1' ); // WRCS: DEFINED_VERSION.
25+
define( 'WC_STRIPE_VERSION', '8.8.2' ); // WRCS: DEFINED_VERSION.
2626
define( 'WC_STRIPE_MIN_PHP_VER', '7.3.0' );
2727
define( 'WC_STRIPE_MIN_WC_VER', '7.4' );
2828
define( 'WC_STRIPE_FUTURE_MIN_WC_VER', '7.5' );

0 commit comments

Comments
 (0)