Commit 57413575c34 for woocommerce

commit 57413575c34ef6aeb9def304761fa2fb5826ead8
Author: Ayush Pahwa <ayush.pahwa@automattic.com>
Date:   Tue Jun 2 13:13:02 2026 +0530

    Fix decimal stock quantities below 1 being marked out of stock (#65023)

    * Fix stock quantity float truncation in validate_props

    Remove the integer cast on get_stock_quantity() inside
    WC_Abstract_Product::validate_props() so decimal stock quantities below 1
    (e.g. 0.5 via the woocommerce_stock_amount + floatval filter) are correctly
    compared against the no-stock notification threshold. The previous (int)
    cast truncated 0.5 to 0, flipping the product to "Out of stock".

    Bug introduced in PR #37855.

    Closes #41676
    Linear: https://linear.app/a8c/issue/RSMAPGJ-338

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * test: cover non-default no-stock threshold + address review nits (RSMAPGJ-338)

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * fix: apply abs() to no-stock-amount threshold + cover negative threshold (RSMAPGJ-338)

    ---------

    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-rsmapgj-338 b/plugins/woocommerce/changelog/fix-rsmapgj-338
new file mode 100644
index 00000000000..2bdc2bbbb31
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-rsmapgj-338
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Allow decimal stock quantities below 1 to remain in stock when the `woocommerce_stock_amount` filter is overridden with `floatval`. The integer cast inside `WC_Abstract_Product::validate_props()` truncated values like `0.5` to `0`, incorrectly flipping the product to "Out of stock".
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
index 0c3cfc87859..c27fb97a784 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
@@ -1527,7 +1527,7 @@ class WC_Product extends WC_Abstract_Legacy_Product {
 			return;
 		}

-		$stock_is_above_notification_threshold = ( (int) $this->get_stock_quantity() > absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ) );
+		$stock_is_above_notification_threshold = ( (float) $this->get_stock_quantity() > abs( (float) get_option( 'woocommerce_notify_no_stock_amount', 0 ) ) );
 		$backorders_are_allowed                = ( 'no' !== $this->get_backorders() );

 		if ( $stock_is_above_notification_threshold ) {
diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
index e42c6d8d3c3..7d7c250975a 100644
--- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
+++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
@@ -338,4 +338,114 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case {
 		$product->set_cogs_value( 12.34 );
 		$this->assertEquals( 123.4, $product->get_cogs_value() );
 	}
+
+	/**
+	 * @testdox validate_props() keeps a product in stock when stock quantity is a positive float below 1 and the woocommerce_stock_amount filter is set to floatval.
+	 *
+	 * See https://github.com/woocommerce/woocommerce/issues/41676 for more details.
+	 */
+	public function test_validate_props_preserves_in_stock_for_float_stock_quantity() {
+		remove_filter( 'woocommerce_stock_amount', 'intval' );
+		add_filter( 'woocommerce_stock_amount', 'floatval' );
+
+		$product = new WC_Product_Simple();
+		$product->set_manage_stock( true );
+		$product->set_stock_quantity( 0.5 );
+		$product->set_stock_status( 'instock' );
+
+		$product->validate_props();
+
+		$this->assertSame( 0.5, $product->get_stock_quantity() );
+		$this->assertSame( 'instock', $product->get_stock_status() );
+
+		remove_filter( 'woocommerce_stock_amount', 'floatval' );
+		add_filter( 'woocommerce_stock_amount', 'intval' );
+	}
+
+	/**
+	 * @testdox validate_props() compares fractional stock against a non-default woocommerce_notify_no_stock_amount threshold using a float comparison.
+	 *
+	 * See https://github.com/woocommerce/woocommerce/issues/41676 for more details.
+	 */
+	public function test_validate_props_respects_non_default_no_stock_threshold_for_float_stock_quantity() {
+		$previous_threshold = get_option( 'woocommerce_notify_no_stock_amount' );
+		update_option( 'woocommerce_notify_no_stock_amount', 5 );
+		remove_filter( 'woocommerce_stock_amount', 'intval' );
+		add_filter( 'woocommerce_stock_amount', 'floatval' );
+
+		$above_threshold = new WC_Product_Simple();
+		$above_threshold->set_manage_stock( true );
+		$above_threshold->set_stock_quantity( 5.5 );
+		$above_threshold->set_stock_status( 'instock' );
+
+		$above_threshold->validate_props();
+
+		$this->assertSame( 5.5, $above_threshold->get_stock_quantity() );
+		$this->assertSame( 'instock', $above_threshold->get_stock_status(), 'A fractional stock quantity above the threshold should stay in stock.' );
+
+		$at_threshold = new WC_Product_Simple();
+		$at_threshold->set_manage_stock( true );
+		$at_threshold->set_stock_quantity( 4.5 );
+		$at_threshold->set_stock_status( 'instock' );
+
+		$at_threshold->validate_props();
+
+		$this->assertSame( 4.5, $at_threshold->get_stock_quantity() );
+		$this->assertSame( 'outofstock', $at_threshold->get_stock_status(), 'A fractional stock quantity below the threshold should flip to out of stock.' );
+
+		remove_filter( 'woocommerce_stock_amount', 'floatval' );
+		add_filter( 'woocommerce_stock_amount', 'intval' );
+		update_option( 'woocommerce_notify_no_stock_amount', $previous_threshold );
+	}
+
+	/**
+	 * @testdox validate_props() treats a negative woocommerce_notify_no_stock_amount threshold by its magnitude (absolute value), matching the pre-#37855 absint() behaviour.
+	 *
+	 * Without the abs() wrap, a negative threshold would let every positive stock value compare as "above threshold"
+	 * and stay in stock. With abs(), the threshold is compared by magnitude, so -5 behaves like 5.
+	 */
+	public function test_validate_props_treats_negative_no_stock_threshold_by_magnitude() {
+		$previous_threshold = get_option( 'woocommerce_notify_no_stock_amount' );
+		update_option( 'woocommerce_notify_no_stock_amount', -5 );
+		remove_filter( 'woocommerce_stock_amount', 'intval' );
+		add_filter( 'woocommerce_stock_amount', 'floatval' );
+
+		$below_magnitude = new WC_Product_Simple();
+		$below_magnitude->set_manage_stock( true );
+		$below_magnitude->set_stock_quantity( 3.5 );
+		$below_magnitude->set_stock_status( 'instock' );
+
+		$below_magnitude->validate_props();
+
+		$this->assertSame( 3.5, $below_magnitude->get_stock_quantity() );
+		$this->assertSame( 'outofstock', $below_magnitude->get_stock_status(), 'A stock quantity below the threshold magnitude (3.5 vs abs(-5)=5) should flip to out of stock.' );
+
+		$above_magnitude = new WC_Product_Simple();
+		$above_magnitude->set_manage_stock( true );
+		$above_magnitude->set_stock_quantity( 5.5 );
+		$above_magnitude->set_stock_status( 'instock' );
+
+		$above_magnitude->validate_props();
+
+		$this->assertSame( 5.5, $above_magnitude->get_stock_quantity() );
+		$this->assertSame( 'instock', $above_magnitude->get_stock_status(), 'A stock quantity above the threshold magnitude (5.5 vs abs(-5)=5) should stay in stock.' );
+
+		remove_filter( 'woocommerce_stock_amount', 'floatval' );
+		add_filter( 'woocommerce_stock_amount', 'intval' );
+		update_option( 'woocommerce_notify_no_stock_amount', $previous_threshold );
+	}
+
+	/**
+	 * @testdox validate_props() marks a product as out of stock when the stock quantity drops to zero.
+	 */
+	public function test_validate_props_marks_zero_stock_as_out_of_stock() {
+		$product = new WC_Product_Simple();
+		$product->set_manage_stock( true );
+		$product->set_stock_quantity( 0 );
+		$product->set_stock_status( 'instock' );
+
+		$product->validate_props();
+
+		$this->assertSame( 'outofstock', $product->get_stock_status() );
+	}
 }