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