Commit d8d86c5efd7 for woocommerce
commit d8d86c5efd7a881b56f41be7cc70b651d3312bb3
Author: Cvetan Cvetanov <cvetan.cvetanov@automattic.com>
Date: Wed Jul 15 16:52:47 2026 +0300
Fix shipping tax for manual orders without products (#66639)
* Fix shipping tax for orders without products
* Add changelog entry for shipping-only order tax fix
* Clarify inherited shipping tax fallback scope
diff --git a/plugins/woocommerce/changelog/fix-shipping-only-order-tax b/plugins/woocommerce/changelog/fix-shipping-only-order-tax
new file mode 100644
index 00000000000..bd92a4811db
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-shipping-only-order-tax
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Apply shipping tax to manual orders that contain shipping but no products.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index e9116bdd302..02717210d99 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -1998,6 +1998,11 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
if ( 'inherit' === $shipping_tax_class ) {
$found_classes = array_intersect( array_merge( array( '' ), WC_Tax::get_tax_class_slugs() ), $this->get_items_tax_classes() );
$shipping_tax_class = count( $found_classes ) ? current( $found_classes ) : false;
+
+ // Orders without product line items have no tax class to inherit, so use the standard class.
+ if ( false === $shipping_tax_class && 0 === count( $this->get_items() ) ) {
+ $shipping_tax_class = '';
+ }
}
$is_vat_exempt = apply_filters( 'woocommerce_order_is_vat_exempt', 'yes' === $this->get_meta( 'is_vat_exempt' ), $this );
diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php
index b9370e0cfa0..06357426cad 100644
--- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php
+++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php
@@ -8,6 +8,7 @@
use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareUnitTestSuiteTrait;
use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\FunctionsMockerHack;
use Automattic\WooCommerce\Enums\OrderStatus;
+use Automattic\WooCommerce\Enums\ProductTaxStatus;
// phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps -- Backward compatibility.
/**
@@ -714,6 +715,86 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
$this->assertSame( 0.50, (float) $tax_item_after->get_shipping_tax_total() );
}
+ /**
+ * @testdox calculate_taxes handles inherited shipping tax when no taxable product class is available.
+ * @dataProvider inherited_shipping_tax_without_taxable_products_provider
+ *
+ * @param bool $add_non_taxable_product Whether to add a non-taxable product to the order.
+ * @param bool $add_non_taxable_fee Whether to add a non-taxable fee to the order.
+ * @param float $expected_shipping_tax Expected shipping tax total.
+ */
+ public function test_calculate_taxes_handles_inherited_shipping_tax_without_taxable_products( bool $add_non_taxable_product, bool $add_non_taxable_fee, float $expected_shipping_tax ): void {
+ $original_calc_taxes = get_option( 'woocommerce_calc_taxes', 'no' );
+ $original_shipping_tax_class = get_option( 'woocommerce_shipping_tax_class', 'inherit' );
+ $order = new WC_Order();
+ $product = null;
+
+ update_option( 'woocommerce_calc_taxes', 'yes' );
+ update_option( 'woocommerce_shipping_tax_class', 'inherit' );
+
+ $tax_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => '',
+ 'tax_rate_state' => '',
+ 'tax_rate' => '10.0000',
+ 'tax_rate_name' => 'Standard tax',
+ 'tax_rate_priority' => '1',
+ 'tax_rate_compound' => '0',
+ 'tax_rate_shipping' => '1',
+ 'tax_rate_order' => '1',
+ 'tax_rate_class' => '',
+ )
+ );
+
+ try {
+ if ( $add_non_taxable_product ) {
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_tax_status( ProductTaxStatus::NONE );
+ $product->save();
+ $order->add_product( $product );
+ }
+
+ if ( $add_non_taxable_fee ) {
+ $fee_item = new WC_Order_Item_Fee();
+ $fee_item->set_name( 'Manual fee' );
+ $fee_item->set_amount( '5' );
+ $fee_item->set_total( '5' );
+ $fee_item->set_tax_status( ProductTaxStatus::NONE );
+ $order->add_item( $fee_item );
+ }
+
+ $shipping_item = new WC_Order_Item_Shipping();
+ $shipping_item->set_method_title( 'Manual shipping' );
+ $shipping_item->set_total( '10' );
+ $order->add_item( $shipping_item );
+
+ $order->calculate_totals();
+
+ $this->assertSame( $expected_shipping_tax, (float) $order->get_shipping_tax() );
+ } finally {
+ WC_Tax::_delete_tax_rate( $tax_rate_id );
+ update_option( 'woocommerce_calc_taxes', $original_calc_taxes );
+ update_option( 'woocommerce_shipping_tax_class', $original_shipping_tax_class );
+ $order->delete( true );
+ if ( $product ) {
+ $product->delete( true );
+ }
+ }
+ }
+
+ /**
+ * Data provider for inherited shipping tax calculations without taxable products.
+ *
+ * @return array<string, array{bool, bool, float}>
+ */
+ public function inherited_shipping_tax_without_taxable_products_provider(): array {
+ return array(
+ 'shipping-only order' => array( false, false, 1.0 ),
+ 'non-taxable product' => array( true, false, 0.0 ),
+ 'non-taxable fee with shipping' => array( false, true, 1.0 ),
+ );
+ }
+
/**
* Get an order object with a fixed total COGS value.
*