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' );
+	}
 }