Commit 8850e8f75cc for woocommerce
commit 8850e8f75cc7a728c4ccf9fadced85157a95f675
Author: Néstor Soriano <konamiman@konamiman.com>
Date: Mon Jul 6 16:30:32 2026 +0200
Fix CartUpdateItem truncating float quantities in the update hook (#66312)
diff --git a/plugins/woocommerce/changelog/fix-cartupdateitem-truncates-float-quantities b/plugins/woocommerce/changelog/fix-cartupdateitem-truncates-float-quantities
new file mode 100644
index 00000000000..b3d3580e509
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-cartupdateitem-truncates-float-quantities
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix/normalize the Store API cart/update-item route truncating decimal quantities when dispatching the cart item updated hook.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php
index d87d57c985a..44d12922096 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php
@@ -130,7 +130,7 @@ class CartAddItem extends AbstractCartRoute {
if ( ! empty( $cart_item ) ) {
$product_id = $cart_item['variation_id'] ? $cart_item['variation_id'] : $cart_item['product_id'];
- $quantity = $add_to_cart_data['quantity'] ?? $cart_item['quantity'];
+ $quantity = wc_stock_amount( $add_to_cart_data['quantity'] ?? $cart_item['quantity'] );
/**
* Fires when an item is added to the cart from a user request.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateItem.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateItem.php
index a71a960f7f3..b4567294cb2 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateItem.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateItem.php
@@ -72,21 +72,22 @@ class CartUpdateItem extends AbstractCartRoute {
if ( isset( $request['quantity'] ) ) {
$cart_item = $cart->get_cart_item( $request['key'] );
- $old_quantity = $cart_item['quantity'] ?? 0;
- $this->cart_controller->set_cart_item_quantity( $request['key'], $request['quantity'] );
+ $old_quantity = wc_stock_amount( $cart_item['quantity'] ?? 0 );
+ $new_quantity = wc_stock_amount( $request['quantity'] );
+ $this->cart_controller->set_cart_item_quantity( $request['key'], $new_quantity );
- if ( $old_quantity !== (int) $request['quantity'] ) {
+ if ( $old_quantity !== $new_quantity ) {
/**
* Fires when a cart item quantity is updated from a user request.
*
* @param string $cart_item_key Cart item key.
- * @param int $quantity New quantity.
+ * @param int|float $quantity New quantity.
* @param int|float $old_quantity Old quantity.
* @param \WC_Cart $cart Cart object.
*
* @since 10.6.0
*/
- do_action( 'internal_woocommerce_cart_item_updated_from_user_request', $request['key'], (int) $request['quantity'], $old_quantity, $cart );
+ do_action( 'internal_woocommerce_cart_item_updated_from_user_request', $request['key'], $new_quantity, $old_quantity, $cart );
}
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
index de1419b10d0..f12deb2fdd5 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
@@ -1315,4 +1315,160 @@ class Cart extends ControllerTestCase {
remove_action( 'internal_woocommerce_cart_item_updated_from_user_request', $callback );
}
+
+ /**
+ * @testdox Should fire internal_woocommerce_cart_item_updated_from_user_request with untruncated quantities on stores with decimal quantities.
+ */
+ public function test_update_item_fires_update_action_with_float_quantity(): void {
+ remove_filter( 'woocommerce_stock_amount', 'intval' );
+ add_filter( 'woocommerce_stock_amount', 'floatval' );
+ $multiple_of_callback = function () {
+ return 0.5;
+ };
+ add_filter( 'woocommerce_store_api_product_quantity_multiple_of', $multiple_of_callback );
+
+ wc()->cart->set_quantity( $this->keys[0], 1.5 );
+
+ $captured_args = array();
+ // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ $callback = function ( $cart_item_key, $quantity, $old_quantity, $cart ) use ( &$captured_args ) {
+ $captured_args = compact( 'cart_item_key', 'quantity', 'old_quantity', 'cart' );
+ };
+
+ add_action( 'internal_woocommerce_cart_item_updated_from_user_request', $callback, 10, 4 );
+
+ $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' => $this->keys[0],
+ 'quantity' => 2.5,
+ )
+ );
+
+ $this->assertAPIResponse( $request, 200 );
+
+ $this->assertNotEmpty( $captured_args, 'The update action should have been fired' );
+ $this->assertSame( 2.5, $captured_args['quantity'], 'The new quantity should not be truncated' );
+ $this->assertSame( 1.5, $captured_args['old_quantity'], 'The old quantity should not be truncated' );
+ $this->assertSame( 2.5, wc()->cart->get_cart_item( $this->keys[0] )['quantity'], 'The cart item quantity should be the untruncated value' );
+
+ remove_action( 'internal_woocommerce_cart_item_updated_from_user_request', $callback );
+ remove_filter( 'woocommerce_store_api_product_quantity_multiple_of', $multiple_of_callback );
+ remove_filter( 'woocommerce_stock_amount', 'floatval' );
+ add_filter( 'woocommerce_stock_amount', 'intval' );
+ }
+
+ /**
+ * @testdox Should not fire internal_woocommerce_cart_item_updated_from_user_request when a decimal quantity is unchanged.
+ */
+ public function test_update_item_with_same_float_quantity_does_not_fire_update_action(): void {
+ remove_filter( 'woocommerce_stock_amount', 'intval' );
+ add_filter( 'woocommerce_stock_amount', 'floatval' );
+ $multiple_of_callback = function () {
+ return 0.5;
+ };
+ add_filter( 'woocommerce_store_api_product_quantity_multiple_of', $multiple_of_callback );
+
+ wc()->cart->set_quantity( $this->keys[0], 1.5 );
+
+ $action_fired = false;
+ $callback = function () use ( &$action_fired ) {
+ $action_fired = true;
+ };
+
+ add_action( 'internal_woocommerce_cart_item_updated_from_user_request', $callback );
+
+ $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' => $this->keys[0],
+ 'quantity' => 1.5,
+ )
+ );
+
+ $this->assertAPIResponse( $request, 200 );
+
+ $this->assertFalse( $action_fired, 'The update action should not fire when a decimal quantity is unchanged' );
+
+ remove_action( 'internal_woocommerce_cart_item_updated_from_user_request', $callback );
+ remove_filter( 'woocommerce_store_api_product_quantity_multiple_of', $multiple_of_callback );
+ remove_filter( 'woocommerce_stock_amount', 'floatval' );
+ add_filter( 'woocommerce_stock_amount', 'intval' );
+ }
+
+ /**
+ * @testdox Should fire internal_woocommerce_cart_item_updated_from_user_request with numeric quantities when the cart contains a string quantity.
+ */
+ public function test_update_item_fires_update_action_with_numeric_quantities_for_string_cart_quantity(): void {
+ $cart_contents = wc()->cart->get_cart_contents();
+ $cart_contents[ $this->keys[0] ]['quantity'] = '2';
+ wc()->cart->set_cart_contents( $cart_contents );
+
+ $captured_args = array();
+ // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ $callback = function ( $cart_item_key, $quantity, $old_quantity, $cart ) use ( &$captured_args ) {
+ $captured_args = compact( 'cart_item_key', 'quantity', 'old_quantity', 'cart' );
+ };
+
+ add_action( 'internal_woocommerce_cart_item_updated_from_user_request', $callback, 10, 4 );
+
+ $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' => $this->keys[0],
+ 'quantity' => 3,
+ )
+ );
+
+ $this->assertAPIResponse( $request, 200 );
+
+ $this->assertNotEmpty( $captured_args, 'The update action should have been fired' );
+ $this->assertSame( 3, $captured_args['quantity'], 'The new quantity should be a numeric value' );
+ $this->assertSame( 2, $captured_args['old_quantity'], 'The old quantity should be normalized to a numeric value' );
+
+ remove_action( 'internal_woocommerce_cart_item_updated_from_user_request', $callback );
+ }
+
+ /**
+ * @testdox Should fire internal_woocommerce_cart_item_added_from_user_request with a numeric quantity when a filter sets a string quantity.
+ */
+ public function test_add_item_fires_add_action_with_numeric_quantity_for_string_quantity(): void {
+ wc_empty_cart();
+
+ $add_to_cart_data_callback = function ( $add_to_cart_data ) {
+ $add_to_cart_data['quantity'] = '2';
+ return $add_to_cart_data;
+ };
+ add_filter( 'woocommerce_store_api_add_to_cart_data', $add_to_cart_data_callback );
+
+ $captured_args = array();
+ $callback = function ( $product_id, $quantity ) use ( &$captured_args ) {
+ $captured_args = array(
+ 'product_id' => $product_id,
+ 'quantity' => $quantity,
+ );
+ };
+
+ add_action( 'internal_woocommerce_cart_item_added_from_user_request', $callback, 10, 2 );
+
+ $request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/add-item' );
+ $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
+ $request->set_body_params(
+ array(
+ 'id' => $this->products[0]->get_id(),
+ 'quantity' => 1,
+ )
+ );
+
+ $this->assertAPIResponse( $request, 201 );
+
+ $this->assertNotEmpty( $captured_args, 'The add action should have been fired' );
+ $this->assertSame( 2, $captured_args['quantity'], 'The quantity should be normalized to a numeric value' );
+
+ remove_action( 'internal_woocommerce_cart_item_added_from_user_request', $callback );
+ remove_filter( 'woocommerce_store_api_add_to_cart_data', $add_to_cart_data_callback );
+ }
}