Commit 39f9674987d for woocommerce

commit 39f9674987dafb8e61e433cf1626d50ceefa1c49
Author: Mike Jolley <mike.jolley@me.com>
Date:   Thu Nov 5 23:43:55 2026 +0000

    Show order preview refunds only on the refunded line item (#70294)

    * Show order preview refunds only on the refunded line item

    The order preview matched refunds to line items by product ID, so a
    refund on one variation, or on one of two lines for the same product,
    showed against every line sharing that product. Match on the refund
    item's `_refunded_item_id` instead, as the order edit screen does.

    Based on the approach in #64677.

    Co-authored-by: priyanshuhaldar007 <priyanshu1843@gmail.com>
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Remove stale get_product_id() PHPStan baseline entry

    The refund loop no longer calls get_product_id() on refund items, so the
    baselined error no longer occurs.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: priyanshuhaldar007 <priyanshu1843@gmail.com>
    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/64323-order-preview-refunds-per-line b/plugins/woocommerce/changelog/64323-order-preview-refunds-per-line
new file mode 100644
index 00000000000..ff48a7beda7
--- /dev/null
+++ b/plugins/woocommerce/changelog/64323-order-preview-refunds-per-line
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show refunds in the order preview only on the line items that were refunded, not on other variations or lines of the same product.
diff --git a/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-orders.php b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-orders.php
index 83c30d1455a..8eae3d3fe9a 100644
--- a/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-orders.php
+++ b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-orders.php
@@ -271,17 +271,16 @@ class WC_Admin_List_Table_Orders extends WC_Admin_List_Table {

 		$refunds = array();
 		foreach ( $order->get_refunds() as $refund ) {
-			foreach ( $refund->get_items() as $item ) {
-				$product_id = $item->get_product_id();
-				if ( array_key_exists( $product_id, $refunds ) ) {
-					$refunds[ $product_id ]['quantity'] += absint( $item->get_quantity() );
-					$refunds[ $product_id ]['total']    += abs( (float) $item->get_total() );
-				} else {
-					$refunds[ $product_id ] = array(
-						'quantity' => absint( $item->get_quantity() ),
-						'total'    => abs( (float) $item->get_total() ),
+			foreach ( $refund->get_items() as $refunded_item ) {
+				$refunded_item_id = absint( $refunded_item->get_meta( '_refunded_item_id' ) );
+				if ( ! isset( $refunds[ $refunded_item_id ] ) ) {
+					$refunds[ $refunded_item_id ] = array(
+						'quantity' => 0,
+						'total'    => 0,
 					);
 				}
+				$refunds[ $refunded_item_id ]['quantity'] += absint( $refunded_item->get_quantity() );
+				$refunds[ $refunded_item_id ]['total']    += abs( (float) $refunded_item->get_total() );
 			}
 		}

@@ -290,7 +289,7 @@ class WC_Admin_List_Table_Orders extends WC_Admin_List_Table {

 			$product_object = is_callable( array( $item, 'get_product' ) ) ? $item->get_product() : null;
 			$row_class      = apply_filters( 'woocommerce_admin_html_order_preview_item_class', '', $item, $order );
-			$refund         = $refunds[ $item->get_product_id() ] ?? null;
+			$refund         = $refunds[ $item->get_id() ] ?? null;

 			$html .= '<tr class="wc-order-preview-table__item wc-order-preview-table__item--' . esc_attr( $item_id ) . ( $row_class ? ' ' . esc_attr( $row_class ) : '' ) . '">';

diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 45179d683ef..9c4f5683eaf 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -4428,16 +4428,10 @@ parameters:
 			count: 1
 			path: includes/admin/list-tables/class-wc-admin-list-table-orders.php

-		-
-			message: '#^Call to an undefined method WC_Order_Item\:\:get_product_id\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: includes/admin/list-tables/class-wc-admin-list-table-orders.php
-
 		-
 			message: '#^Call to an undefined method WC_Order_Item\:\:get_total\(\)\.$#'
 			identifier: method.notFound
-			count: 2
+			count: 1
 			path: includes/admin/list-tables/class-wc-admin-list-table-orders.php

 		-
diff --git a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php
index 09b143ee6b7..4d0de1a760e 100644
--- a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php
@@ -540,6 +540,64 @@ class WC_Admin_List_Table_Orders_Test extends WC_Unit_Test_Case {
 		$this->assertSame( 1, $refunded_nodes->length, 'The preview should pass the refund total to the wc_price filter as a negative amount.' );
 	}

+	/**
+	 * @testdox Order previews show a refund only on the refunded line item ($line_items).
+	 * @testWith ["sibling_variations"]
+	 *           ["same_simple_product"]
+	 *           ["same_variation"]
+	 *
+	 * @param string $line_items Which pair of products to add as separate line items.
+	 */
+	public function test_order_preview_shows_refund_only_on_refunded_line_item( string $line_items ): void {
+		if ( 'same_simple_product' === $line_items ) {
+			$simple_product = WC_Helper_Product::create_simple_product();
+			$products       = array( $simple_product, $simple_product );
+		} else {
+			$variation_ids = WC_Helper_Product::create_variation_product()->get_children();
+			$products      = array(
+				wc_get_product( $variation_ids[0] ),
+				wc_get_product( 'same_variation' === $line_items ? $variation_ids[0] : $variation_ids[1] ),
+			);
+		}
+
+		$order             = wc_create_order();
+		$refunded_item_id  = $order->add_product( $products[0], 2 );
+		$untouched_item_id = $order->add_product( $products[1], 2 );
+		$order->calculate_totals();
+		$order->save();
+
+		wc_create_refund(
+			array(
+				'amount'        => 5,
+				'order_id'      => $order->get_id(),
+				'line_items'    => array(
+					$refunded_item_id => array(
+						'qty'          => 1,
+						'refund_total' => 5,
+						'refund_tax'   => array(),
+					),
+				),
+				'restock_items' => false,
+			)
+		);
+
+		$preview_html = WC_Admin_List_Table_Orders::get_order_preview_item_html( wc_get_order( $order->get_id() ) );
+
+		$document       = new DOMDocument();
+		$previous_state = libxml_use_internal_errors( true );
+		$document->loadHTML( '<!DOCTYPE html><html><body>' . $preview_html . '</body></html>' );
+		libxml_clear_errors();
+		libxml_use_internal_errors( $previous_state );
+		$xpath = new DOMXPath( $document );
+
+		$refunded_qty_nodes = $xpath->query( "//tr[contains(@class, 'wc-order-preview-table__item--{$refunded_item_id}')]/td[contains(@class, 'wc-order-preview-table__column--quantity')]//small[contains(@class, 'refunded')]" );
+		$untouched_nodes    = $xpath->query( "//tr[contains(@class, 'wc-order-preview-table__item--{$untouched_item_id}')]//small[contains(@class, 'refunded')]" );
+
+		$this->assertSame( 1, $refunded_qty_nodes->length, 'The refunded line item should show its refunded quantity.' );
+		$this->assertSame( '-1', trim( $refunded_qty_nodes->item( 0 )->textContent ), 'The refunded quantity should only count the refunded line item.' );
+		$this->assertSame( 0, $untouched_nodes->length, 'A line item that was not refunded should not show a refund.' );
+	}
+
 	/**
 	 * @testdox Should not warn or drop orders when the m parameter is not a string.
 	 */