Commit fbf347ca795 for woocommerce

commit fbf347ca795714edbe4b3d62e63ad762aa8bdf6d
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Thu Oct 29 10:08:11 2026 +0100

    Improve Product Filters cache consistency (#70354)

    * Improve Product Filters cache consistency

    * Add changelog entry for Product Filters consistency

diff --git a/plugins/woocommerce/changelog/fix-product-filter-visibility b/plugins/woocommerce/changelog/fix-product-filter-visibility
new file mode 100644
index 00000000000..afd8e54d027
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-product-filter-visibility
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Improve the consistency of Product Filters prices and counts across user roles.
diff --git a/plugins/woocommerce/src/Internal/ProductFilters/FilterData.php b/plugins/woocommerce/src/Internal/ProductFilters/FilterData.php
index ab83992b1f8..3a51f1f9dbb 100644
--- a/plugins/woocommerce/src/Internal/ProductFilters/FilterData.php
+++ b/plugins/woocommerce/src/Internal/ProductFilters/FilterData.php
@@ -4,6 +4,7 @@ declare(strict_types=1);

 namespace Automattic\WooCommerce\Internal\ProductFilters;

+use Automattic\WooCommerce\Enums\ProductStatus;
 use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
 use Automattic\WooCommerce\Internal\ProductFilters\Interfaces\QueryClausesGenerator;
 use Automattic\WooCommerce\Internal\ProductFilters\TaxonomyHierarchyData;
@@ -58,6 +59,8 @@ class FilterData {
 	 * @return object
 	 */
 	public function get_filtered_price( array $query_vars ) {
+		$query_vars = $this->get_query_vars_with_default_post_status( $query_vars );
+
 		/**
 		 * Allows offloading the filter data to external services like Elasticsearch.
 		 *
@@ -133,6 +136,8 @@ class FilterData {
 	 * @return array status=>count pairs.
 	 */
 	public function get_stock_status_counts( array $query_vars, array $statuses ) {
+		$query_vars = $this->get_query_vars_with_default_post_status( $query_vars );
+
 		/**
 		 * Filter the data. @see get_filtered_price() for full documentation.
 		 */
@@ -198,6 +203,8 @@ class FilterData {
 	 * @return array rating=>count pairs.
 	 */
 	public function get_rating_counts( array $query_vars ) {
+		$query_vars = $this->get_query_vars_with_default_post_status( $query_vars );
+
 		/**
 		 * Filter the data. @see get_filtered_price() for full documentation.
 		 */
@@ -258,6 +265,8 @@ class FilterData {
 	 * @return array termId=>count pairs.
 	 */
 	public function get_attribute_counts( array $query_vars, string $attribute_to_count ) {
+		$query_vars = $this->get_query_vars_with_default_post_status( $query_vars );
+
 		/**
 		 * Filter the data. @see get_filtered_price() for full documentation.
 		 */
@@ -345,6 +354,8 @@ class FilterData {
 	 * @return array termId=>count pairs.
 	 */
 	public function get_taxonomy_counts( array $query_vars, string $taxonomy_to_count ) {
+		$query_vars = $this->get_query_vars_with_default_post_status( $query_vars );
+
 		/**
 		 * Filter the data. @see get_filtered_price() for full documentation.
 		 *
@@ -502,6 +513,24 @@ class FilterData {
 		return $final_counts;
 	}

+	/**
+	 * Make default visibility explicit before generating SQL or shared cache keys.
+	 *
+	 * @param array $query_vars The WP_Query arguments.
+	 * @return array Query arguments with an explicit post status.
+	 */
+	private function get_query_vars_with_default_post_status( array $query_vars ): array {
+		if ( empty( $query_vars['post_status'] ) ) {
+			$post_statuses = get_post_stati( array( 'public' => true ) );
+			if ( current_user_can( 'read_private_products' ) ) {
+				$post_statuses = array_merge( $post_statuses, get_post_stati( array( 'private' => true ) ) );
+			}
+			$query_vars['post_status'] = array_values( $post_statuses );
+		}
+
+		return $query_vars;
+	}
+
 	/**
 	 * Get filter data transient key.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php
index b5fd9fdd9f7..8170f597281 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php
@@ -2,6 +2,8 @@

 namespace Automattic\WooCommerce\Tests\Internal\ProductFilters;

+use Automattic\WooCommerce\Enums\ProductStatus;
+use Automattic\WooCommerce\Enums\ProductStockStatus;
 use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
 use Automattic\WooCommerce\Internal\ProductFilters\FilterDataProvider;
 use Automattic\WooCommerce\Internal\ProductFilters\QueryClauses;
@@ -91,6 +93,36 @@ class FilterDataTest extends AbstractProductFiltersTest {
 		$this->test_get_filtered_price_with( $wp_query );
 	}

+	/**
+	 * @testdox Product filter caches reflect the current user's product visibility.
+	 */
+	public function test_filter_cache_separates_staff_and_visitors(): void {
+		$private    = \WC_Helper_Product::create_simple_product(
+			true,
+			array(
+				'status'        => ProductStatus::PRIVATE,
+				'regular_price' => 4321,
+				'price'         => 4321,
+			)
+		);
+		$query_vars = array(
+			'post_type' => 'product',
+			'post__in'  => array( $this->products[1]->get_id(), $private->get_id() ),
+		);
+		$manager    = self::factory()->user->create( array( 'role' => 'shop_manager' ) );
+		wp_set_current_user( $manager );
+		$price = $this->sut->get_filtered_price( $query_vars );
+		$this->assertSame( 4321.0, (float) $price['max_price'] );
+
+		wp_set_current_user( 0 );
+		$price = $this->sut->get_filtered_price( $query_vars );
+		$this->assertSame( 20.0, (float) $price['max_price'] );
+
+		// Stock has no cached counts yet, so this also checks the shared product-ID cache.
+		$stock = $this->sut->get_stock_status_counts( $query_vars, array( ProductStockStatus::IN_STOCK ) );
+		$this->assertSame( 1, $stock[ ProductStockStatus::IN_STOCK ] );
+	}
+
 	/**
 	 * @testdox Changing the taxonomy URL map cannot reuse filter-data or product-ID cache entries.
 	 */