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' );
+ }
}