Commit f9b451fa788 for woocommerce

commit f9b451fa78891197ea69454604d376c073a7e8bd
Author: Ján Mikláš <neosinner@gmail.com>
Date:   Thu Aug 27 19:42:40 2026 +0200

    Fix duplicate Analytics customers for manual orders (#67722)

    Viewing the customer history of an email-less manual order under CPT
    storage created a second blank wc_customer_lookup row on every view.
    Use the read-only customer lookup when rendering, so Analytics customers
    are only created by a legitimate import. Adds regression tests and a
    changelog entry.


    Claude-Session: https://claude.ai/code/session_01GU3W7KnYnsMo2dTVk65LPB

    Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-wooplug-4518-duplicate-manual-order-customers b/plugins/woocommerce/changelog/fix-wooplug-4518-duplicate-manual-order-customers
new file mode 100644
index 00000000000..e3574cf441a
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-4518-duplicate-manual-order-customers
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent duplicate customer entries in Analytics after manually creating guest orders without an email address.
diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomerHistory.php b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomerHistory.php
index f8721a53d53..537a2dd1495 100644
--- a/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomerHistory.php
+++ b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomerHistory.php
@@ -3,8 +3,8 @@ declare( strict_types = 1 );

 namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;

+use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
 use Automattic\WooCommerce\Admin\API\Reports\Customers\Query as CustomersQuery;
-use Automattic\WooCommerce\Admin\Overrides\Order as AdminOrder;
 use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
 use Automattic\WooCommerce\Utilities\OrderUtil;
 use WC_Order;
@@ -230,9 +230,7 @@ class CustomerHistory {
 			return 0;
 		}

-		$report_order = $order instanceof AdminOrder ? $order : new AdminOrder( $order->get_id() );
-
-		return (int) $report_order->get_report_customer_id();
+		return (int) CustomersDataStore::get_existing_customer_id_from_order( $order );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php
index 891a412e037..2439bfbe846 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php
@@ -3,8 +3,10 @@ declare( strict_types = 1 );

 namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders\MetaBoxes;

+use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
 use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
 use Automattic\WooCommerce\Admin\Overrides\Order as AdminOrder;
+use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomerHistory;
 use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
 use Automattic\WooCommerce\Utilities\OrderUtil;
@@ -680,6 +682,61 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
 		$this->assertMatchesRegularExpression( '/order-attribution-total-spend">\s*.*100\.00/', $output, 'Should show total spend of 100' );
 	}

+	/**
+	 * @testdox CPT fallback should not duplicate a guest customer without an email address.
+	 */
+	public function test_cpt_fallback_does_not_duplicate_guest_without_email(): void {
+		global $wpdb;
+
+		$this->use_cpt_orders();
+		$this->assertFalse( OrderUtil::custom_orders_table_usage_is_enabled(), 'Test should use CPT order storage.' );
+
+		$order = WC_Helper_Order::create_order( 0 );
+		$order->set_billing_first_name( 'Guest' );
+		$order->set_billing_last_name( 'Customer' );
+		$order->set_billing_email( '' );
+		$order->set_status( OrderStatus::COMPLETED );
+		$order->set_total( 100 );
+		$order->save();
+
+		$this->assertSame( '', $order->get_billing_email( 'edit' ), 'Test order should not have a billing email.' );
+		\WC_Helper_Reports::reset_stats_dbs();
+
+		$new_customer_fired = 0;
+		$callback           = static function () use ( &$new_customer_fired ) {
+			++$new_customer_fired;
+		};
+		add_action( 'woocommerce_analytics_new_customer', $callback );
+
+		try {
+			ob_start();
+			try {
+				$this->sut->output( $order );
+				$output = (string) ob_get_contents();
+			} finally {
+				ob_end_clean();
+			}
+
+			$customer_lookup_table = CustomersDataStore::get_db_table_name();
+			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is provided by the data store.
+			$customers_after_render     = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$customer_lookup_table}" );
+			$new_customers_after_render = $new_customer_fired;
+
+			OrdersStatsDataStore::update( new AdminOrder( $order->get_id() ) );
+
+			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is provided by the data store.
+			$customers_after_import = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$customer_lookup_table}" );
+		} finally {
+			remove_action( 'woocommerce_analytics_new_customer', $callback );
+		}
+
+		$this->assertStringContainsString( 'order-attribution-total-orders', $output, 'Should render the metabox template.' );
+		$this->assertSame( 0, $customers_after_render, 'Rendering customer history should not create an analytics customer.' );
+		$this->assertSame( 0, $new_customers_after_render, 'Rendering customer history should not fire the new-customer action.' );
+		$this->assertSame( 1, $customers_after_import, 'Importing the order should create one analytics customer.' );
+		$this->assertSame( 1, $new_customer_fired, 'Importing the order should fire the new-customer action once.' );
+	}
+
 	/**
 	 * Switches the order data store to CPT for fallback coverage.
 	 */