Commit 28b4574c71b for woocommerce

commit 28b4574c71b625d9defde5c97fb8fb9275361a07
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Fri Sep 25 17:42:59 2026 +0200

    [Performance] Eliminate duplicate variation price aggregation on tax-enabled stores in read_price_data method of variable data store (#68938)

    The problem we are solving is specific to tax-enabled stores (e. g. EU): \WC_Product_Variable_Data_Store_CPT::read_price_data has an issue with running the price aggregation twice in certain cases. Product updates (incl. those triggered by checkout), invalidate caches and transients, leading to the method invocation. This scenario is amplified for HVMs and during peak sale events, causing longer execution times and longer PHP workers holding, with more chaos following.

diff --git a/plugins/woocommerce/changelog/performance-66892-read_price_data-aggreagation-perfromance b/plugins/woocommerce/changelog/performance-66892-read_price_data-aggreagation-perfromance
new file mode 100644
index 00000000000..3a8cb7f358b
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-66892-read_price_data-aggreagation-perfromance
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Eliminated duplicate variation price aggregation on tax-enabled stores in read_price_data method of variable data store.
diff --git a/plugins/woocommerce/includes/class-wc-product-variable.php b/plugins/woocommerce/includes/class-wc-product-variable.php
index a812e1c62e4..20a09d55b23 100644
--- a/plugins/woocommerce/includes/class-wc-product-variable.php
+++ b/plugins/woocommerce/includes/class-wc-product-variable.php
@@ -194,7 +194,7 @@ class WC_Product_Variable extends WC_Product {

 			if ( $min_price !== $max_price ) {
 				$price = wc_format_price_range( $min_price, $max_price );
-			} elseif ( $this->is_on_sale() && $min_reg_price === $max_reg_price ) {
+			} elseif ( $min_reg_price === $max_reg_price && $this->is_on_sale() ) {
 				$price = wc_format_sale_price( wc_price( $max_reg_price ), wc_price( $min_price ) );
 			} else {
 				$price = wc_price( $min_price );
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php
index 12269330fe3..cc5f9c957da 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php
@@ -30,6 +30,13 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 	 */
 	protected $prices_array = array();

+	/**
+	 * Cached prices array hash pairs for child variations.
+	 *
+	 * @var array<string,string>
+	 */
+	private $prices_hashes = array();
+
 	/**
 	 * Read attributes from post meta.
 	 *
@@ -325,13 +332,33 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 	 * Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter.
 	 * This is to ensure modified prices are not cached, unless intended.
 	 *
-	 * @param WC_Product $product Product object.
+	 * @param WC_Product $product     Product object.
 	 * @param bool       $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes).
 	 *
 	 * @return array of prices
 	 * @since  3.0.0
 	 */
 	public function read_price_data( &$product, $for_display = false ) {
+		$hashes = $this->prime_price_data_cache( $product );
+		$hash   = $for_display ? $hashes->hash_display : $hashes->hash_raw;
+
+		return array_key_exists( $hash, $this->prices_array ) ? $this->prices_array[ $hash ] : array();
+	}
+
+	/**
+	 * Primes the price data cache (request-level and when necessary transient).
+	 *
+	 * @param WC_Product $product Product object.
+	 * @return object{ hash_display:string, hash_raw:string }
+	 */
+	private function prime_price_data_cache( &$product ): object {
+		// Performance note: cache hash pairs so $hash_raw calculation costs nothing for repetitive calls.
+		$hash_display = $this->get_price_hash( $product, true );
+		if ( ! array_key_exists( $hash_display, $this->prices_hashes ) ) {
+			$this->prices_hashes[ $hash_display ] = $this->get_price_hash( $product, false );
+		}
+		$hash_raw = $this->prices_hashes[ $hash_display ];
+
 		/**
 		 * If you are here to investigate performance of this method: yes, it is heavy in RAM and CPU usage overall.
 		 *
@@ -348,16 +375,14 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 		 * That leaves single optimization route:
 		 * - rewrite this method, if breaking the contracts is an option (it's not as per WooCommerce v11.1)
 		 */
-		$price_hash = $this->get_price_hash( $product, $for_display );
-		if ( empty( $this->prices_array[ $price_hash ] ) ) {
+		if ( empty( $this->prices_array[ $hash_display ] ) || empty( $this->prices_array[ $hash_raw ] ) ) {
 			/**
 			 * Transient name for storing prices for this product (note: Max transient length is 45)
 			 *
 			 * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product.
 			 */
-			$transient_name      = 'wc_var_prices_' . $product->get_id();
-			$transient_version   = WC_Cache_Helper::get_transient_version( 'product' );
-			$opposite_price_hash = $this->taxes_influence_price( $product ) ? null : $this->get_price_hash( $product, ! $for_display );
+			$transient_name    = 'wc_var_prices_' . $product->get_id();
+			$transient_version = WC_Cache_Helper::get_transient_version( 'product' );

 			// If the prices are not valid, reset the transient cache.
 			$transient_cached_prices_array = array_filter( (array) json_decode( (string) get_transient( $transient_name ), true ) );
@@ -367,12 +392,14 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple

 			// If the prices are not stored for this hash, generate them and add to the transient.
 			// Check also the opposite price hash as it may have changed (see get_price_hash).
-			if ( empty( $transient_cached_prices_array[ $price_hash ] ) || ( null !== $opposite_price_hash && empty( $transient_cached_prices_array[ $opposite_price_hash ] ) ) ) {
-				$prices_array = array(
+			if ( empty( $transient_cached_prices_array[ $hash_display ] ) || empty( $transient_cached_prices_array[ $hash_raw ] ) ) {
+				$empty_prices         = array(
 					'price'         => array(),
 					'regular_price' => array(),
 					'sale_price'    => array(),
 				);
+				$display_prices_array = $empty_prices;
+				$raw_prices_array     = $empty_prices;

 				$variation_ids = $product->get_visible_children();

@@ -381,7 +408,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 					_prime_post_caches( $variation_ids );
 				}

-				$tax_display_mode = $for_display ? get_option( 'woocommerce_tax_display_shop' ) : null;
+				$tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
 				$price_decimals   = wc_get_price_decimals();
 				foreach ( $variation_ids as $variation_id ) {
 					$variation = wc_get_product( $variation_id );
@@ -457,61 +484,29 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 							$sale_price = $regular_price;
 						}

-						// If we are getting prices for display, we need to account for taxes.
-						if ( $for_display ) {
-							if ( TaxDisplayMode::INCLUSIVE === $tax_display_mode ) {
-								$price         = '' === $price ? '' : wc_get_price_including_tax(
-									$variation,
-									array(
-										'qty'   => 1,
-										'price' => $price,
-									)
-								);
-								$regular_price = '' === $regular_price ? '' : wc_get_price_including_tax(
-									$variation,
-									array(
-										'qty'   => 1,
-										'price' => $regular_price,
-									)
-								);
-								$sale_price    = '' === $sale_price ? '' : wc_get_price_including_tax(
-									$variation,
-									array(
-										'qty'   => 1,
-										'price' => $sale_price,
-									)
-								);
-							} else {
-								$price         = '' === $price ? '' : wc_get_price_excluding_tax(
-									$variation,
-									array(
-										'qty'   => 1,
-										'price' => $price,
-									)
-								);
-								$regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax(
-									$variation,
-									array(
-										'qty'   => 1,
-										'price' => $regular_price,
-									)
-								);
-								$sale_price    = '' === $sale_price ? '' : wc_get_price_excluding_tax(
-									$variation,
-									array(
-										'qty'   => 1,
-										'price' => $sale_price,
-									)
-								);
-							}
+						// Capture raw prices before tax adjustment.
+						$raw_prices_array['price'][ $variation_id ]         = wc_format_decimal( $price, $price_decimals );
+						$raw_prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, $price_decimals );
+						$raw_prices_array['sale_price'][ $variation_id ]    = wc_format_decimal( $sale_price, $price_decimals );
+
+						// Tax-adjust for display prices.
+						if ( TaxDisplayMode::INCLUSIVE === $tax_display_mode ) {
+							$price         = '' === $price ? '' : wc_get_price_including_tax( $variation, array( 'price' => $price ) );
+							$regular_price = '' === $regular_price ? '' : wc_get_price_including_tax( $variation, array( 'price' => $regular_price ) );
+							$sale_price    = '' === $sale_price ? '' : wc_get_price_including_tax( $variation, array( 'price' => $sale_price ) );
+						} else {
+							$price         = '' === $price ? '' : wc_get_price_excluding_tax( $variation, array( 'price' => $price ) );
+							$regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax( $variation, array( 'price' => $regular_price ) );
+							$sale_price    = '' === $sale_price ? '' : wc_get_price_excluding_tax( $variation, array( 'price' => $sale_price ) );
 						}

-						$prices_array['price'][ $variation_id ]         = wc_format_decimal( $price, $price_decimals );
-						$prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, $price_decimals );
-						$prices_array['sale_price'][ $variation_id ]    = wc_format_decimal( $sale_price, $price_decimals );
+						$display_prices_array['price'][ $variation_id ]         = wc_format_decimal( $price, $price_decimals );
+						$display_prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, $price_decimals );
+						$display_prices_array['sale_price'][ $variation_id ]    = wc_format_decimal( $sale_price, $price_decimals );

 						if ( has_filter( 'woocommerce_variation_prices_array' ) ) {
-							$original_prices_array = $prices_array;
+							$original_display_prices_array = $display_prices_array;
+							$original_raw_prices_array     = $raw_prices_array;

 							/**
 							 * Filter the variation prices array before storing in transient cache.
@@ -526,23 +521,16 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 							 * @param WC_Product   $variation    The variation product object.
 							 * @param bool         $for_display  Whether prices are for display (with tax adjustments) or for calculations.
 							 */
-							$prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display );
-							if ( null !== $opposite_price_hash ) {
-								// $for_display doesn't affect raw prices here, but a woocommerce_variation_prices_array hook might.
-								// phpcs:ignore WooCommerce.Commenting.CommentHooks
-								$opposite_prices_array = apply_filters( 'woocommerce_variation_prices_array', $original_prices_array, $variation, ! $for_display );
-								if ( $opposite_prices_array !== $prices_array ) {
-									$opposite_price_hash = null;
-								}
-							}
+							$display_prices_array = apply_filters( 'woocommerce_variation_prices_array', $original_display_prices_array, $variation, true );
+							$raw_prices_array     = apply_filters( 'woocommerce_variation_prices_array', $original_raw_prices_array, $variation, false );
 						}
 					}
 				}

 				// Add all pricing data to the transient array: ensure the hashes always pushed to the end.
-				foreach ( array_filter( array( $price_hash, $opposite_price_hash ) ) as $hash ) {
+				foreach ( array( $hash_display, $hash_raw ) as $hash ) {
 					unset( $transient_cached_prices_array[ $hash ] );
-					foreach ( $prices_array as $key => $values ) {
+					foreach ( ( $hash === $hash_display ? $display_prices_array : $raw_prices_array ) as $key => $values ) {
 						$transient_cached_prices_array[ $hash ][ $key ] = $values;
 					}
 				}
@@ -586,8 +574,8 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 			 * @param WC_Product $product      The variable product object.
 			 * @param bool       $for_display  Whether prices are being retrieved for display.
 			 */
-			$this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display );
-			if ( $this->prices_array[ $price_hash ] !== $transient_cached_prices_array[ $price_hash ] && ! $this->validate_prices_data( array( $price_hash => $this->prices_array[ $price_hash ] ), $transient_version ) ) {
+			$this->prices_array[ $hash_display ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $hash_display ], $product, true );
+			if ( $this->prices_array[ $hash_display ] !== $transient_cached_prices_array[ $hash_display ] && ! $this->validate_prices_data( array( $hash_display => $this->prices_array[ $hash_display ] ), $transient_version ) ) {
 				wc_doing_it_wrong(
 					__METHOD__,
 					__( '`woocommerce_variation_prices` returned an unsupported data format. The value is currently used as-is but will be ignored in a future release. Ensure your callback returns the expected array structure.', 'woocommerce' ),
@@ -595,18 +583,20 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 				);
 			}

-			if ( null !== $opposite_price_hash && $opposite_price_hash !== $price_hash ) {
-				$this->prices_array[ $opposite_price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $opposite_price_hash ], $product, ! $for_display );
-				if ( $this->prices_array[ $opposite_price_hash ] !== $transient_cached_prices_array[ $opposite_price_hash ] && ! $this->validate_prices_data( array( $opposite_price_hash => $this->prices_array[ $opposite_price_hash ] ), $transient_version ) ) {
-					wc_doing_it_wrong(
-						__METHOD__,
-						__( '`woocommerce_variation_prices` returned an unsupported data format. The value is currently used as-is but will be ignored in a future release. Ensure your callback returns the expected array structure.', 'woocommerce' ),
-						'11.1'
-					);
-				}
+			$this->prices_array[ $hash_raw ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $hash_raw ], $product, false );
+			if ( $this->prices_array[ $hash_raw ] !== $transient_cached_prices_array[ $hash_raw ] && ! $this->validate_prices_data( array( $hash_raw => $this->prices_array[ $hash_raw ] ), $transient_version ) ) {
+				wc_doing_it_wrong(
+					__METHOD__,
+					__( '`woocommerce_variation_prices` returned an unsupported data format. The value is currently used as-is but will be ignored in a future release. Ensure your callback returns the expected array structure.', 'woocommerce' ),
+					'11.1'
+				);
 			}
 		}
-		return $this->prices_array[ $price_hash ];
+
+		return (object) array(
+			'hash_display' => $hash_display,
+			'hash_raw'     => $hash_raw,
+		);
 	}

 	/**
@@ -616,8 +606,11 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 	 * @return bool True if the prices will be different with or without taxes.
 	 *
 	 * @since 10.4.0
+	 * @deprecated since 11.3.0 after read_price_data method optimization renders this method obsolete
 	 */
 	protected function taxes_influence_price( $product ): bool {
+		wc_deprecated_function( __METHOD__, '11.3.0' );
+
 		if ( ! $product->is_taxable() ) {
 			$taxes_influence_price = false;
 		} elseif ( empty( WC_Tax::get_rates( $product->get_tax_class() ) ) ) {
@@ -625,7 +618,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 		} else {
 			// Taxes influence the price regardless of VAT exempt status. Even when a
 			// customer is VAT exempt, the displayed prices differ from non-exempt
-			// prices, so they need separate cache entries and the opposite_price_hash
+			// prices, so they need separate cache entries and the hash_raw
 			// optimization should not apply. Returning false here was causing cached
 			// non-exempt prices to be served to VAT exempt customers.
 			$taxes_influence_price = true;
@@ -644,8 +637,9 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 		 * @param WC_Product $product               The variable product being evaluated.
 		 *
 		 * @since 10.9.0
+		 * @deprecated since 11.3.0 after read_price_data method optimization renders this filter obsolete
 		 */
-		return (bool) apply_filters( 'woocommerce_variable_product_taxes_influence_price', $taxes_influence_price, $product );
+		return (bool) apply_filters_deprecated( 'woocommerce_variable_product_taxes_influence_price', array( $taxes_influence_price, $product ), '11.3.0' );
 	}

 	/**
@@ -731,10 +725,12 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 		 * @param array      $price_hash Array of factors used to generate the cache key hash.
 		 * @param WC_Product $product     The variable product object.
 		 * @param bool       $for_display Whether prices are for display (with tax adjustments) or calculations.
+		 * @return array
 		 */
 		$price_hash = apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $for_display );

-		return md5( wp_json_encode( $price_hash ) );
+		// Pushing $for_display into the hash array is means of scoping, introduced in order to reduce hash collisions risk.
+		return md5( wp_json_encode( array( $price_hash, (bool) $for_display ) ) );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php b/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
index 5ed3c0c4704..366a6098b5f 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
@@ -484,7 +484,7 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case {
 		$bad_filter = static fn() => $malformed_value;
 		add_filter( 'woocommerce_variation_prices', $bad_filter );

-		$this->setExpectedIncorrectUsage( 'WC_Product_Variable_Data_Store_CPT::read_price_data' );
+		$this->setExpectedIncorrectUsage( 'WC_Product_Variable_Data_Store_CPT::prime_price_data_cache' );

 		try {
 			$prices = $product->get_variation_prices();
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php
index 69e1f94b740..c9e45bb30e6 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php
@@ -476,14 +476,19 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {

 		$this->assertEquals( $prices_no_tax, array_values( $variation_prices['price'] ) );

+		// Verify that the cached raw prices are not tax-adjusted while the customer is VAT-exempt.
+		// At this point display prices have tax removed (9.09, ...), so raw prices diverge and the assertion is meaningful.
+		$prices_with_tax = array( '10.00', '15.00', '16.00', '17.00', '18.00', '19.00' );
+		$raw_prices      = $product->get_variation_prices( false );
+
+		$this->assertSame( $prices_with_tax, array_values( $raw_prices['price'] ), 'Cached raw prices must not be tax-adjusted.' );
+
 		// Verify that a normal customer gets prices with tax included.
 		// This indirectly proves that the customer's VAT exemption influences the cache key.
 		WC()->customer->set_is_vat_exempt( false );
-
-		$prices_with_tax  = array( '10.00', '15.00', '16.00', '17.00', '18.00', '19.00' );
 		$variation_prices = $product->get_variation_prices( true );

-		$this->assertEquals( $prices_with_tax, array_values( $variation_prices['price'] ) );
+		$this->assertSame( $prices_with_tax, array_values( $variation_prices['price'] ) );

 		// Clean up.
 		WC_Tax::_delete_tax_rate( $tax_id );
@@ -623,6 +628,8 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 	 * @testdox taxes_influence_price returns true even when customer is VAT exempt, because exempt prices differ from non-exempt.
 	 */
 	public function test_taxes_influence_price_returns_true_for_vat_exempt() {
+		$this->setExpectedDeprecated( 'WC_Product_Variable_Data_Store_CPT::taxes_influence_price' );
+
 		add_filter( 'wc_tax_enabled', '__return_true' );
 		add_filter( 'woocommerce_product_is_taxable', '__return_true' );
 		add_filter( 'woocommerce_matched_rates', array( $this, '__return_rates' ) );
@@ -665,6 +672,9 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 	 * @param bool $expected         Expected return value from taxes_influence_price.
 	 */
 	public function test_taxes_influence_price_filter_overrides_default( bool $taxable_product, bool $tax_has_rates, bool $expected_default, bool $filter_returns, bool $expected ) {
+		$this->setExpectedDeprecated( 'WC_Product_Variable_Data_Store_CPT::taxes_influence_price' );
+		$this->setExpectedDeprecated( 'woocommerce_variable_product_taxes_influence_price' );
+
 		add_filter( 'wc_tax_enabled', '__return_true' );
 		add_filter( 'woocommerce_product_is_taxable', $taxable_product ? '__return_true' : '__return_false' );
 		add_filter( 'woocommerce_matched_rates', $tax_has_rates ? array( $this, '__return_rates' ) : '__return_empty_array' );
@@ -708,15 +718,16 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		$data_store     = new WC_Product_Variable_Data_Store_CPT();
 		$product        = $this->get_variation_product_fixture();
 		$transient_name = 'wc_var_prices_' . $product->get_id();
-		delete_transient( $transient_name );

 		$extended_data_store = $this->get_data_store_with_public_get_price_hash();

 		delete_transient( $transient_name );
 		$data_store->read_price_data( $product, $for_display );
-		$expected_hashes = array( $extended_data_store->get_price_hash( $product, $for_display ) );
+
+		// Both display and raw hashes are always cached — prime_price_data_cache computes both in a single pass.
+		$expected_hashes = array( $extended_data_store->get_price_hash( $product, true ), $extended_data_store->get_price_hash( $product, false ) );
 		$actual_hashes   = array_unique( $this->get_keys_for_json_encoded_transient( $transient_name ) );
-		$this->assertEquals( $expected_hashes, $actual_hashes );
+		$this->assertSame( $expected_hashes, $actual_hashes );

 		remove_filter( 'wc_tax_enabled', '__return_true' );
 		remove_filter( 'woocommerce_product_is_taxable', '__return_true' );
@@ -753,17 +764,15 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		$data_store     = new WC_Product_Variable_Data_Store_CPT();
 		$product        = $this->get_variation_product_fixture();
 		$transient_name = 'wc_var_prices_' . $product->get_id();
-		delete_transient( $transient_name );

 		$extended_data_store = $this->get_data_store_with_public_get_price_hash();

+		delete_transient( $transient_name );
 		$data_store->read_price_data( $product, true );
+
+		$expected_hashes = array( $extended_data_store->get_price_hash( $product, true ), $extended_data_store->get_price_hash( $product, false ) );
 		$actual_hashes   = array_unique( $this->get_keys_for_json_encoded_transient( $transient_name ) );
-		$expected_hashes =
-			$hook_modifies_prices ?
-			array( $extended_data_store->get_price_hash( $product, true ) ) :
-			array( $extended_data_store->get_price_hash( $product, true ), $extended_data_store->get_price_hash( $product, false ) );
-		$this->assertEquals( $expected_hashes, $actual_hashes );
+		$this->assertSame( $expected_hashes, $actual_hashes );

 		remove_all_filters( 'woocommerce_variation_prices_array' );
 		remove_filter( 'wc_tax_enabled', '__return_true' );
@@ -2040,4 +2049,70 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {

 		$product->delete();
 	}
+
+	/**
+	 * @testdox read_price_data does not trigger a second aggregation pass for the raw variant after the display variant is primed.
+	 */
+	public function test_read_price_data_no_double_aggregation(): void {
+		add_filter( 'wc_tax_enabled', '__return_true' );
+		add_filter( 'woocommerce_product_is_taxable', '__return_true' );
+		add_filter( 'woocommerce_matched_rates', array( $this, '__return_rates' ) );
+
+		$product        = $this->get_variation_product_fixture();
+		$data_store     = new WC_Product_Variable_Data_Store_CPT();
+		$transient_name = 'wc_var_prices_' . $product->get_id();
+
+		delete_transient( $transient_name );
+
+		$aggregation_count = 0;
+		$count_aggregation = function ( $price ) use ( &$aggregation_count ) {
+			++$aggregation_count;
+			return $price;
+		};
+		add_filter( 'woocommerce_variation_prices_price', $count_aggregation );
+
+		// First call primes both display and raw hashes in a single pass.
+		$data_store->read_price_data( $product, true );
+		$hashes_after_first_call = $this->get_keys_for_json_encoded_transient( $transient_name );
+		$aggregation_after_first = $aggregation_count;
+
+		$this->assertSame( count( $product->get_visible_children() ), $aggregation_after_first, 'First call must aggregate exactly once (one filter invocation per variation).' );
+
+		// Second call must reuse the cache — no re-aggregation.
+		$data_store->read_price_data( $product, false );
+		$hashes_after_second_call = $this->get_keys_for_json_encoded_transient( $transient_name );
+
+		$this->assertSame( $hashes_after_first_call, $hashes_after_second_call, 'Hashes must not change after the second call.' );
+		$this->assertSame( $aggregation_after_first, $aggregation_count, 'Second call must not trigger re-aggregation.' );
+
+		remove_filter( 'woocommerce_variation_prices_price', $count_aggregation );
+
+		remove_filter( 'wc_tax_enabled', '__return_true' );
+		remove_filter( 'woocommerce_product_is_taxable', '__return_true' );
+		remove_filter( 'woocommerce_matched_rates', array( $this, '__return_rates' ) );
+	}
+
+	/**
+	 * @testdox read_price_data excludes variations with empty prices from the result.
+	 */
+	public function test_read_price_data_excludes_empty_price_variations(): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_children();
+
+		// Clear the price on the first variation.
+		$empty_variation = wc_get_product( $child_ids[0] );
+		$empty_variation->set_price( '' );
+		$empty_variation->set_regular_price( '' );
+		$empty_variation->save();
+
+		delete_transient( 'wc_var_prices_' . $product->get_id() );
+		$prices = ( new WC_Product_Variable_Data_Store_CPT() )->read_price_data( $product, false );
+
+		$this->assertCount( count( $child_ids ) - 1, $prices['price'], 'Only priced variations must be in the result.' );
+		$this->assertArrayNotHasKey( $child_ids[0], $prices['price'], 'Empty-price variation must be excluded from price array.' );
+		$this->assertArrayNotHasKey( $child_ids[0], $prices['regular_price'], 'Empty-price variation must be excluded from regular_price array.' );
+		$this->assertArrayNotHasKey( $child_ids[0], $prices['sale_price'], 'Empty-price variation must be excluded from sale_price array.' );
+
+		$product->delete( true );
+	}
 }