Commit d22eeb13b9 for woocommerce

commit d22eeb13b9e7723670cde6f015b4ebff5df838ba
Author: Łukasz Strączyński <8038404+straku@users.noreply.github.com>
Date:   Fri Jan 30 15:53:17 2026 +0100

    Fix V4 Refunds API extracting tax from refund_total on zero-tax shipping items (#63004)

    * Fix V4 Refunds API extracting tax from refund_total on zero-tax shipping items

    * Fix indentation

diff --git a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Refunds/DataUtils.php b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Refunds/DataUtils.php
index 88ba835fc8..9ec1615f1a 100644
--- a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Refunds/DataUtils.php
+++ b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Refunds/DataUtils.php
@@ -51,7 +51,14 @@ class DataUtils {
 				$original_item = $order->get_item( $line_item['line_item_id'] );
 				if ( $original_item ) {
 					$original_taxes = $original_item->get_taxes();
-					$tax_ids        = array_keys( $original_taxes['total'] ?? array() );
+					// Filter to only include tax IDs that have non-zero amounts.
+					$tax_totals = array_filter(
+						$original_taxes['total'] ?? array(),
+						function ( $amount ) {
+							return is_numeric( $amount ) && $amount > 0;
+						}
+					);
+					$tax_ids    = array_keys( $tax_totals );

 					if ( ! empty( $tax_ids ) ) {
 						$tax_rates = $this->build_tax_rates_array( $order, $tax_ids );
diff --git a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Refunds/DataUtilsTest.php b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Refunds/DataUtilsTest.php
index 8dfddad02e..cc83fb58ee 100644
--- a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Refunds/DataUtilsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Refunds/DataUtilsTest.php
@@ -243,6 +243,114 @@ class DataUtilsTest extends WC_Unit_Test_Case {
 		$this->assertEquals( 7.50, $result[ $item->get_id() ]['refund_tax'][ $tax_rate_id ] );
 	}

+	/**
+	 * Test that tax extraction is skipped for items with zero tax amounts.
+	 *
+	 * This tests the scenario where a line item (e.g., shipping) has tax rate IDs
+	 * in its taxes array but the actual tax amounts are zero. The API should NOT
+	 * attempt to extract taxes from refund_total in this case.
+	 */
+	public function test_convert_line_items_skips_tax_extraction_for_zero_tax_items() {
+		// Create a tax rate (applied to products but not shipping in this test).
+		$tax_rate_id = WC_Tax::_insert_tax_rate(
+			array(
+				'tax_rate_country'  => 'US',
+				'tax_rate_state'    => '',
+				'tax_rate'          => '10.0000',
+				'tax_rate_name'     => 'VAT',
+				'tax_rate_priority' => '1',
+				'tax_rate_compound' => '0',
+				'tax_rate_shipping' => '0', // Tax does NOT apply to shipping.
+				'tax_rate_order'    => '1',
+				'tax_rate_class'    => '',
+			)
+		);
+
+		// Create an order with shipping that has zero tax.
+		$order = $this->create_order_with_zero_tax_shipping( $tax_rate_id );
+
+		$shipping_items = $order->get_items( 'shipping' );
+		$shipping_item  = reset( $shipping_items );
+
+		// Verify the shipping item has tax IDs but zero amounts (the bug scenario).
+		$shipping_taxes = $shipping_item->get_taxes();
+		$this->assertArrayHasKey( 'total', $shipping_taxes );
+		$this->assertArrayHasKey( $tax_rate_id, $shipping_taxes['total'] );
+		$this->assertEquals( 0, (float) $shipping_taxes['total'][ $tax_rate_id ] );
+
+		// Line items WITHOUT explicit refund_tax for shipping.
+		$line_items = array(
+			array(
+				'line_item_id' => $shipping_item->get_id(),
+				'quantity'     => 1,
+				'refund_total' => 10.00, // Shipping cost to refund (no tax included).
+			),
+		);
+
+		// Convert line items.
+		$result = $this->data_utils->convert_line_items_to_internal_format( $line_items, $order );
+
+		// Assertions.
+		$this->assertIsArray( $result );
+		$this->assertArrayHasKey( $shipping_item->get_id(), $result );
+
+		// refund_total should remain unchanged (10.00) since there's no tax to extract.
+		$this->assertEquals( 10.00, $result[ $shipping_item->get_id() ]['refund_total'] );
+
+		// refund_tax should be empty since the item has zero taxes.
+		$this->assertEmpty( $result[ $shipping_item->get_id() ]['refund_tax'] );
+	}
+
+	/**
+	 * Helper: Create an order with shipping that has tax rate IDs but zero tax amounts.
+	 *
+	 * This simulates the scenario where a tax rate exists but doesn't apply to shipping.
+	 *
+	 * @param int $tax_rate_id Tax rate ID.
+	 * @return WC_Order Order with zero-tax shipping.
+	 */
+	private function create_order_with_zero_tax_shipping( int $tax_rate_id ): WC_Order {
+		// Enable tax calculations.
+		update_option( 'woocommerce_calc_taxes', 'yes' );
+		update_option( 'woocommerce_prices_include_tax', 'no' );
+
+		// Create an order.
+		$order = wc_create_order();
+
+		// Add a shipping item with zero taxes but tax rate IDs present.
+		$shipping_item = new \WC_Order_Item_Shipping();
+		$shipping_item->set_method_title( 'Flat Rate' );
+		$shipping_item->set_method_id( 'flat_rate' );
+		$shipping_item->set_total( 10.00 );
+		// Set taxes with the tax rate ID but zero amount (this is the bug scenario).
+		$shipping_item->set_taxes(
+			array(
+				'total' => array( $tax_rate_id => '0' ),
+			)
+		);
+		$shipping_item->save();
+		$order->add_item( $shipping_item );
+
+		// Add a tax item to the order (for the tax rate to be recognized).
+		$tax_item = new \WC_Order_Item_Tax();
+		$tax_item->set_rate( $tax_rate_id );
+		$tax_item->set_order_id( $order->get_id() );
+		$tax_item->set_tax_total( 0 ); // Product tax would be here, but we're focusing on shipping.
+		$tax_item->set_shipping_tax_total( 0 );
+		$tax_item->save();
+		$order->add_item( $tax_item );
+
+		// Set billing address.
+		$order->set_billing_country( 'US' );
+		$order->set_billing_state( '' );
+
+		// Save order.
+		$order->calculate_totals( false );
+		$order->save();
+
+		return $order;
+	}
+
 	/**
 	 * Helper: Create an order with taxes applied.
 	 *