Commit 4cf343a143c for woocommerce

commit 4cf343a143cd8acc3733c378b8f5e84b1c369950
Author: Tung Du <dinhtungdu@gmail.com>
Date:   Mon Nov 2 18:32:56 2026 +0700

    Show the tax price suffix when all variations cost the same (#70286)

    * Document the woocommerce_show_variation_price filter

    Add the missing hook docblock. No behavior change.

    * Show the chosen variation's price when the suffix needs it

    A price suffix with a tag like {price_including_tax} only appears on the
    chosen variation's price, because variable products leave it off the main
    price. The classic add-to-cart form hid that price when all variations cost
    the same, so shoppers never saw the suffix. Show it in that case when taxes
    are on and the product is taxable.

    * Switch the Product Price block to the chosen variation for tax suffixes

    Apply the same rule to the Product Price block, which kept the main price
    when all variations cost the same and so never showed the suffix.

diff --git a/plugins/woocommerce/changelog/54016-variable-price-display-suffix b/plugins/woocommerce/changelog/54016-variable-price-display-suffix
new file mode 100644
index 00000000000..66febec4e8f
--- /dev/null
+++ b/plugins/woocommerce/changelog/54016-variable-price-display-suffix
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show the tax price display suffix after choosing a variation, even when all variations cost the same.
diff --git a/plugins/woocommerce/includes/class-wc-product-variable.php b/plugins/woocommerce/includes/class-wc-product-variable.php
index d2284a7390d..02a69b396ef 100644
--- a/plugins/woocommerce/includes/class-wc-product-variable.php
+++ b/plugins/woocommerce/includes/class-wc-product-variable.php
@@ -9,6 +9,7 @@
  */

 use Automattic\WooCommerce\Enums\ProductStockStatus;
+use Automattic\WooCommerce\Enums\ProductTaxStatus;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Internal\Utilities\ProductUtil;

@@ -473,8 +474,19 @@ class WC_Product_Variable extends WC_Product {
 			$variation_gallery_html = wc_get_product_gallery_html( $this, $gallery_html_ids );
 		}

-		// See if prices should be shown for each variation after selection.
-		$show_variation_price = apply_filters( 'woocommerce_show_variation_price', $variation->get_price() === '' || $this->get_variation_sale_price( 'min' ) !== $this->get_variation_sale_price( 'max' ) || $this->get_variation_regular_price( 'min' ) !== $this->get_variation_regular_price( 'max' ), $this, $variation );
+		// A suffix like {price_including_tax} only shows on the chosen variation's price, so show that price even when all prices match.
+		$has_variation_only_suffix = wc_tax_enabled() && ProductTaxStatus::TAXABLE === $variation->get_tax_status() && str_contains( (string) get_option( 'woocommerce_price_display_suffix' ), '{' );
+
+		/**
+		 * Filters whether the variation price is shown after the variation is selected.
+		 *
+		 * @since 2.4.0
+		 *
+		 * @param bool                 $show_variation_price Whether to show the variation price.
+		 * @param WC_Product_Variable  $product              The variable product.
+		 * @param WC_Product_Variation $variation            The variation.
+		 */
+		$show_variation_price = apply_filters( 'woocommerce_show_variation_price', $variation->get_price() === '' || $has_variation_only_suffix || $this->get_variation_sale_price( 'min' ) !== $this->get_variation_sale_price( 'max' ) || $this->get_variation_regular_price( 'min' ) !== $this->get_variation_regular_price( 'max' ), $this, $variation );

 		return apply_filters(
 			'woocommerce_available_variation',
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductPrice.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductPrice.php
index 81fefc091f5..48823a015af 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductPrice.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductPrice.php
@@ -2,6 +2,7 @@
 namespace Automattic\WooCommerce\Blocks\BlockTypes;

 use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
+use Automattic\WooCommerce\Enums\ProductTaxStatus;
 use Automattic\WooCommerce\Enums\ProductType;

 /**
@@ -77,8 +78,10 @@ class ProductPrice extends AbstractBlock {
 				/** @var \WC_Product_Variable $product */
 				$prices_vary = $product->get_variation_sale_price( 'min' ) !== $product->get_variation_sale_price( 'max' )
 					|| $product->get_variation_regular_price( 'min' ) !== $product->get_variation_regular_price( 'max' );
+				// A suffix like {price_including_tax} only shows on the chosen variation's price, so switch to it even when all prices match.
+				$has_variation_only_suffix = wc_tax_enabled() && ProductTaxStatus::TAXABLE === $product->get_tax_status() && str_contains( (string) get_option( 'woocommerce_price_display_suffix' ), '{' );

-				if ( ! $prices_vary ) {
+				if ( ! $prices_vary && ! $has_variation_only_suffix ) {
 					$is_interactive = false;
 				}
 			}
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 366a6098b5f..e46c4d3ecd1 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
@@ -407,6 +407,52 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case {
 		$this->assertSame( $filtered_image_id, $available_variation['image_id'] );
 	}

+	/**
+	 * @testdox When all variations cost the same, the selected variation's price shows only if taxes are on, the product is taxable, and the price suffix uses a tag like {price_including_tax}.
+	 * @testWith ["{price_including_tax} (inc vat)", "yes", "taxable", "$12.00 (inc vat)"]
+	 *           ["inc. VAT", "yes", "taxable", ""]
+	 *           ["{price_including_tax} (inc vat)", "no", "taxable", ""]
+	 *           ["{price_including_tax} (inc vat)", "yes", "none", ""]
+	 *
+	 * @param string $suffix          The price display suffix option.
+	 * @param string $calc_taxes      The calc taxes option.
+	 * @param string $tax_status      The variable product tax status.
+	 * @param string $expected_suffix Text expected in the variation price HTML, or empty when no price HTML is expected.
+	 */
+	public function test_same_price_variation_shows_price_only_for_tax_tag_suffix( string $suffix, string $calc_taxes, string $tax_status, string $expected_suffix ): void {
+		update_option( 'woocommerce_calc_taxes', $calc_taxes );
+		update_option( 'woocommerce_price_display_suffix', $suffix );
+		WC_Tax::_insert_tax_rate(
+			array(
+				'tax_rate_country'  => '',
+				'tax_rate'          => '20.0000',
+				'tax_rate_name'     => 'VAT',
+				'tax_rate_priority' => '1',
+				'tax_rate_order'    => '1',
+			)
+		);
+		$product = WC_Helper_Product::create_variation_product();
+		$product->set_tax_status( $tax_status );
+		$product->save();
+		foreach ( $product->get_children() as $child_id ) {
+			$child = wc_get_product( $child_id );
+			$child->set_regular_price( '10' );
+			$child->set_sale_price( '' );
+			$child->save();
+		}
+		WC_Product_Variable::sync( $product->get_id() );
+		$product   = wc_get_product( $product->get_id() );
+		$variation = wc_get_product( $product->get_children()[0] );
+
+		$price_html = html_entity_decode( wp_strip_all_tags( $product->get_available_variation( $variation )['price_html'] ) );
+
+		if ( '' === $expected_suffix ) {
+			$this->assertSame( '', $price_html );
+		} else {
+			$this->assertStringContainsString( $expected_suffix, $price_html );
+		}
+	}
+
 	/**
 	 * @testdox get_variation_prices sorts on first call, skips re-sorting on repeat calls, and treats float and string prices as equal via loose comparison.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductPriceTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductPriceTest.php
new file mode 100644
index 00000000000..62e25c14570
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductPriceTest.php
@@ -0,0 +1,46 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WC_Helper_Product;
+use WC_Product_Variable;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the ProductPrice block type.
+ */
+class ProductPriceTest extends WC_Unit_Test_Case {
+
+	/**
+	 * @testdox When all variations cost the same, the Product Price block switches to the selected variation's price only if taxes are on, the product is taxable, and the price suffix uses a tag like {price_including_tax}.
+	 * @testWith ["{price_including_tax} (inc vat)", "yes", "taxable", true]
+	 *           ["inc. VAT", "yes", "taxable", false]
+	 *           ["{price_including_tax} (inc vat)", "no", "taxable", false]
+	 *           ["{price_including_tax} (inc vat)", "yes", "none", false]
+	 *
+	 * @param string $suffix               The price display suffix option.
+	 * @param string $calc_taxes           The calc taxes option.
+	 * @param string $tax_status           The variable product tax status.
+	 * @param bool   $expected_interactive Whether the block switches to the selected variation's price.
+	 */
+	public function test_same_price_variations_switch_price_only_for_tax_tag_suffix( string $suffix, string $calc_taxes, string $tax_status, bool $expected_interactive ): void {
+		update_option( 'woocommerce_calc_taxes', $calc_taxes );
+		update_option( 'woocommerce_price_display_suffix', $suffix );
+		$product = WC_Helper_Product::create_variation_product();
+		$product->set_tax_status( $tax_status );
+		$product->save();
+		foreach ( $product->get_children() as $child_id ) {
+			$child = wc_get_product( $child_id );
+			$child->set_regular_price( '10' );
+			$child->set_sale_price( '' );
+			$child->save();
+		}
+		WC_Product_Variable::sync( $product->get_id() );
+
+		$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $product->get_id() . '} --><!-- wp:woocommerce/product-price /--><!-- /wp:woocommerce/single-product -->' );
+
+		$this->assertSame( $expected_interactive, str_contains( $markup, 'data-wp-watch="callbacks.updateValue"' ) );
+	}
+}