Commit 23e622e830f for woocommerce
commit 23e622e830fe046cd17410dd45ff89bc97e2f9f2
Author: Ján Mikláš <neosinner@gmail.com>
Date: Wed Aug 12 10:20:31 2026 +0200
Fix refund customer type being clobbered by first order recalculation in order stats (#67638)
* Fix refund customer type clobbered when first order is recalculated
* Add changelog entry for refund customer type fix
diff --git a/plugins/woocommerce/changelog/fix-refund-returning-customer-clobbered-on-first-order-recalc b/plugins/woocommerce/changelog/fix-refund-returning-customer-clobbered-on-first-order-recalc
new file mode 100644
index 00000000000..51f7ecb8e0e
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-refund-returning-customer-clobbered-on-first-order-recalc
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep refund rows' returning_customer NULL when a customer's first order is recalculated, so Analytics > Orders keeps reporting refunds with the refunded order's customer type
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
index 3d9c22612a7..6226f26bad7 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
@@ -906,10 +906,13 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
global $wpdb;
$orders_stats_table = self::get_db_table_name();
+ // Refund rows share the customer ID of their parent order but are stored with a NULL
+ // returning_customer, which the orders report relies on to fall back to the refunded
+ // order's value. Keep them NULL by only updating rows that carry their own flag.
$wpdb->query(
$wpdb->prepare(
- 'UPDATE %i SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d',
- $orders_stats_table,
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name cannot be prepared.
+ "UPDATE {$orders_stats_table} SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d AND returning_customer IS NOT NULL",
$order_id,
$customer_id
)
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders.php
index 4855a8e9351..d6db1a193c1 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders.php
@@ -501,4 +501,48 @@ class WC_Admin_Tests_Reports_Orders extends WC_Unit_Test_Case {
$this->assertEqualSets( array( $order->get_id() ), array_keys( $order_rows ), 'Excluding refunds should return the order only' );
}
+
+ /**
+ * Recalculating a customer's first order runs an UPDATE across all of the customer's stats
+ * rows, which must not overwrite the NULL returning_customer of refund rows — the report
+ * relies on that NULL to fall back to the refunded order's customer type.
+ *
+ * @testdox Should keep reporting a refund with the refunded order's customer type after the customer's first order is recalculated.
+ */
+ public function test_refund_keeps_customer_type_after_first_order_recalculation() {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $simple_product = new WC_Product_Simple();
+ $simple_product->set_name( 'Simple Product' );
+ $simple_product->set_regular_price( 25 );
+ $simple_product->save();
+
+ $first_order = $this->create_guest_order( $simple_product, 'guest-33410-recalc@example.org' );
+ $second_order = $this->create_guest_order( $simple_product, 'guest-33410-recalc@example.org' );
+
+ $refund = wc_create_refund(
+ array(
+ 'amount' => 25,
+ 'order_id' => $second_order->get_id(),
+ )
+ );
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ // Cancelling the first order makes the second order the customer's first order,
+ // triggering the returning_customer recalculation across the customer's rows.
+ $first_order->set_status( OrderStatus::CANCELLED );
+ $first_order->save();
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $customer_types = $this->get_customer_types_by_order_id( $second_order );
+
+ $this->assertEquals( 'new', $customer_types[ $second_order->get_id() ], 'The second order should be reported as new once the first order is cancelled' );
+ $this->assertEquals( 'new', $customer_types[ $refund->get_id() ], 'A refund should keep reporting the customer type of the refunded order after the recalculation' );
+
+ $returning_customer_rows = $this->get_customer_types_by_order_id( $second_order, array( 'customer_type' => 'returning' ) );
+
+ $this->assertEmpty( $returning_customer_rows, 'Filtering by returning customers should not match the refund after the recalculation' );
+ }
}