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.
*/