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