Commit bc45a18da1b for woocommerce
commit bc45a18da1b6ccdb83b2a971b118a18317b619e9
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date: Fri Jun 12 13:08:26 2026 +0200
[Performance] Reduce the number of SQL queries required to fetch tax rates in the cart and during checkout (#65558)
Reduces the number of SQL queries on the checkout hot path (by bulk-fetching tax rates), giving the Database more headroom for HVM/BFCM use cases.
diff --git a/plugins/woocommerce/changelog/performance-tax-rates-sqls-number b/plugins/woocommerce/changelog/performance-tax-rates-sqls-number
new file mode 100644
index 00000000000..f097d61a3d2
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-tax-rates-sqls-number
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Reduced the number of SQL queries required to fetch tax rates in the cart and during checkout.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index 051a9eae379..8dd3e01c12b 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -18,6 +18,7 @@ use Automattic\WooCommerce\Enums\TaxBasedOn;
use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareTrait;
use Automattic\WooCommerce\Internal\Customers\SearchService as CustomersSearchService;
use Automattic\WooCommerce\Internal\Orders\PaymentInfo;
+use Automattic\WooCommerce\Internal\Tax\TaxRateDataStore;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\NumberUtil;
@@ -2070,17 +2071,27 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
}
}
+ $tax_rate_objects = wc_get_container()->get( TaxRateDataStore::class )->get_rate_objects_for_ids( array_keys( $cart_taxes + $shipping_taxes ) );
+
foreach ( $existing_taxes as $tax ) {
+ $tax_rate_id = $tax->get_rate_id();
// Remove taxes which no longer exist for cart/shipping.
- if ( ( ! array_key_exists( $tax->get_rate_id(), $cart_taxes ) && ! array_key_exists( $tax->get_rate_id(), $shipping_taxes ) ) || in_array( $tax->get_rate_id(), $saved_rate_ids, true ) ) {
+ if ( ( ! array_key_exists( $tax_rate_id, $cart_taxes ) && ! array_key_exists( $tax_rate_id, $shipping_taxes ) ) || in_array( $tax_rate_id, $saved_rate_ids, true ) ) {
$this->remove_item( $tax->get_id() );
continue;
}
- $saved_rate_ids[] = $tax->get_rate_id();
- $tax->set_rate( $tax->get_rate_id() );
- $tax->set_tax_total( isset( $cart_taxes[ $tax->get_rate_id() ] ) ? $cart_taxes[ $tax->get_rate_id() ] : 0 );
- $tax->set_label( WC_Tax::get_rate_label( $tax->get_rate_id() ) );
- $tax->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax->get_rate_id() ] ) ? $shipping_taxes[ $tax->get_rate_id() ] : 0 );
+
+ $tax_rate_object_or_id = $tax_rate_objects[ $tax_rate_id ] ?? $tax_rate_id;
+
+ $tax->set_rate_id( $tax_rate_id );
+ $tax->set_rate_code( WC_Tax::get_rate_code( $tax_rate_object_or_id ) );
+ $tax->set_label( WC_Tax::get_rate_label( $tax_rate_object_or_id ) );
+ $tax->set_compound( WC_Tax::is_compound( $tax_rate_object_or_id ) );
+ $tax->set_rate_percent( WC_Tax::get_rate_percent_value( $tax_rate_object_or_id ) );
+ $tax->set_tax_total( $cart_taxes[ $tax_rate_id ] ?? 0 );
+ $tax->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax_rate_id ] ) ? $shipping_taxes[ $tax_rate_id ] : 0 );
+
+ $saved_rate_ids[] = $tax_rate_id;
$tax->save();
}
@@ -2088,10 +2099,17 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
// New taxes.
foreach ( $new_rate_ids as $tax_rate_id ) {
+ $tax_rate_object_or_id = $tax_rate_objects[ $tax_rate_id ] ?? $tax_rate_id;
+
$item = new WC_Order_Item_Tax();
- $item->set_rate( $tax_rate_id );
- $item->set_tax_total( isset( $cart_taxes[ $tax_rate_id ] ) ? $cart_taxes[ $tax_rate_id ] : 0 );
+ $item->set_rate_id( $tax_rate_id );
+ $item->set_rate_code( WC_Tax::get_rate_code( $tax_rate_object_or_id ) );
+ $item->set_label( WC_Tax::get_rate_label( $tax_rate_object_or_id ) );
+ $item->set_compound( WC_Tax::is_compound( $tax_rate_object_or_id ) );
+ $item->set_rate_percent( WC_Tax::get_rate_percent_value( $tax_rate_object_or_id ) );
+ $item->set_tax_total( $cart_taxes[ $tax_rate_id ] ?? 0 );
$item->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax_rate_id ] ) ? $shipping_taxes[ $tax_rate_id ] : 0 );
+
$this->add_item( $item );
}
diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php
index b6e976e832d..4e29045e352 100644
--- a/plugins/woocommerce/includes/class-wc-cart.php
+++ b/plugins/woocommerce/includes/class-wc-cart.php
@@ -12,10 +12,11 @@
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Enums\ProductType;
+use Automattic\WooCommerce\Internal\Tax\TaxRateDataStore;
+use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
use Automattic\WooCommerce\Utilities\DiscountsUtil;
use Automattic\WooCommerce\Utilities\NumberUtil;
use Automattic\WooCommerce\Utilities\ShippingUtil;
-use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
defined( 'ABSPATH' ) || exit;
@@ -979,12 +980,14 @@ class WC_Cart extends WC_Legacy_Cart {
* @return array
*/
public function get_tax_totals() {
- $shipping_taxes = $this->get_shipping_taxes(); // Shipping taxes are rounded differently, so we will subtract from all taxes, then round and then add them back.
- $taxes = $this->get_taxes();
- $tax_totals = array();
+ $shipping_taxes = $this->get_shipping_taxes();
+ $taxes = $this->get_taxes();
+ $tax_rate_objects = wc_get_container()->get( TaxRateDataStore::class )->get_rate_objects_for_ids( array_keys( $taxes ) );
+ $tax_totals = array();
foreach ( $taxes as $key => $tax ) {
- $code = WC_Tax::get_rate_code( $key );
+ $tax_rate_object_or_id = $tax_rate_objects[ $key ] ?? $key;
+ $code = WC_Tax::get_rate_code( $tax_rate_object_or_id );
if ( $code || apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) === $key ) {
if ( ! isset( $tax_totals[ $code ] ) ) {
@@ -993,9 +996,10 @@ class WC_Cart extends WC_Legacy_Cart {
}
$tax_totals[ $code ]->tax_rate_id = $key;
- $tax_totals[ $code ]->is_compound = WC_Tax::is_compound( $key );
- $tax_totals[ $code ]->label = WC_Tax::get_rate_label( $key );
+ $tax_totals[ $code ]->is_compound = WC_Tax::is_compound( $tax_rate_object_or_id );
+ $tax_totals[ $code ]->label = WC_Tax::get_rate_label( $tax_rate_object_or_id );
+ // Shipping taxes are rounded differently, so we will subtract from all taxes, then round and then add them back.
if ( isset( $shipping_taxes[ $key ] ) ) {
$tax -= $shipping_taxes[ $key ];
$tax = wc_round_tax_total( $tax );
@@ -2377,7 +2381,7 @@ class WC_Cart extends WC_Legacy_Cart {
*/
public function get_tax_amount( $tax_rate_id ) {
$taxes = wc_array_merge_recursive_numeric( $this->get_cart_contents_taxes(), $this->get_fee_taxes() );
- return isset( $taxes[ $tax_rate_id ] ) ? $taxes[ $tax_rate_id ] : 0;
+ return $taxes[ $tax_rate_id ] ?? 0;
}
/**
@@ -2388,7 +2392,7 @@ class WC_Cart extends WC_Legacy_Cart {
*/
public function get_shipping_tax_amount( $tax_rate_id ) {
$taxes = $this->get_shipping_taxes();
- return isset( $taxes[ $tax_rate_id ] ) ? $taxes[ $tax_rate_id ] : 0;
+ return $taxes[ $tax_rate_id ] ?? 0;
}
/**
@@ -2399,17 +2403,21 @@ class WC_Cart extends WC_Legacy_Cart {
* @return float price
*/
public function get_taxes_total( $compound = true, $display = true ) {
- $total = 0;
$taxes = $this->get_taxes();
- foreach ( $taxes as $key => $tax ) {
- if ( ! $compound && WC_Tax::is_compound( $key ) ) {
- continue;
+
+ // Skip compounding taxes if requested.
+ if ( ! $compound ) {
+ $tax_rate_objects = wc_get_container()->get( TaxRateDataStore::class )->get_rate_objects_for_ids( array_keys( $taxes ) );
+ foreach ( $taxes as $key => $tax ) {
+ if ( WC_Tax::is_compound( $tax_rate_objects[ $key ] ?? $key ) ) {
+ unset( $taxes[ $key ] );
+ }
}
- $total += $tax;
- }
- if ( $display ) {
- $total = wc_format_decimal( $total, wc_get_price_decimals() );
}
+
+ $total = array_sum( $taxes );
+ $total = $display ? wc_format_decimal( $total, wc_get_price_decimals() ) : $total;
+
return apply_filters( 'woocommerce_cart_taxes_total', $total, $compound, $display, $this );
}
diff --git a/plugins/woocommerce/includes/class-wc-checkout.php b/plugins/woocommerce/includes/class-wc-checkout.php
index 4ba18230ac8..520c1ef8036 100644
--- a/plugins/woocommerce/includes/class-wc-checkout.php
+++ b/plugins/woocommerce/includes/class-wc-checkout.php
@@ -11,6 +11,7 @@
use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareTrait;
+use Automattic\WooCommerce\Internal\Tax\TaxRateDataStore;
defined( 'ABSPATH' ) || exit;
@@ -680,7 +681,10 @@ class WC_Checkout {
* @param WC_Cart $cart Cart instance.
*/
public function create_order_tax_lines( &$order, $cart ) {
- foreach ( array_keys( $cart->get_cart_contents_taxes() + $cart->get_shipping_taxes() + $cart->get_fee_taxes() ) as $tax_rate_id ) {
+ $tax_rate_ids = array_keys( $cart->get_cart_contents_taxes() + $cart->get_shipping_taxes() + $cart->get_fee_taxes() );
+ $tax_rate_objects = wc_get_container()->get( TaxRateDataStore::class )->get_rate_objects_for_ids( $tax_rate_ids );
+
+ foreach ( $tax_rate_ids as $tax_rate_id ) {
/**
* Controls the zero rate tax ID.
*
@@ -691,16 +695,17 @@ class WC_Checkout {
* @param string $tax_rate_id The ID of the zero rate tax.
*/
if ( $tax_rate_id && apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) !== $tax_rate_id ) {
- $item = new WC_Order_Item_Tax();
+ $tax_rate_object_or_id = $tax_rate_objects[ $tax_rate_id ] ?? $tax_rate_id;
+ $item = new WC_Order_Item_Tax();
$item->set_props(
array(
'rate_id' => $tax_rate_id,
'tax_total' => $cart->get_tax_amount( $tax_rate_id ),
'shipping_tax_total' => $cart->get_shipping_tax_amount( $tax_rate_id ),
- 'rate_code' => WC_Tax::get_rate_code( $tax_rate_id ),
- 'label' => WC_Tax::get_rate_label( $tax_rate_id ),
- 'compound' => WC_Tax::is_compound( $tax_rate_id ),
- 'rate_percent' => WC_Tax::get_rate_percent_value( $tax_rate_id ),
+ 'rate_code' => WC_Tax::get_rate_code( $tax_rate_object_or_id ),
+ 'label' => WC_Tax::get_rate_label( $tax_rate_object_or_id ),
+ 'compound' => WC_Tax::is_compound( $tax_rate_object_or_id ),
+ 'rate_percent' => WC_Tax::get_rate_percent_value( $tax_rate_object_or_id ),
)
);
diff --git a/plugins/woocommerce/src/Internal/Tax/TaxRateDataStore.php b/plugins/woocommerce/src/Internal/Tax/TaxRateDataStore.php
new file mode 100644
index 00000000000..9d48c9c15e1
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Tax/TaxRateDataStore.php
@@ -0,0 +1,37 @@
+<?php
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Tax;
+
+/**
+ * Data store for tax rates.
+ */
+class TaxRateDataStore {
+
+ /**
+ * Fetch multiple tax rate rows in a single query, keyed by tax_rate_id.
+ *
+ * @since 11.0.0
+ *
+ * @param int[] $ids Tax rate IDs to fetch.
+ * @return array<int,object>
+ */
+ public function get_rate_objects_for_ids( array $ids ): array {
+ $tax_rate_objects = array();
+ $ids = array_values( array_filter( array_map( 'absint', array_unique( $ids ) ) ) );
+
+ if ( ! empty( $ids ) ) {
+ global $wpdb;
+
+ $list = implode( ', ', $ids );
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id IN ( $list )" );
+ foreach ( $rows as $row ) {
+ $tax_rate_objects[ (int) $row->tax_rate_id ] = $row;
+ }
+ }
+
+ return $tax_rate_objects;
+ }
+}
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/CartSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/CartSchema.php
index 55b2a3cf52c..21a2c257bdc 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/CartSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/CartSchema.php
@@ -1,11 +1,11 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
+use Automattic\WooCommerce\Internal\Tax\TaxRateDataStore;
use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
use Automattic\WooCommerce\StoreApi\Utilities\CartController;
-use WC_Tax;
/**
* CartSchema class.
@@ -416,14 +416,15 @@ class CartSchema extends AbstractSchema {
return $tax_lines;
}
- $cart_tax_totals = $cart->get_tax_totals();
- $decimals = wc_get_price_decimals();
+ $cart_tax_totals = $cart->get_tax_totals();
+ $tax_rate_objects = wc_get_container()->get( TaxRateDataStore::class )->get_rate_objects_for_ids( array_column( $cart_tax_totals, 'tax_rate_id' ) );
+ $decimals = wc_get_price_decimals();
foreach ( $cart_tax_totals as $cart_tax_total ) {
$tax_lines[] = array(
'name' => $cart_tax_total->label,
'price' => $this->prepare_money_response( $cart_tax_total->amount, $decimals ),
- 'rate' => WC_Tax::get_rate_percent( $cart_tax_total->tax_rate_id ),
+ 'rate' => \WC_Tax::get_rate_percent( $tax_rate_objects[ $cart_tax_total->tax_rate_id ] ?? $cart_tax_total->tax_rate_id ),
);
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php b/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php
index b1ff8083453..9b38797249d 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php
@@ -1998,6 +1998,51 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$this->assertEquals( array(), $tax_totals );
}
+ /**
+ * Test that get_taxes_total excludes compound rates when $compound is false.
+ */
+ public function test_get_taxes_total_excludes_compound_rates(): void {
+ update_option( 'woocommerce_calc_taxes', 'yes' );
+
+ // Standard (non-compound) 10% rate.
+ $standard_rate = array(
+ 'tax_rate_country' => '',
+ 'tax_rate_state' => '',
+ 'tax_rate' => '10.0000',
+ 'tax_rate_name' => 'TAX',
+ 'tax_rate_priority' => '1',
+ 'tax_rate_compound' => '0',
+ 'tax_rate_shipping' => '1',
+ 'tax_rate_order' => '1',
+ 'tax_rate_class' => '',
+ );
+ WC_Tax::_insert_tax_rate( $standard_rate );
+
+ // Compound 5% rate applied on top.
+ $compound_rate = array(
+ 'tax_rate_country' => '',
+ 'tax_rate_state' => '',
+ 'tax_rate' => '5.0000',
+ 'tax_rate_name' => 'COMPOUND',
+ 'tax_rate_priority' => '2',
+ 'tax_rate_compound' => '1',
+ 'tax_rate_shipping' => '1',
+ 'tax_rate_order' => '2',
+ 'tax_rate_class' => '',
+ );
+ WC_Tax::_insert_tax_rate( $compound_rate );
+
+ $product = WC_Helper_Product::create_simple_product();
+ WC()->customer->set_is_vat_exempt( false );
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
+
+ // $compound = false: only the standard 10% rate included → $10 × 10% = $1.00.
+ $this->assertSame( 1.0, WC()->cart->get_taxes_total( false, false ) );
+
+ // $compound = true: both rates included → $1.00 + ($10 + $1.00) × 5% = $1.55.
+ $this->assertSame( 1.55, WC()->cart->get_taxes_total( true, false ) );
+ }
+
/**
* Check subtotals align when using filters. Ref: 23340
*/
@@ -2006,7 +2051,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
update_option( 'woocommerce_prices_include_tax', 'yes' );
update_option( 'woocommerce_calc_taxes', 'yes' );
- // 5% tax.
+ // 5% non-compound rate for the standard (default) tax class.
$tax_rate = array(
'tax_rate_country' => '',
'tax_rate_state' => '',
@@ -2020,7 +2065,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
);
WC_Tax::_insert_tax_rate( $tax_rate );
- // 20% tax.
+ // 20% non-compound rate for the reduced-rate class (switched to via filter below).
$tax_rate = array(
'tax_rate_country' => '',
'tax_rate_state' => '',
@@ -2042,6 +2087,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product1->get_id(), 1 );
WC()->cart->calculate_totals();
+ // Standard class (5%): price is tax-inclusive, subtotal is the ex-tax base, total matches the entered price.
$this->assertEquals( '5.71', WC()->cart->get_subtotal() );
$this->assertEquals( '6.00', WC()->cart->get_total( 'edit' ) );
@@ -2049,6 +2095,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
add_filter( 'woocommerce_product_variation_get_tax_class', array( $this, 'change_tax_class_filter' ) );
WC()->cart->calculate_totals();
+ // Reduced-rate class (20%) applied via filter: ex-tax subtotal is unchanged, total rises to reflect higher rate.
$this->assertEquals( '5.71', WC()->cart->get_subtotal() );
$this->assertEquals( '6.85', WC()->cart->get_total( 'edit' ) );
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 70b7b366721..ef6dfb13ce5 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
@@ -621,6 +621,86 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
$this->assertEquals( $expected, $actual );
}
+ /**
+ * @testdox update_taxes persists cart and shipping tax totals as order tax items, and updates existing items in-place on a second call.
+ */
+ public function test_update_taxes_persists_cart_and_shipping_tax_totals(): void {
+ update_option( 'woocommerce_calc_taxes', 'yes' );
+
+ // German standard 19% non-compound VAT rate.
+ $tax_rate = array(
+ 'tax_rate_country' => 'DE',
+ 'tax_rate_state' => '',
+ 'tax_rate' => '19.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 );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $order = new WC_Order();
+
+ // Line item carrying $1.00 cart tax for the rate.
+ // WC_Order_Item_Product::set_taxes() requires both 'total' and 'subtotal' to be non-empty.
+ $line_item = new WC_Order_Item_Product();
+ $line_item->set_product( $product );
+ $line_item->set_taxes(
+ array(
+ 'total' => array( $tax_rate_id => '1.00' ),
+ 'subtotal' => array( $tax_rate_id => '1.00' ),
+ )
+ );
+ $order->add_item( $line_item );
+
+ // Shipping item carrying $0.50 shipping tax for the rate.
+ $shipping_item = new WC_Order_Item_Shipping();
+ $shipping_item->set_taxes( array( 'total' => array( $tax_rate_id => '0.50' ) ) );
+ $order->add_item( $shipping_item );
+
+ $order->save();
+ $order->update_taxes();
+
+ $tax_items = $order->get_taxes();
+ $this->assertCount( 1, $tax_items );
+
+ /** @var WC_Order_Item_Tax $tax_item */
+ $tax_item = reset( $tax_items );
+ // Confirm the German 19% VAT rate is correctly associated with the tax item.
+ $this->assertSame( 19.0, (float) $tax_item->get_rate_percent() );
+ // Cart and shipping taxes are accumulated from line items and persisted on the tax item.
+ $this->assertSame( 1.00, (float) $tax_item->get_tax_total() );
+ $this->assertSame( 0.50, (float) $tax_item->get_shipping_tax_total() );
+ // Order-level totals are rolled up from all tax items.
+ $this->assertSame( 1.00, (float) $order->get_cart_tax() );
+ $this->assertSame( 0.50, (float) $order->get_shipping_tax() );
+
+ // Second call: update line item taxes and verify existing tax item is updated in-place, not duplicated.
+ foreach ( $order->get_items() as $item ) {
+ if ( $item instanceof WC_Order_Item_Product ) {
+ $item->set_taxes(
+ array(
+ 'total' => array( $tax_rate_id => '2.00' ),
+ 'subtotal' => array( $tax_rate_id => '2.00' ),
+ )
+ );
+ $item->save();
+ }
+ }
+
+ $order->update_taxes();
+
+ $tax_items_after = $order->get_taxes();
+ $this->assertCount( 1, $tax_items_after, 'update_taxes() must update the existing tax item, not create a duplicate.' );
+
+ $tax_item_after = reset( $tax_items_after );
+ $this->assertSame( 2.00, (float) $tax_item_after->get_tax_total() );
+ $this->assertSame( 0.50, (float) $tax_item_after->get_shipping_tax_total() );
+ }
+
/**
* Get an order object with a fixed total COGS value.
*
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
index e12689bc026..cff3fc7ae0b 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
@@ -43,6 +43,7 @@ class WC_Checkout_Test extends \WC_Unit_Test_Case {
*/
public function tearDown(): void {
remove_filter( 'woocommerce_checkout_registration_enabled', '__return_true' );
+ delete_option( 'woocommerce_calc_taxes' );
}
/**
@@ -249,6 +250,46 @@ class WC_Checkout_Test extends \WC_Unit_Test_Case {
WC()->customer = $orig_customer;
}
+ /**
+ * @testdox create_order_tax_lines sets rate_code, label, compound and rate_percent on order tax items.
+ */
+ public function test_create_order_tax_lines_sets_correct_tax_item_props(): void {
+ update_option( 'woocommerce_calc_taxes', 'yes' );
+
+ // German standard 19% non-compound VAT rate.
+ $tax_rate = array(
+ 'tax_rate_country' => 'DE',
+ 'tax_rate_state' => '',
+ 'tax_rate' => '19.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 );
+
+ $product = WC_Helper_Product::create_simple_product();
+ WC()->customer->set_billing_country( 'DE' );
+ WC()->customer->set_shipping_country( 'DE' );
+ WC()->customer->set_is_vat_exempt( false );
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
+ WC()->cart->calculate_totals();
+
+ $order = wc_get_order( $this->sut->create_order( array( 'payment_method' => WC_Gateway_BACS::ID ) ) );
+ $tax_items = $order->get_taxes();
+
+ $this->assertCount( 1, $tax_items );
+
+ /** @var WC_Order_Item_Tax $tax_item */
+ $tax_item = array_values( $tax_items )[0];
+ $this->assertSame( 'DE-VAT-1', $tax_item->get_rate_code() );
+ $this->assertSame( 'VAT', $tax_item->get_label() );
+ $this->assertFalse( $tax_item->get_compound() );
+ $this->assertSame( 19.0, $tax_item->get_rate_percent() );
+ }
+
/**
* @testdox Checkout page contains login form for guests.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/Tax/TaxRateDataStoreTest.php b/plugins/woocommerce/tests/php/src/Internal/Tax/TaxRateDataStoreTest.php
new file mode 100644
index 00000000000..14c176169f7
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Tax/TaxRateDataStoreTest.php
@@ -0,0 +1,53 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Tax;
+
+use Automattic\WooCommerce\Internal\Tax\TaxRateDataStore;
+
+/**
+ * Tests for TaxRateDataStore.
+ */
+class TaxRateDataStoreTest extends \WC_Unit_Test_Case {
+
+ /**
+ * @var TaxRateDataStore
+ */
+ private $sut;
+
+ /**
+ * Set up subject under test.
+ */
+ public function set_up() {
+ $this->sut = wc_get_container()->get( TaxRateDataStore::class );
+ parent::set_up();
+ }
+
+ /**
+ * @testdox get_rate_objects_for_ids() deduplicates mixed int/string IDs and returns a map keyed by int tax_rate_id.
+ */
+ public function test_get_rate_objects_for_ids(): void {
+ // Arrange.
+ $tax_rate = array(
+ 'tax_rate_country' => 'DE',
+ 'tax_rate_state' => '',
+ 'tax_rate' => '19.0000',
+ 'tax_rate_name' => 'VAT',
+ 'tax_rate_priority' => '1',
+ 'tax_rate_compound' => '1',
+ 'tax_rate_shipping' => '1',
+ 'tax_rate_order' => '1',
+ 'tax_rate_class' => '',
+ );
+ $tax_rate_id = \WC_Tax::_insert_tax_rate( $tax_rate );
+
+ // Act.
+ $result = $this->sut->get_rate_objects_for_ids( array( $tax_rate_id, (string) $tax_rate_id, PHP_INT_MAX ) );
+
+ // Assert.
+ $this->assertCount( 1, $result );
+ $this->assertSame( array( $tax_rate_id ), array_keys( $result ) );
+ $this->assertSame( 'VAT', $result[ $tax_rate_id ]->tax_rate_name );
+ }
+}