Skip to content

Commit c58df12

Browse files
mattallanwjrosa
andauthored
Refactor order locking to use order meta instead of transients (#3573)
* Use order meta instead of transients to lock order payments * Add unit tests * Update inline comments * edit inline comment * Add changelog entry * Fix issues with phpunit tests comparing versions of an order that didn't contain locking metadata * Update includes/abstracts/abstract-wc-stripe-payment-gateway.php Co-authored-by: Wesley Rosa <[email protected]> * fix small whitespace --------- Co-authored-by: Wesley Rosa <[email protected]>
1 parent 2c6cab0 commit c58df12

File tree

5 files changed

+77
-14
lines changed

5 files changed

+77
-14
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* Add - Show ECE button preview on settings page.
2020
* Tweak - Remove the subscription order notes added each time a source wasn't migrated.
2121
* Fix - Prevent marking renewal orders as processing/completed multiple times due to handling the Stripe webhook in parallel.
22+
* Dev - Refactor lock_order_payment() to use order meta instead of transients.
2223

2324
= 8.8.1 - 2024-10-28 =
2425
* Tweak - Disables APMs when using the legacy checkout experience due Stripe deprecation by October 29, 2024.

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
/**

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,5 +129,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
129129
* Add - Show ECE button preview on settings page.
130130
* Tweak - Remove the subscription order notes added each time a source wasn't migrated.
131131
* Fix - Prevent marking renewal orders as processing/completed multiple times due to handling the Stripe webhook in parallel.
132+
* Dev - Refactor lock_order_payment() to use order meta instead of transients.
132133

133134
[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
@@ -625,4 +625,51 @@ public function test_render_subscription_payment_method() {
625625
// Card brands that WC core doesn't recognize will be displayed as ucwords.
626626
$this->assertEquals( 'Via Dummy card ending in 0000', $this->gateway->maybe_render_subscription_payment_method( 'N/A', $mock_subscription ) );
627627
}
628+
629+
/**
630+
* Tests for `lock_order_payment` method.
631+
*/
632+
public function test_lock_order_payment() {
633+
$order_1 = WC_Helper_Order::create_order();
634+
$locked = $this->gateway->lock_order_payment( $order_1 );
635+
636+
$this->assertFalse( $locked );
637+
$current_lock = $order_1->get_meta( '_stripe_lock_payment' );
638+
$this->assertEqualsWithDelta( (int) $current_lock, ( time() + 5 * MINUTE_IN_SECONDS ), 3 );
639+
640+
$locked = $this->gateway->lock_order_payment( $order_1 );
641+
$this->assertTrue( $locked );
642+
643+
// lock with an intent ID.
644+
$order_2 = WC_Helper_Order::create_order();
645+
$intent_id = 'pi_123intent';
646+
647+
$locked = $this->gateway->lock_order_payment( $order_2, $intent_id );
648+
$current_lock = $order_2->get_meta( '_stripe_lock_payment' );
649+
650+
$this->assertFalse( $locked );
651+
$locked = $this->gateway->lock_order_payment( $order_2, $intent_id );
652+
$this->assertTrue( $locked );
653+
$locked = $this->gateway->lock_order_payment( $order_2 ); // test that you don't need to pass the intent ID to check lock.
654+
$this->assertTrue( $locked );
655+
656+
// test expired locks.
657+
$order_3 = WC_Helper_Order::create_order();
658+
$order_3->update_meta_data( '_stripe_lock_payment', time() - 1 );
659+
$order_3->save_meta_data();
660+
661+
$locked = $this->gateway->lock_order_payment( $order_3, $intent_id );
662+
$current_lock = $order_3->get_meta( '_stripe_lock_payment' );
663+
664+
$this->assertFalse( $locked );
665+
$this->assertEqualsWithDelta( (int) $current_lock, ( time() + 5 * MINUTE_IN_SECONDS ), 3 );
666+
667+
// test two instances of the same order, one locked and one not.
668+
$order_4 = WC_Helper_Order::create_order();
669+
$dup_order = wc_get_order( $order_4->get_id() );
670+
671+
$this->gateway->lock_order_payment( $order_4 );
672+
$dup_locked = $this->gateway->lock_order_payment( $dup_order );
673+
$this->assertTrue( $dup_locked ); // Confirms lock from $order_4 prevents payment on $dup_order.
674+
}
628675
}

0 commit comments

Comments
 (0)