Commit 21145103ad9 for woocommerce
commit 21145103ad99e021e31adbdbe75022953edd6744
Author: Lucio Giannotta <lucio.giannotta@a8c.com>
Date: Sat Mar 28 06:25:50 2026 +0800
Fix scheduled sale actions skipping lookup table refresh (#63856)
The scheduled sale handler `wc_apply_sale_state_for_product()` introduced
in #62115 sets the product price via `set_price()` and directly updates
`_price` meta after `save()`. Because `price` is not in the tracked props
list in `handle_updated_props()`, the `wc_product_meta_lookup` table is
never refreshed. This leaves `onsale`, `min_price`, and `max_price` stale
indefinitely — affecting On Sale collection queries and price filters.
Explicitly refresh the lookup table after the `_price` meta update, and
make `update_lookup_table()` public (consistent with its counterpart
`delete_from_lookup_table()` which is already public) so it can be called
through the `WC_Data_Store` wrapper.
diff --git a/plugins/woocommerce/changelog/woo6-52-codex-review-scheduled-sale-actions-skip-lookup-table b/plugins/woocommerce/changelog/woo6-52-codex-review-scheduled-sale-actions-skip-lookup-table
new file mode 100644
index 00000000000..d6489cc2104
--- /dev/null
+++ b/plugins/woocommerce/changelog/woo6-52-codex-review-scheduled-sale-actions-skip-lookup-table
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix scheduled sale actions leaving lookup table data stale
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php b/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php
index 657d7c2c1d6..91e2181bf19 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php
@@ -600,7 +600,7 @@ class WC_Data_Store_WP {
*
* @return NULL
*/
- protected function update_lookup_table( $id, $table ) {
+ public function update_lookup_table( $id, $table ) {
global $wpdb;
$id = absint( $id );
diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php
index d215f39957f..d626efd1f28 100644
--- a/plugins/woocommerce/includes/wc-product-functions.php
+++ b/plugins/woocommerce/includes/wc-product-functions.php
@@ -644,6 +644,11 @@ function wc_apply_sale_state_for_product( WC_Product $product, string $mode ): v
update_post_meta( $product_id, '_price', $regular_price );
}
+ // Refresh the lookup table since only the `price` prop changed, which is
+ // not in the tracked props list in handle_updated_props().
+ $data_store = WC_Data_Store::load( 'product' );
+ $data_store->update_lookup_table( $product_id, 'wc_product_meta_lookup' ); // @phpstan-ignore method.notFound (Called via __call() on the underlying WC_Data_Store_WP instance.)
+
wc_delete_product_transients( $product_id );
// Sync parent variable product price range if this is a variation.
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 299212412a4..b0f37f5de15 100644
--- a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
@@ -243,6 +243,66 @@ class WC_Product_Functions_Tests extends \WC_Unit_Test_Case {
$this->assertEquals( 100, wc_get_product( $product->get_id() )->get_price() );
}
+ /**
+ * @testdox Lookup table is refreshed when scheduled sale starts.
+ */
+ public function test_wc_scheduled_sales_sale_start_updates_lookup_table(): void {
+ global $wpdb;
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_price( 100 );
+ $product->set_regular_price( 100 );
+ $product->set_sale_price( 50 );
+ $product->set_date_on_sale_from( gmdate( 'Y-m-d H:i:s', time() + 10 ) );
+ $product->save();
+
+ // Bypass product after save hook to prevent price change on save.
+ update_post_meta( $product->get_id(), '_sale_price_dates_from', time() - 5 );
+
+ $lookup_before = $wpdb->get_row(
+ $wpdb->prepare( "SELECT onsale, min_price, max_price FROM {$wpdb->prefix}wc_product_meta_lookup WHERE product_id = %d", $product->get_id() )
+ );
+ $this->assertEquals( 0, (int) $lookup_before->onsale, 'Product should not be on sale before scheduled sale starts' );
+
+ wc_scheduled_sales();
+
+ $lookup_after = $wpdb->get_row(
+ $wpdb->prepare( "SELECT onsale, min_price, max_price FROM {$wpdb->prefix}wc_product_meta_lookup WHERE product_id = %d", $product->get_id() )
+ );
+ $this->assertEquals( 1, (int) $lookup_after->onsale, 'Lookup table onsale flag should be updated after sale starts' );
+ $this->assertEquals( 50, (float) $lookup_after->min_price, 'Lookup table min_price should reflect sale price' );
+ }
+
+ /**
+ * @testdox Lookup table is refreshed when scheduled sale ends.
+ */
+ public function test_wc_scheduled_sales_sale_end_updates_lookup_table(): void {
+ global $wpdb;
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_price( 50 );
+ $product->set_regular_price( 100 );
+ $product->set_sale_price( 50 );
+ $product->set_date_on_sale_to( gmdate( 'Y-m-d H:i:s', time() + 10 ) );
+ $product->save();
+
+ // Bypass product after save hook to prevent price change on save.
+ update_post_meta( $product->get_id(), '_sale_price_dates_to', time() - 5 );
+
+ $lookup_before = $wpdb->get_row(
+ $wpdb->prepare( "SELECT onsale, min_price, max_price FROM {$wpdb->prefix}wc_product_meta_lookup WHERE product_id = %d", $product->get_id() )
+ );
+ $this->assertEquals( 1, (int) $lookup_before->onsale, 'Product should be on sale before scheduled sale ends' );
+
+ wc_scheduled_sales();
+
+ $lookup_after = $wpdb->get_row(
+ $wpdb->prepare( "SELECT onsale, min_price, max_price FROM {$wpdb->prefix}wc_product_meta_lookup WHERE product_id = %d", $product->get_id() )
+ );
+ $this->assertEquals( 0, (int) $lookup_after->onsale, 'Lookup table onsale flag should be updated after sale ends' );
+ $this->assertEquals( 100, (float) $lookup_after->min_price, 'Lookup table min_price should reflect regular price' );
+ }
+
/**
* @testDox Action Scheduler events are scheduled when product with sale dates is saved.
*/