Commit c1b6b7b7f0 for woocommerce

commit c1b6b7b7f0715bca8e54616601758f009e0e5b47
Author: Neil Carlo Sucuangco <necafasu@gmail.com>
Date:   Sat Feb 14 00:37:50 2026 +0800

    Add filter for tax-inclusive shipping prices (#62944)

    * Add filter for tax-inclusive shipping prices

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Refactor single filter call, shared cost-adjust block

    * Normalize shipping tax filter to boolean and guard cart item tax class

    * adjustments

    * Update abstract-wc-shipping-method.php

    * Lint and PHPStan fixes

    * Update class-wc-tax-test.php

    * Update class-wc-tax-test.php

    * Update plugins/woocommerce/tests/php/includes/class-wc-tax-test.php

    Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

    * Update class-wc-tax-test.php

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
    Co-authored-by: Taha Paksu <3295+tpaksu@users.noreply.github.com>
    Co-authored-by: Sam Najian <dev@najian.info>
    Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/62944-fix-62927-tax-inclusive-shipping b/plugins/woocommerce/changelog/62944-fix-62927-tax-inclusive-shipping
new file mode 100644
index 0000000000..3578b0e671
--- /dev/null
+++ b/plugins/woocommerce/changelog/62944-fix-62927-tax-inclusive-shipping
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add `woocommerce_shipping_prices_include_tax` filter to support tax-inclusive shipping prices
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-shipping-method.php b/plugins/woocommerce/includes/abstracts/abstract-wc-shipping-method.php
index da63bc4602..6b621c21ce 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-shipping-method.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-shipping-method.php
@@ -318,7 +318,25 @@ abstract class WC_Shipping_Method extends WC_Settings_API {

 		// Taxes - if not an array and not set to false, calc tax based on cost and passed calc_tax variable. This saves shipping methods having to do complex tax calculations.
 		if ( ! is_array( $taxes ) && false !== $taxes && $total_cost > 0 && $this->is_taxable() ) {
-			$taxes = 'per_item' === $args['calc_tax'] ? $this->get_taxes_per_item( $args['cost'] ) : WC_Tax::calc_shipping_tax( $total_cost, WC_Tax::get_shipping_tax_rates() );
+			if ( 'per_item' === $args['calc_tax'] ) {
+				$taxes = $this->get_taxes_per_item( $args['cost'] );
+			} else {
+				$shipping_tax_rates = WC_Tax::get_shipping_tax_rates();
+				$taxes              = WC_Tax::calc_shipping_tax( $total_cost, $shipping_tax_rates );
+			}
+
+			/**
+			 * Filter whether shipping prices include tax.
+			 *
+			 * @since 10.6.0
+			 * @param bool $shipping_prices_include_tax Whether shipping cost includes tax. Default false.
+			 */
+			$shipping_prices_include_tax = wc_string_to_bool( apply_filters( 'woocommerce_shipping_prices_include_tax', false ) );
+
+			// If prices include tax, convert gross to net.
+			if ( $shipping_prices_include_tax && ! empty( $taxes ) ) {
+				$total_cost = $total_cost - array_sum( $taxes );
+			}
 		}

 		// Round the total cost after taxes have been calculated.
@@ -378,7 +396,14 @@ abstract class WC_Shipping_Method extends WC_Settings_API {
 					continue;
 				}

-				$item_taxes = WC_Tax::calc_shipping_tax( $amount, WC_Tax::get_shipping_tax_rates( $cart[ $cost_key ]['data']->get_tax_class() ) );
+				$cart_item_data = $cart[ $cost_key ]['data'];
+				if ( is_object( $cart_item_data ) && is_callable( array( $cart_item_data, 'get_tax_class' ) ) ) {
+					$tax_class = $cart_item_data->get_tax_class();
+				} else {
+					$tax_class = null;
+				}
+				$item_tax_rates = WC_Tax::get_shipping_tax_rates( $tax_class );
+				$item_taxes     = WC_Tax::calc_shipping_tax( $amount, $item_tax_rates );

 				// Sum the item taxes.
 				foreach ( array_keys( $taxes + $item_taxes ) as $key ) {
@@ -388,7 +413,8 @@ abstract class WC_Shipping_Method extends WC_Settings_API {

 			// Add any cost for the order - order costs are in the key 'order'.
 			if ( isset( $costs['order'] ) ) {
-				$item_taxes = WC_Tax::calc_shipping_tax( $costs['order'], WC_Tax::get_shipping_tax_rates() );
+				$order_tax_rates = WC_Tax::get_shipping_tax_rates();
+				$item_taxes      = WC_Tax::calc_shipping_tax( $costs['order'], $order_tax_rates );

 				// Sum the item taxes.
 				foreach ( array_keys( $taxes + $item_taxes ) as $key ) {
diff --git a/plugins/woocommerce/includes/class-wc-tax.php b/plugins/woocommerce/includes/class-wc-tax.php
index 3ce1732998..cea9e82cdc 100644
--- a/plugins/woocommerce/includes/class-wc-tax.php
+++ b/plugins/woocommerce/includes/class-wc-tax.php
@@ -82,7 +82,20 @@ class WC_Tax {
 	 * @return array
 	 */
 	public static function calc_shipping_tax( $price, $rates ) {
-		$taxes = self::calc_exclusive_tax( $price, $rates );
+		/**
+		 * Filter to control if shipping prices include tax.
+		 *
+		 * @since 10.6.0
+		 * @param bool $shipping_prices_include_tax True if shipping cost is gross (includes tax), false if net. Default false.
+		 */
+		$shipping_prices_include_tax = wc_string_to_bool( apply_filters( 'woocommerce_shipping_prices_include_tax', false ) );
+
+		if ( $shipping_prices_include_tax ) {
+			$taxes = self::calc_inclusive_tax( $price, $rates );
+		} else {
+			$taxes = self::calc_exclusive_tax( $price, $rates );
+		}
+
 		return apply_filters( 'woocommerce_calc_shipping_tax', $taxes, $price, $rates );
 	}

diff --git a/plugins/woocommerce/tests/legacy/unit-tests/tax/tax.php b/plugins/woocommerce/tests/legacy/unit-tests/tax/tax.php
index 6c1cbce5f7..8c1fde458f 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/tax/tax.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/tax/tax.php
@@ -331,7 +331,7 @@ class WC_Tests_Tax extends WC_Unit_Test_Case {
 	}

 	/**
-	 * Shipping tax amounts.
+	 * Shipping tax amounts (default behavior - exclusive).
 	 */
 	public function test_calc_shipping_tax() {
 		$tax_rate = array(
@@ -358,11 +358,51 @@ class WC_Tests_Tax extends WC_Unit_Test_Case {
 			)
 		);

+		// Default behavior: shipping cost is net, tax is added on top.
+		// 10.00 * 20% = 2.00.
 		$calced_tax = WC_Tax::calc_shipping_tax( '10', $tax_rates );

 		$this->assertEquals( $calced_tax, array( $tax_rate_id => '2' ) );
 	}

+	/**
+	 * Shipping tax amounts (inclusive behavior when filter enabled).
+	 */
+	public function test_calc_shipping_tax_inclusive() {
+		$tax_rate = array(
+			'tax_rate_country'  => 'GB',
+			'tax_rate_state'    => '',
+			'tax_rate'          => '20.0000',
+			'tax_rate_name'     => 'VAT',
+			'tax_rate_priority' => '1',
+			'tax_rate_compound' => '0',
+			'tax_rate_shipping' => '1',
+			'tax_rate_order'    => '1',
+			'tax_rate_class'    => '',
+		);
+
+		$tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
+
+		$tax_rates = WC_Tax::find_rates(
+			array(
+				'country'   => 'GB',
+				'state'     => 'Cambs',
+				'postcode'  => 'PE14 1XX',
+				'city'      => 'Somewhere',
+				'tax_class' => '',
+			)
+		);
+
+		// With filter: shipping cost is gross, tax is calculated from inclusive price.
+		// 10.00 gross, tax = 10.00 - (10.00 / 1.20) ≈ 1.67.
+		add_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+		$calced_tax = WC_Tax::calc_shipping_tax( '10', $tax_rates );
+		remove_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+
+		$expected_tax = 10.00 - ( 10.00 / 1.20 );
+		$this->assertEqualsWithDelta( $expected_tax, $calced_tax[ $tax_rate_id ], 0.01, 'Inclusive: tax should be calculated from gross price' );
+	}
+
 	/**
 	 * Test rate labels.
 	 */
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-tax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-tax-test.php
index 79337319cf..efd7f3a220 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-tax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-tax-test.php
@@ -71,6 +71,7 @@ class WC_Tax_Test extends WC_Unit_Test_Case {
 		WC()->cart->empty_cart();

 		remove_all_filters( 'woocommerce_shipping_tax_class' );
+		remove_all_filters( 'woocommerce_shipping_prices_include_tax' );

 		// Clean up created products.
 		foreach ( $this->created_products as $product_id ) {
@@ -700,4 +701,314 @@ class WC_Tax_Test extends WC_Unit_Test_Case {

 		return WC_Tax::_insert_tax_rate( $tax_rate );
 	}
+
+	/**
+	 * Test calc_shipping_tax default behavior (exclusive).
+	 */
+	public function test_calc_shipping_tax_default_behavior() {
+		$tax_rate = array(
+			'tax_rate_country'  => 'GB',
+			'tax_rate_state'    => '',
+			'tax_rate'          => '10.0000',
+			'tax_rate_name'     => 'VAT',
+			'tax_rate_priority' => '1',
+			'tax_rate_compound' => '0',
+			'tax_rate_shipping' => '1',
+			'tax_rate_order'    => '1',
+			'tax_rate_class'    => '',
+		);
+
+		$tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
+
+		$tax_rates = WC_Tax::find_rates(
+			array(
+				'country'   => 'GB',
+				'state'     => 'Cambs',
+				'postcode'  => 'PE14 1XX',
+				'city'      => 'Somewhere',
+				'tax_class' => '',
+			)
+		);
+
+		$taxes = WC_Tax::calc_shipping_tax( 10.00, $tax_rates );
+
+		// 10.00 * 10% = 1.00
+		$this->assertEquals( 1.00, array_sum( $taxes ), 'Default: 10% of 10.00 should be 1.00' );
+		$this->assertArrayHasKey( $tax_rate_id, $taxes, 'Default: Tax should be calculated from net price' );
+		$this->assertEquals( 1.00, $taxes[ $tax_rate_id ], 'Default: Tax amount should be 1.00' );
+	}
+
+	/**
+	 * Test calc_shipping_tax with inclusive filter enabled.
+	 */
+	public function test_calc_shipping_tax_inclusive_filter() {
+		$tax_rate = array(
+			'tax_rate_country'  => 'GB',
+			'tax_rate_state'    => '',
+			'tax_rate'          => '20.0000',
+			'tax_rate_name'     => 'VAT',
+			'tax_rate_priority' => '1',
+			'tax_rate_compound' => '0',
+			'tax_rate_shipping' => '1',
+			'tax_rate_order'    => '1',
+			'tax_rate_class'    => '',
+		);
+
+		WC_Tax::_insert_tax_rate( $tax_rate );
+
+		$tax_rates = WC_Tax::find_rates(
+			array(
+				'country'   => 'GB',
+				'state'     => 'Cambs',
+				'postcode'  => 'PE14 1XX',
+				'city'      => 'Somewhere',
+				'tax_class' => '',
+			)
+		);
+
+		// With filter: shipping cost is gross, tax is calculated from inclusive price.
+		// 10.00 gross, tax = 10.00 - (10.00 / 1.20) ≈ 1.67.
+		add_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+		$taxes = WC_Tax::calc_shipping_tax( 10.00, $tax_rates );
+		remove_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+
+		$expected_tax = 10.00 - ( 10.00 / 1.20 );
+		$this->assertEqualsWithDelta( $expected_tax, array_sum( $taxes ), 0.01, 'Inclusive: tax should be calculated from gross price' );
+	}
+
+	/**
+	 * Test calc_shipping_tax filter can be toggled.
+	 */
+	public function test_calc_shipping_tax_filter_toggle() {
+		$tax_rate = array(
+			'tax_rate_country'  => 'GB',
+			'tax_rate_state'    => '',
+			'tax_rate'          => '20.0000',
+			'tax_rate_name'     => 'VAT',
+			'tax_rate_priority' => '1',
+			'tax_rate_compound' => '0',
+			'tax_rate_shipping' => '1',
+			'tax_rate_order'    => '1',
+			'tax_rate_class'    => '',
+		);
+
+		$tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
+
+		$tax_rates = WC_Tax::find_rates(
+			array(
+				'country'   => 'GB',
+				'state'     => 'Cambs',
+				'postcode'  => 'PE14 1XX',
+				'city'      => 'Somewhere',
+				'tax_class' => '',
+			)
+		);
+
+		// Without filter: 10.00 * 20% = 2.00.
+		$taxes_exclusive = WC_Tax::calc_shipping_tax( 10.00, $tax_rates );
+		$this->assertEquals( 2.00, array_sum( $taxes_exclusive ), 'Without filter: tax should be 2.00' );
+
+		// With filter: 10.00 is gross, tax = 10.00 - (10.00 / 1.20) ≈ 1.67.
+		add_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+		$taxes_inclusive = WC_Tax::calc_shipping_tax( 10.00, $tax_rates );
+		$expected_tax    = 10.00 - ( 10.00 / 1.20 );
+		$this->assertEqualsWithDelta( $expected_tax, array_sum( $taxes_inclusive ), 0.01, 'With filter: tax should be calculated from gross' );
+
+		// Verify net cost after removing tax from gross.
+		$expected_net = 10.00 / 1.20;
+		$net          = 10.00 - array_sum( $taxes_inclusive );
+		$this->assertEqualsWithDelta( $expected_net, $net, 0.01, 'Net cost should equal gross divided by (1 + rate)' );
+
+		remove_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+	}
+
+	/**
+	 * Test calc_shipping_tax with zero tax rate.
+	 */
+	public function test_calc_shipping_tax_zero_rate() {
+		$tax_rate = array(
+			'tax_rate_country'  => 'GB',
+			'tax_rate_state'    => '',
+			'tax_rate'          => '0.0000',
+			'tax_rate_name'     => 'VAT',
+			'tax_rate_priority' => '1',
+			'tax_rate_compound' => '0',
+			'tax_rate_shipping' => '1',
+			'tax_rate_order'    => '1',
+			'tax_rate_class'    => '',
+		);
+
+		WC_Tax::_insert_tax_rate( $tax_rate );
+
+		$tax_rates = WC_Tax::find_rates(
+			array(
+				'country'   => 'GB',
+				'state'     => 'Cambs',
+				'postcode'  => 'PE14 1XX',
+				'city'      => 'Somewhere',
+				'tax_class' => '',
+			)
+		);
+
+		// With zero tax, both exclusive and inclusive should return same result.
+		$taxes_exclusive = WC_Tax::calc_shipping_tax( 10.00, $tax_rates );
+		$this->assertEquals( 0.00, array_sum( $taxes_exclusive ), 'Zero tax: exclusive should be 0' );
+
+		add_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+		$taxes_inclusive = WC_Tax::calc_shipping_tax( 10.00, $tax_rates );
+		$this->assertEquals( 0.00, array_sum( $taxes_inclusive ), 'Zero tax: inclusive should be 0' );
+		remove_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+	}
+
+	/**
+	 * Test calc_shipping_tax with no tax rates.
+	 */
+	public function test_calc_shipping_tax_no_rates() {
+		$taxes = WC_Tax::calc_shipping_tax( 10.00, array() );
+		$this->assertEmpty( $taxes, 'No tax rates should return empty array' );
+
+		add_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+		$taxes = WC_Tax::calc_shipping_tax( 10.00, array() );
+		$this->assertEmpty( $taxes, 'No tax rates with filter should return empty array' );
+		remove_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+	}
+
+	/**
+	 * Test calc_shipping_tax filter receives correct parameters.
+	 */
+	public function test_calc_shipping_tax_filter_parameters() {
+		$tax_rate = array(
+			'tax_rate_country'  => 'GB',
+			'tax_rate_state'    => '',
+			'tax_rate'          => '10.0000',
+			'tax_rate_name'     => 'VAT',
+			'tax_rate_priority' => '1',
+			'tax_rate_compound' => '0',
+			'tax_rate_shipping' => '1',
+			'tax_rate_order'    => '1',
+			'tax_rate_class'    => '',
+		);
+
+		$tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
+
+		$tax_rates = WC_Tax::find_rates(
+			array(
+				'country'   => 'GB',
+				'state'     => 'Cambs',
+				'postcode'  => 'PE14 1XX',
+				'city'      => 'Somewhere',
+				'tax_class' => '',
+			)
+		);
+
+		$received_args = null;
+
+		add_filter(
+			'woocommerce_shipping_prices_include_tax',
+			// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Parameter is captured via func_get_args() for the assertion.
+			function ( $include_tax ) use ( &$received_args ) {
+				$received_args = func_get_args();
+				return true;
+			},
+			10,
+			1
+		);
+
+		$taxes_with_filter = WC_Tax::calc_shipping_tax( 15.00, $tax_rates );
+
+		$this->assertIsArray( $received_args, 'Filter should be called' );
+		$this->assertCount( 1, $received_args, 'Filter should only receive the include_tax parameter' );
+
+		$expected_tax = 15.00 - ( 15.00 / 1.10 );
+		$this->assertEqualsWithDelta( $expected_tax, array_sum( $taxes_with_filter ), 0.01, 'Filter return value should be used for inclusive calculation' );
+
+		remove_all_filters( 'woocommerce_shipping_prices_include_tax' );
+	}
+
+	/**
+	 * Test per-item shipping tax when prices do not include tax.
+	 */
+	public function test_calc_shipping_tax_per_item_flow_exclusive() {
+		$tax_rate = array(
+			'tax_rate_country'  => 'GB',
+			'tax_rate_state'    => '',
+			'tax_rate'          => '20.0000',
+			'tax_rate_name'     => 'VAT',
+			'tax_rate_priority' => '1',
+			'tax_rate_compound' => '0',
+			'tax_rate_shipping' => '1',
+			'tax_rate_order'    => '1',
+			'tax_rate_class'    => '',
+		);
+
+		WC_Tax::_insert_tax_rate( $tax_rate );
+
+		$tax_rates = WC_Tax::find_rates(
+			array(
+				'country'   => 'GB',
+				'state'     => 'Cambs',
+				'postcode'  => 'PE14 1XX',
+				'city'      => 'Somewhere',
+				'tax_class' => '',
+			)
+		);
+
+		// Two items at 5 each. Tax is added on top.
+		$cost_item1 = 5.00;
+		$cost_item2 = 5.00;
+		$taxes1     = WC_Tax::calc_shipping_tax( $cost_item1, $tax_rates );
+		$taxes2     = WC_Tax::calc_shipping_tax( $cost_item2, $tax_rates );
+
+		$total_tax  = array_sum( $taxes1 ) + array_sum( $taxes2 );
+		$total_cost = $cost_item1 + $cost_item2;
+
+		$this->assertEquals( 2.00, $total_tax, 'Tax should be 2.00' );
+		$this->assertEquals( 10.00, $total_cost, 'Cost should stay 10.00' );
+	}
+
+	/**
+	 * Test per-item shipping tax when prices include tax.
+	 */
+	public function test_calc_shipping_tax_per_item_flow_inclusive() {
+		$tax_rate = array(
+			'tax_rate_country'  => 'GB',
+			'tax_rate_state'    => '',
+			'tax_rate'          => '20.0000',
+			'tax_rate_name'     => 'VAT',
+			'tax_rate_priority' => '1',
+			'tax_rate_compound' => '0',
+			'tax_rate_shipping' => '1',
+			'tax_rate_order'    => '1',
+			'tax_rate_class'    => '',
+		);
+
+		WC_Tax::_insert_tax_rate( $tax_rate );
+
+		$tax_rates = WC_Tax::find_rates(
+			array(
+				'country'   => 'GB',
+				'state'     => 'Cambs',
+				'postcode'  => 'PE14 1XX',
+				'city'      => 'Somewhere',
+				'tax_class' => '',
+			)
+		);
+
+		add_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+
+		// Two items at 6 each. Price includes tax.
+		$cost_item1 = 6.00;
+		$cost_item2 = 6.00;
+		$taxes1     = WC_Tax::calc_shipping_tax( $cost_item1, $tax_rates );
+		$taxes2     = WC_Tax::calc_shipping_tax( $cost_item2, $tax_rates );
+
+		$total_tax   = array_sum( $taxes1 ) + array_sum( $taxes2 );
+		$total_gross = $cost_item1 + $cost_item2;
+		$net_cost    = $total_gross - $total_tax;
+
+		$this->assertEqualsWithDelta( 2.00, $total_tax, 0.02, 'Tax taken from 12.00 should be about 2.00' );
+		$this->assertEqualsWithDelta( 10.00, $net_cost, 0.02, 'Net cost should be 10.00' );
+
+		remove_filter( 'woocommerce_shipping_prices_include_tax', '__return_true' );
+	}
 }