Commit 73b8b8abe60 for woocommerce
commit 73b8b8abe60805ae60ac616f7cc93b2732656f19
Author: Thomas Roberts <5656702+opr@users.noreply.github.com>
Date: Fri Sep 18 14:45:04 2026 +0100
Prevent failed Store API requests from syncing carts (#68023)
* Prevent failed Store API requests from syncing carts
* Add changelog entry
* Test successful cart sync with uncalculated shipping
* Preserve shipping until the cart calculates it
* Require shipping evaluation before syncing
* Fix cart route test data alignment
* Explain shipping evaluation state
* Replace order test magic strings with enums
diff --git a/plugins/woocommerce/changelog/fix-store-api-failed-cart-sync b/plugins/woocommerce/changelog/fix-store-api-failed-cart-sync
new file mode 100644
index 00000000000..c3ec23649ce
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-store-api-failed-cart-sync
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent failed Store API cart requests from syncing cart data to pending orders.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
index 610563d1edb..e9a62896f93 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
@@ -165,8 +165,8 @@ abstract class AbstractCartRoute extends AbstractRoute {
}
}
- // For update requests, this will recalculate cart totals and sync draft orders with the current cart.
- if ( $this->is_update_request( $request ) ) {
+ // For successful update requests, recalculate cart totals and sync draft orders with the current cart.
+ if ( $this->is_update_request( $request ) && ! is_wp_error( $response ) ) {
$this->cart_updated( $request );
}
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php b/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php
index 9f6b22ba638..0bfb0dbfc95 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php
@@ -845,10 +845,14 @@ class OrderController {
wc()->checkout->create_order_line_items( $order, $cart );
}
- if ( $order->get_meta( '_shipping_hash' ) !== $cart_hashes['shipping'] ) {
- $order->update_meta_data( '_shipping_hash', $cart_hashes['shipping'] );
+ // Shipping is evaluated after calculation, or when no shipping is needed and methods were initialized.
+ // A non-null methods value means evaluation ran; it may still be an empty array.
+ $shipping_evaluated = $cart->has_calculated_shipping() || ( ! $cart->needs_shipping() && null !== $cart->get_shipping_methods() );
+
+ if ( $shipping_evaluated && $order->get_meta( '_shipping_hash' ) !== $cart_hashes['shipping'] ) {
$order->remove_order_items( OrderItemType::SHIPPING );
wc()->checkout->create_order_shipping_lines( $order, wc()->session->get( 'chosen_shipping_methods' ), wc()->shipping()->get_packages() );
+ $order->update_meta_data( '_shipping_hash', $cart_hashes['shipping'] );
}
if ( $order->get_meta( '_coupons_hash' ) !== $cart_hashes['coupons'] ) {
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartExtensions.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartExtensions.php
index 8220d0fe708..0eacde6b443 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartExtensions.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartExtensions.php
@@ -5,6 +5,8 @@
namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes;
+use Automattic\WooCommerce\Enums\OrderItemType;
+use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes\ControllerTestCase;
use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
@@ -139,4 +141,142 @@ class CartExtensions extends ControllerTestCase {
)
);
}
+
+ /**
+ * A rejected cart update must not sync the cart to a pending order.
+ *
+ * @see https://github.com/woocommerce/woocommerce/issues/68007
+ */
+ public function test_rejected_cart_update_does_not_remove_shipping_from_pending_order() {
+ $order = $this->create_pending_order_with_shipping();
+ $this->reset_shipping_calculation();
+
+ $request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/extensions' );
+ $request->set_header( 'Nonce', 'invalid' );
+ $request->set_body_params(
+ array(
+ 'namespace' => 'valid-test-plugin',
+ )
+ );
+
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertSame( 403, $response->get_status() );
+ $this->assertPendingOrderRetainsShipping( $order );
+ }
+
+ /**
+ * A successful no-op cart update must not sync uncalculated shipping to a pending order.
+ *
+ * @dataProvider needs_shipping_filter_provider
+ * @see https://github.com/woocommerce/woocommerce/issues/68007
+ *
+ * @param bool $filter_needs_shipping Whether to filter the cart to not need shipping.
+ */
+ public function test_successful_no_op_cart_update_does_not_remove_shipping_from_pending_order( $filter_needs_shipping ) {
+ $fixtures = new FixtureData();
+ $fixtures->shipping_add_flat_rate();
+
+ $order = $this->create_pending_order_with_shipping();
+ $this->reset_shipping_calculation();
+
+ $needs_shipping_filter = static function () {
+ return false;
+ };
+
+ if ( $filter_needs_shipping ) {
+ add_filter( 'woocommerce_cart_needs_shipping', $needs_shipping_filter, 999 );
+ }
+
+ try {
+ $this->assertSame( ! $filter_needs_shipping, wc()->cart->needs_shipping() );
+ $this->assertFalse( wc()->cart->has_calculated_shipping() );
+ $this->assertNull( wc()->cart->get_shipping_methods() );
+ $this->assertSame( array(), wc()->shipping()->get_packages() );
+
+ $request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/update-item' );
+ $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
+ $request->set_body_params(
+ array(
+ 'key' => array_key_first( wc()->cart->get_cart() ),
+ )
+ );
+
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $this->assertFalse( wc()->cart->has_calculated_shipping() );
+ $this->assertSame( array(), wc()->shipping()->get_packages() );
+ } finally {
+ remove_filter( 'woocommerce_cart_needs_shipping', $needs_shipping_filter, 999 );
+ }
+
+ $reloaded_order = wc_get_order( $order->get_id() );
+ $this->assertSame( 'hash-from-calculated-shipping', $reloaded_order->get_meta( '_shipping_hash' ) );
+ $this->assertPendingOrderRetainsShipping( $order );
+ }
+
+ /**
+ * Data provider for carts whose shipping requirement may be filtered.
+ *
+ * @return array
+ */
+ public function needs_shipping_filter_provider() {
+ return array(
+ 'cart needs shipping' => array( false ),
+ 'needs shipping filtered false' => array( true ),
+ );
+ }
+
+ /**
+ * Create a pending order that matches the current cart and contains shipping.
+ *
+ * @return \WC_Order
+ */
+ private function create_pending_order_with_shipping() {
+ $order = wc_create_order();
+ $order->add_product( $this->product, 1 );
+
+ $shipping_item = new \WC_Order_Item_Shipping();
+ $shipping_item->set_method_title( 'Flat rate' );
+ $shipping_item->set_method_id( 'flat_rate' );
+ $shipping_item->set_instance_id( 1 );
+ $shipping_item->set_total( 10 );
+ $order->add_item( $shipping_item );
+
+ $order->set_status( OrderStatus::PENDING );
+ $order->set_cart_hash( wc()->cart->get_cart_hash() );
+ $order->update_meta_data( '_shipping_hash', 'hash-from-calculated-shipping' );
+ $order->calculate_totals();
+ $order->save();
+
+ wc()->session->set( 'store_api_draft_order', $order->get_id() );
+ wc()->session->set( 'chosen_shipping_methods', array( 'flat_rate:1' ) );
+
+ return $order;
+ }
+
+ /**
+ * Reset shipping state to match a new request that has not calculated shipping.
+ */
+ private function reset_shipping_calculation() {
+ $shipping_methods = new \ReflectionProperty( \WC_Cart::class, 'shipping_methods' );
+ $shipping_methods->setAccessible( true );
+ $shipping_methods->setValue( wc()->cart, null );
+
+ $has_calculated_shipping = new \ReflectionProperty( \WC_Cart::class, 'has_calculated_shipping' );
+ $has_calculated_shipping->setAccessible( true );
+ $has_calculated_shipping->setValue( wc()->cart, false );
+
+ wc()->shipping()->reset_shipping();
+ }
+
+ /**
+ * Assert that a pending order retains its shipping line and total.
+ *
+ * @param \WC_Order $order Order to reload and inspect.
+ */
+ private function assertPendingOrderRetainsShipping( $order ) {
+ $reloaded_order = wc_get_order( $order->get_id() );
+ $this->assertCount( 1, $reloaded_order->get_items( OrderItemType::SHIPPING ) );
+ $this->assertEquals( 20.0, (float) $reloaded_order->get_total() );
+ }
}