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