Commit 68003cc9cc7 for woocommerce
commit 68003cc9cc7a654544c8a3ae6b295db1a2fdf140
Author: dzeusking-dev <dzeusking@googlemail.com>
Date: Tue Oct 27 16:48:35 2026 +0100
Store API: don't reuse the first checkout's payment_data in later checkout requests (#70273)
* Store API: don't reuse the first checkout's payment_data in later requests
CheckoutTrait::get_request_payment_data() cached the parsed payment data
in a function-static array. Store API batch requests run several checkout
subrequests in one PHP request, so every checkout after the first received
the first subrequest's payment_data. Parse the data from each request
instead.
Fixes #70272.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Assert each checkout response in the payment_data regression test
Check that both checkout requests end with the handler's failed payment,
so a later route error cannot leave the test green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Remove PHPStan baseline entries fixed by dropping the payment_data cache
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: dzeusking-dev <262029076+dzeusking-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mike Jolley <mike.jolley@me.com>
diff --git a/plugins/woocommerce/changelog/70272-fix-store-api-batch-payment-data b/plugins/woocommerce/changelog/70272-fix-store-api-batch-payment-data
new file mode 100644
index 00000000000..ec0f52df8e1
--- /dev/null
+++ b/plugins/woocommerce/changelog/70272-fix-store-api-batch-payment-data
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Store API: give each checkout request in a batch its own payment data instead of reusing the first one.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 44958054efc..c445ec1e922 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -65694,12 +65694,6 @@ parameters:
count: 2
path: src/StoreApi/Routes/V1/Checkout.php
- -
- message: '#^Cannot access offset string on 0\|0\.0\|''''\|''0''\|array\<string, array\|string\>\|false\|null\.$#'
- identifier: offsetAccess.nonOffsetAccessible
- count: 1
- path: src/StoreApi/Routes/V1/Checkout.php
-
-
message: '#^Expression on left side of \?\? is not nullable\.$#'
identifier: nullCoalesce.expr
@@ -65748,12 +65742,6 @@ parameters:
count: 1
path: src/StoreApi/Routes/V1/Checkout.php
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Checkout\:\:get_request_payment_data\(\) should return array but returns array\<string, array\|string\>\|float\|int\|string\|false\|null\.$#'
- identifier: return.type
- count: 1
- path: src/StoreApi/Routes/V1/Checkout.php
-
-
message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Checkout\:\:get_request_payment_method\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
identifier: missingType.generics
@@ -65952,12 +65940,6 @@ parameters:
count: 1
path: src/StoreApi/Routes/V1/CheckoutOrder.php
- -
- message: '#^Cannot access offset string on 0\|0\.0\|''''\|''0''\|array\<string, array\|string\>\|false\|null\.$#'
- identifier: offsetAccess.nonOffsetAccessible
- count: 1
- path: src/StoreApi/Routes/V1/CheckoutOrder.php
-
-
message: '#^Instanceof between Automattic\\WooCommerce\\StoreApi\\Payments\\PaymentResult and Automattic\\WooCommerce\\StoreApi\\Payments\\PaymentResult will always evaluate to true\.$#'
identifier: instanceof.alwaysTrue
@@ -65988,12 +65970,6 @@ parameters:
count: 1
path: src/StoreApi/Routes/V1/CheckoutOrder.php
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\CheckoutOrder\:\:get_request_payment_data\(\) should return array but returns array\<string, array\|string\>\|float\|int\|string\|false\|null\.$#'
- identifier: return.type
- count: 1
- path: src/StoreApi/Routes/V1/CheckoutOrder.php
-
-
message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\CheckoutOrder\:\:get_request_payment_method\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
identifier: missingType.generics
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php b/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
index eddcc62157d..3234cf4b447 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
@@ -321,10 +321,7 @@ trait CheckoutTrait {
* @return array
*/
private function get_request_payment_data( \WP_REST_Request $request ) {
- static $payment_data = [];
- if ( ! empty( $payment_data ) ) {
- return $payment_data;
- }
+ $payment_data = [];
if ( ! empty( $request['payment_data'] ) ) {
foreach ( $request['payment_data'] as $data ) {
$payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
index 169fc9fab3d..19b6acb09d4 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
@@ -3554,6 +3554,41 @@ class Checkout extends \WP_Test_REST_TestCase {
$this->assertSame( $order_id, (int) $persisted_session_data['order_awaiting_payment'], 'Redirect payments should leave the persisted order linked to the shopper session.' );
}
+ /**
+ * @testdox Each checkout request handled in the same PHP request receives its own payment data, as Store API batch subrequests do.
+ */
+ public function test_payment_data_is_not_reused_across_checkout_requests_in_one_php_request(): void {
+ $received_markers = array();
+ $payment_handler = function ( $context, $payment_result ) use ( &$received_markers ) {
+ $received_markers[] = $context->payment_data['marker'] ?? '';
+ $payment_result->set_status( 'failure' );
+ };
+
+ add_action( 'woocommerce_rest_checkout_process_payment_with_context', $payment_handler, 1, 2 );
+
+ try {
+ foreach ( array( 'ALPHA', 'BETA' ) as $marker ) {
+ $request = $this->build_valid_post_request();
+ $request->set_param(
+ 'payment_data',
+ array(
+ array(
+ 'key' => 'marker',
+ 'value' => $marker,
+ ),
+ )
+ );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+ $this->assertSame( 'failure', $data['payment_result']['payment_status'] ?? null, "Checkout {$marker} should end with the handler's failed payment: " . print_r( $data, true ) );
+ }
+ } finally {
+ remove_action( 'woocommerce_rest_checkout_process_payment_with_context', $payment_handler, 1 );
+ }
+
+ $this->assertSame( array( 'ALPHA', 'BETA' ), $received_markers, 'The second checkout must not receive payment data from the first.' );
+ }
+
/**
* @testdox A logged in customer's VAT exemption should survive a failed checkout that saves their account.
*/