Commit 1a16e006d72 for woocommerce

commit 1a16e006d723033fb08bced211f92c0dbc62391c
Author: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>
Date:   Wed Oct 28 20:50:05 2026 +0200

    Fix scheduled-sales cron requeuing products with an empty sale price (#70296)

    * Fix scheduled-sales cron requeuing products with an empty sale price

    get_starting_sales() returned products whose sale price is '' or '0', which wc_apply_sale_state_for_product() skips, so nothing ever changed and they matched on every run. Exclude those values in the query. '0.00' is still applied and settles, so it stays matched.

    * Add changelog entry for scheduled-sales empty sale price fix

    * Consolidate scheduled-sales empty sale price tests

    The regular-price-lowered test hit the same stored row and query predicate as the '' case, and the '0.00' settle check covered behaviour this change doesn't touch. One data-driven test on get_starting_sales() keeps both the fix and the guard against a '> 0' rewrite.

diff --git a/plugins/woocommerce/changelog/fix-67995-scheduled-sales-empty-sale-price b/plugins/woocommerce/changelog/fix-67995-scheduled-sales-empty-sale-price
new file mode 100644
index 00000000000..ff01877e5ba
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-67995-scheduled-sales-empty-sale-price
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Stop the daily scheduled-sales check reprocessing products that have a sale start date but no sale price.
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
index 89a87788ab7..2b0ccceaa34 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -1465,6 +1465,10 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 	 * The exclusion must keep reading `_sale_price_dates_to` exactly the way
 	 * get_ending_sales() does, or a product ends up in neither queue. See the note there.
 	 *
+	 * Sale prices of '' and '0' are excluded because wc_apply_sale_state_for_product() skips
+	 * them, so the price never changes and the product would match on every run. '0.00' is
+	 * applied and settles, so it must stay matched: do not replace this with `> 0`.
+	 *
 	 * Keep the exclusion as NOT EXISTS: the LEFT JOIN ... IS NULL rewrite is faster on MariaDB
 	 * but materially slower on MySQL.
 	 *
@@ -1488,6 +1492,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 					AND postmeta.meta_value > 0
 					AND postmeta.meta_value < %s
 					AND postmeta_2.meta_value != postmeta_3.meta_value
+					AND postmeta_3.meta_value NOT IN ( '', '0' )
 					AND NOT EXISTS (
 						SELECT 1 FROM {$wpdb->postmeta} as ended
 						WHERE ended.post_id = postmeta.post_id
diff --git a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
index b148f1000ab..4b44cd1a539 100644
--- a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
@@ -552,6 +552,29 @@ class WC_Product_Functions_Tests extends \WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * @testdox get_starting_sales() returns a started sale only when the cron can apply its price: '$sale_price'.
+	 *
+	 * @testWith ["", false]
+	 *           ["0", false]
+	 *           ["0.00", true]
+	 *
+	 * @param string $sale_price Stored `_sale_price` value.
+	 * @param bool   $expected   Whether the product should be queued.
+	 */
+	public function test_get_starting_sales_only_returns_sale_prices_the_cron_applies( string $sale_price, bool $expected ): void {
+		$product = WC_Helper_Product::create_simple_product();
+		$product->set_regular_price( 100 );
+		$product->save();
+		update_post_meta( $product->get_id(), '_price', 100 );
+		update_post_meta( $product->get_id(), '_sale_price', $sale_price );
+		update_post_meta( $product->get_id(), '_sale_price_dates_from', time() - 300 );
+
+		$queued = in_array( (string) $product->get_id(), WC_Data_Store::load( 'product' )->get_starting_sales(), true );
+
+		$this->assertSame( $expected, $queued, 'The cron skips falsy sale prices, so queuing them repeats every run. \'0.00\' is applied and must stay queued.' );
+	}
+
 	/**
 	 * @testdox A product left at an expired sale price is repaired once and then goes inert.
 	 */