Commit 5963d78299d for woocommerce

commit 5963d78299d3ee86ae78e47c9eb38e310c06c3a7
Author: Faisal Ahammad <faisalahammad24@gmail.com>
Date:   Fri Jun 26 16:35:10 2026 +0600

    fix(analytics): prevent fatal when plain WC_Order passed to customer DataStore (#64407)

diff --git a/plugins/woocommerce/changelog/fix-64338-order-customer-name-method-fallback b/plugins/woocommerce/changelog/fix-64338-order-customer-name-method-fallback
new file mode 100644
index 00000000000..76d09188572
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-64338-order-customer-name-method-fallback
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix fatal error in Analytics Customers DataStore when a plain WC_Order (rather than the Overrides\Order subclass) is passed to get_or_create_customer_from_order(), and harden date_last_active against a missing date_created by cascading through date_modified and date_paid before falling back to null.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 71ab5275e35..73c8d0183a0 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -41469,59 +41469,7 @@ parameters:
 			count: 3
 			path: src/Admin/API/Reports/Customers/DataStore.php

-		-
-			message: '#^Call to an undefined method object\:\:get_billing_city\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php
-
-		-
-			message: '#^Call to an undefined method object\:\:get_billing_country\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php
-
-		-
-			message: '#^Call to an undefined method object\:\:get_billing_email\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php
-
-		-
-			message: '#^Call to an undefined method object\:\:get_billing_postcode\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php
-
-		-
-			message: '#^Call to an undefined method object\:\:get_billing_state\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php
-
-		-
-			message: '#^Call to an undefined method object\:\:get_customer_first_name\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php
-
-		-
-			message: '#^Call to an undefined method object\:\:get_customer_last_name\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php

-		-
-			message: '#^Call to an undefined method object\:\:get_date_created\(\)\.$#'
-			identifier: method.notFound
-			count: 3
-			path: src/Admin/API/Reports/Customers/DataStore.php
-
-		-
-			message: '#^Call to an undefined method object\:\:get_email\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php

 		-
 			message: '#^Call to an undefined method object\:\:get_id\(\)\.$#'
@@ -41529,17 +41477,7 @@ parameters:
 			count: 1
 			path: src/Admin/API/Reports/Customers/DataStore.php

-		-
-			message: '#^Call to an undefined method object\:\:get_user_id\(\)\.$#'
-			identifier: method.notFound
-			count: 2
-			path: src/Admin/API/Reports/Customers/DataStore.php

-		-
-			message: '#^Call to an undefined method object\:\:get_username\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php

 		-
 			message: '#^Cannot call method get_id\(\) on Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\WC_Order\|WC_Order\|WC_Order_Refund\|false\.$#'
@@ -41547,12 +41485,6 @@ parameters:
 			count: 1
 			path: src/Admin/API/Reports/Customers/DataStore.php

-		-
-			message: '#^Cannot call method get_id\(\) on WC_Order\|WC_Order_Refund\|false\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore\:\:add_sql_query_params\(\) has no return type specified\.$#'
 			identifier: missingType.return
@@ -41673,18 +41605,6 @@ parameters:
 			count: 1
 			path: src/Admin/API/Reports/Customers/DataStore.php

-		-
-			message: '#^Parameter \#1 \$order of static method Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore\:\:get_customer_order_data_and_format\(\) expects object, WC_Order\|WC_Order_Refund\|false given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php
-
-		-
-			message: '#^Parameter \#1 \$order of static method Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore\:\:get_existing_customer_id_from_order\(\) expects object, WC_Order\|WC_Order_Refund\|false given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Admin/API/Reports/Customers/DataStore.php
-
 		-
 			message: '#^Parameter \$order of method Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore\:\:anonymize_customer\(\) has invalid type Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\WC_Order\.$#'
 			identifier: class.notFound
@@ -47823,12 +47743,6 @@ parameters:
 			count: 1
 			path: src/Admin/Overrides/OrderRefund.php

-		-
-			message: '#^Parameter \#1 \$order of static method Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore\:\:get_or_create_customer_from_order\(\) expects object, WC_Order\|WC_Order_Refund\|false given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Admin/Overrides/OrderRefund.php
-
 		-
 			message: '#^Parameter \$item of method Automattic\\WooCommerce\\Admin\\Overrides\\OrderRefund\:\:get_item_cart_tax_amount\(\) has invalid type Automattic\\WooCommerce\\Admin\\Overrides\\WC_Order_Item\.$#'
 			identifier: class.notFound
@@ -47859,12 +47773,6 @@ parameters:
 			count: 1
 			path: src/Admin/Overrides/OrderRefund.php

-		-
-			message: '#^Property Automattic\\WooCommerce\\Admin\\Overrides\\OrderRefund\:\:\$customer_id \(int\) does not accept false\.$#'
-			identifier: assign.propertyType
-			count: 1
-			path: src/Admin/Overrides/OrderRefund.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgrader\:\:install\(\) has invalid return type Automattic\\WooCommerce\\Admin\\Overrides\\WP_Error\.$#'
 			identifier: class.notFound
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php
index ab840db60f9..0f21c5ec573 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php
@@ -12,6 +12,7 @@ use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
 use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
 use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
 use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
+use Automattic\WooCommerce\Admin\Overrides\Order as OverridesOrder;
 use Automattic\WooCommerce\Utilities\OrderUtil;

 /**
@@ -191,7 +192,10 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 			return -1;
 		}

-		$order       = wc_get_order( $post_id );
+		$order = wc_get_order( $post_id );
+		if ( ! $order instanceof \WC_Order ) {
+			return -1;
+		}
 		$customer_id = self::get_existing_customer_id_from_order( $order );
 		if ( false === $customer_id ) {
 			return -1;
@@ -652,7 +656,10 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 	/**
 	 * Get or create a customer from a given order.
 	 *
-	 * @param object $order WC Order.
+	 * A plain WC_Order will be converted to an Overrides\Order internally
+	 * to ensure consistent name resolution (user meta → billing → shipping fallback).
+	 *
+	 * @param \WC_Order $order WC Order.
 	 * @return int|bool
 	 */
 	public static function get_or_create_customer_from_order( $order ) {
@@ -666,6 +673,13 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 			return false;
 		}

+		if ( ! $order instanceof OverridesOrder ) {
+			if ( ! $order->get_id() ) {
+				return false;
+			}
+			$order = new OverridesOrder( $order->get_id() );
+		}
+
 		$returning_customer_id = self::get_existing_customer_id_from_order( $order );

 		if ( $returning_customer_id ) {
@@ -691,11 +705,29 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 	/**
 	 * Returns a data object and format object of the customers data coming from the order.
 	 *
-	 * @param object      $order         WC_Order where we get customer info from.
-	 * @param object|null $customer_user WC_Customer registered customer WP user.
+	 * A plain WC_Order will be converted to an Overrides\Order internally
+	 * to ensure consistent name resolution (user meta → billing → shipping fallback).
+	 *
+	 * @param \WC_Order         $order         WC_Order where we get customer info from.
+	 * @param \WC_Customer|null $customer_user WC_Customer registered customer WP user.
 	 * @return array ($data, $format)
 	 */
 	public static function get_customer_order_data_and_format( $order, $customer_user = null ) {
+		if ( ! is_a( $order, 'WC_Order' ) ) {
+			return array( array(), array() );
+		}
+
+		if ( ! $order instanceof OverridesOrder ) {
+			if ( ! $order->get_id() ) {
+				return array( array(), array() );
+			}
+			$order = new OverridesOrder( $order->get_id() );
+		}
+
+		$date_created = $order->get_date_created( 'edit' )
+			?? $order->get_date_modified( 'edit' )
+			?? $order->get_date_paid( 'edit' );
+
 		$data   = array(
 			'first_name'       => $order->get_customer_first_name(),
 			'last_name'        => $order->get_customer_last_name(),
@@ -704,7 +736,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 			'state'            => $order->get_billing_state( 'edit' ),
 			'postcode'         => $order->get_billing_postcode( 'edit' ),
 			'country'          => $order->get_billing_country( 'edit' ),
-			'date_last_active' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
+			'date_last_active' => $date_created ? gmdate( 'Y-m-d H:i:s', $date_created->getTimestamp() ) : null,
 		);
 		$format = array(
 			'%s',
diff --git a/plugins/woocommerce/src/Admin/Overrides/OrderRefund.php b/plugins/woocommerce/src/Admin/Overrides/OrderRefund.php
index 68bec74eeba..c30451810a7 100644
--- a/plugins/woocommerce/src/Admin/Overrides/OrderRefund.php
+++ b/plugins/woocommerce/src/Admin/Overrides/OrderRefund.php
@@ -61,11 +61,9 @@ class OrderRefund extends \WC_Order_Refund {
 		if ( is_null( $this->customer_id ) ) {
 			$parent_order = \wc_get_order( $this->get_parent_id() );

-			if ( ! $parent_order ) {
-				$this->customer_id = false;
-			}
-
-			$this->customer_id = CustomersDataStore::get_or_create_customer_from_order( $parent_order );
+			$this->customer_id = $parent_order instanceof \WC_Order
+				? CustomersDataStore::get_or_create_customer_from_order( $parent_order )
+				: false;
 		}

 		return $this->customer_id;
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
index ed0c99d2617..aa35d3fddac 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
@@ -781,6 +781,222 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
 		$this->assertEquals( '54321', $reports[ $second_customer_index ]['postcode'] );
 	}

+	/**
+	 * Test that get_or_create_customer_from_order works with a plain WC_Order (not Overrides\Order).
+	 *
+	 * Bug condition: When a plain WC_Order is passed, get_customer_first_name() does not exist,
+	 * causing a fatal error. The fix should convert to Overrides\Order internally.
+	 *
+	 * Validates: Requirements 1.1, 1.2
+	 */
+	public function test_get_or_create_customer_from_plain_wc_order() {
+		// Remove the filter that converts WC_Order to Overrides\Order so we get a plain WC_Order.
+		remove_filter( 'woocommerce_order_class', array( \Automattic\WooCommerce\Admin\Overrides\Order::class, 'order_class_name' ), 10 );
+
+		$order = new \WC_Order();
+		$order->set_billing_first_name( 'Plain' );
+		$order->set_billing_last_name( 'Order' );
+		$order->set_billing_email( 'plain.order@example.com' );
+		$order->set_date_created( time() );
+		$order->save();
+
+		// Restore the filter.
+		add_filter( 'woocommerce_order_class', array( \Automattic\WooCommerce\Admin\Overrides\Order::class, 'order_class_name' ), 10, 3 );
+
+		$customer_id = CustomersDataStore::get_or_create_customer_from_order( $order );
+
+		$this->assertIsInt( $customer_id );
+		$this->assertGreaterThan( 0, $customer_id );
+
+		// Verify the converted order resolved billing names via Overrides\Order.
+		global $wpdb;
+		$table_name = $wpdb->prefix . 'wc_customer_lookup';
+		$record     = $wpdb->get_row(
+			$wpdb->prepare( "SELECT * FROM {$table_name} WHERE customer_id = %d", $customer_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+		);
+
+		$this->assertEquals( 'Plain', $record->first_name );
+		$this->assertEquals( 'Order', $record->last_name );
+		$this->assertEquals( 'plain.order@example.com', $record->email );
+		$this->assertNotNull( $record->date_last_active );
+	}
+
+	/**
+	 * Test that get_or_create_customer_from_order returns false for unsaved orders.
+	 *
+	 * Bug condition: An unsaved order has get_id() === 0. Constructing
+	 * new OverridesOrder( 0 ) returns an empty order, which would write
+	 * a blank customer row.
+	 */
+	public function test_get_or_create_customer_from_unsaved_order() {
+		// Remove the filter that converts WC_Order to Overrides\Order so we get a plain WC_Order.
+		remove_filter( 'woocommerce_order_class', array( \Automattic\WooCommerce\Admin\Overrides\Order::class, 'order_class_name' ), 10 );
+
+		$order = new \WC_Order();
+		$order->set_billing_first_name( 'Unsaved' );
+		$order->set_billing_last_name( 'Order' );
+		$order->set_billing_email( 'unsaved@example.com' );
+		// Do NOT call save() — order has no ID yet.
+
+		$result = CustomersDataStore::get_or_create_customer_from_order( $order );
+
+		$this->assertFalse( $result );
+
+		// Restore the filter.
+		add_filter( 'woocommerce_order_class', array( \Automattic\WooCommerce\Admin\Overrides\Order::class, 'order_class_name' ), 10, 3 );
+	}
+
+	/**
+	 * Test that get_customer_order_data_and_format handles null date_created without fatal.
+	 *
+	 * Bug condition: When get_date_created('edit') returns null, calling ->getTimestamp() on null
+	 * causes a fatal error. The fix should handle null dates gracefully.
+	 *
+	 * Validates: Requirements 1.3
+	 */
+	public function test_get_customer_order_data_with_null_date_created() {
+		$order = new \Automattic\WooCommerce\Admin\Overrides\Order();
+		$order->set_billing_first_name( 'NullDate' );
+		$order->set_billing_last_name( 'Test' );
+		$order->set_billing_email( 'nulldate@example.com' );
+		$order->save();
+
+		// Force all date fields to null after save (simulates edge case / corrupted data).
+		// Since the order is already an OverridesOrder, the instanceof check passes
+		// and no re-instantiation from DB occurs.
+		$order->set_date_created( null );
+		$order->set_date_modified( null );
+
+		// This should not fatal — date_last_active should be null when all dates are null.
+		list( $data, $format ) = CustomersDataStore::get_customer_order_data_and_format( $order );
+
+		$this->assertNull( $data['date_last_active'] );
+	}
+
+	/**
+	 * Test that sync_order_customer handles a non-existent order ID without fatal.
+	 *
+	 * Bug condition: When wc_get_order() returns false for a non-existent order,
+	 * the code proceeds to call methods on false, causing a fatal error.
+	 * The fix should return -1 gracefully.
+	 *
+	 * Validates: Requirements 1.2
+	 */
+	public function test_sync_order_customer_with_nonexistent_order() {
+		// Use a very high order ID that doesn't exist.
+		$result = CustomersDataStore::sync_order_customer( 999999 );
+
+		$this->assertEquals( -1, $result );
+	}
+
+	/**
+	 * Test that get_or_create_customer_from_order preserves correct behavior with Overrides\Order.
+	 *
+	 * Preservation: Overrides\Order with billing name and valid date_created produces
+	 * a customer record with correct first_name, last_name, and date_last_active.
+	 *
+	 * Validates: Requirements 3.1, 3.2
+	 */
+	public function test_overrides_order_customer_creation_preserved() {
+		$order = new \Automattic\WooCommerce\Admin\Overrides\Order();
+		$order->set_billing_first_name( 'Preserved' );
+		$order->set_billing_last_name( 'Customer' );
+		$order->set_billing_email( 'preserved.customer@example.com' );
+		$order->set_date_created( time() );
+		$order->save();
+
+		$customer_id = CustomersDataStore::get_or_create_customer_from_order( $order );
+
+		// Returns an int customer ID.
+		$this->assertIsInt( $customer_id );
+		$this->assertGreaterThan( 0, $customer_id );
+
+		// Verify customer record has correct first_name/last_name (from billing since no user_id).
+		global $wpdb;
+		$table_name = $wpdb->prefix . 'wc_customer_lookup';
+		$record     = $wpdb->get_row(
+			$wpdb->prepare( "SELECT * FROM {$table_name} WHERE customer_id = %d", $customer_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+		);
+
+		$this->assertEquals( 'Preserved', $record->first_name );
+		$this->assertEquals( 'Customer', $record->last_name );
+
+		// date_last_active matches the order's date_created formatted as Y-m-d H:i:s.
+		$expected_date = gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() );
+		$this->assertEquals( $expected_date, $record->date_last_active );
+	}
+
+	/**
+	 * Test that get_or_create_customer_from_order preserves correct behavior with a registered user.
+	 *
+	 * Preservation: Overrides\Order with a registered user produces a customer record
+	 * with correct user_id and username from the WC_Customer.
+	 *
+	 * Validates: Requirements 3.3
+	 */
+	public function test_overrides_order_registered_user_preserved() {
+		// Create a WordPress user to associate with the order.
+		$user_id = wp_insert_user(
+			array(
+				'user_login' => 'preserveduser',
+				'user_pass'  => 'password',
+				'user_email' => 'preserveduser@example.com',
+				'first_name' => 'RegFirst',
+				'last_name'  => 'RegLast',
+				'role'       => 'customer',
+			)
+		);
+
+		$order = new \Automattic\WooCommerce\Admin\Overrides\Order();
+		$order->set_customer_id( $user_id );
+		$order->set_billing_first_name( 'BillingFirst' );
+		$order->set_billing_last_name( 'BillingLast' );
+		$order->set_billing_email( 'preserveduser@example.com' );
+		$order->set_date_created( time() );
+		$order->save();
+
+		$customer_id = CustomersDataStore::get_or_create_customer_from_order( $order );
+
+		// Returns an int customer ID.
+		$this->assertIsInt( $customer_id );
+		$this->assertGreaterThan( 0, $customer_id );
+
+		// Verify customer record has correct user_id and username from WC_Customer.
+		global $wpdb;
+		$table_name = $wpdb->prefix . 'wc_customer_lookup';
+		$record     = $wpdb->get_row(
+			$wpdb->prepare( "SELECT * FROM {$table_name} WHERE customer_id = %d", $customer_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+		);
+
+		$this->assertEquals( $user_id, (int) $record->user_id );
+		$this->assertEquals( 'preserveduser', $record->username );
+	}
+
+	/**
+	 * Test that calling get_or_create_customer_from_order twice for the same order
+	 * returns the same customer_id without creating a duplicate.
+	 *
+	 * Preservation: Existing customer record for same order returns existing customer_id.
+	 *
+	 * Validates: Requirements 3.4
+	 */
+	public function test_existing_customer_not_duplicated() {
+		$order = new \Automattic\WooCommerce\Admin\Overrides\Order();
+		$order->set_billing_first_name( 'NoDupe' );
+		$order->set_billing_last_name( 'Test' );
+		$order->set_billing_email( 'nodupe@example.com' );
+		$order->set_date_created( time() );
+		$order->save();
+
+		$customer_id_first  = CustomersDataStore::get_or_create_customer_from_order( $order );
+		$customer_id_second = CustomersDataStore::get_or_create_customer_from_order( $order );
+
+		// Both calls return the same customer_id (no duplicate created).
+		$this->assertIsInt( $customer_id_first );
+		$this->assertIsInt( $customer_id_second );
+		$this->assertEquals( $customer_id_first, $customer_id_second );
+	}
+
 	/**
 	 * Test get_last_order.
 	 */
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-customers.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-customers.php
index 14d3bed5b78..0e77c8adff4 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-customers.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-customers.php
@@ -31,7 +31,8 @@ class WC_Admin_Tests_Reports_Customer extends WC_Unit_Test_Case {

 		WC_Helper_Queue::run_all_pending( 'wc-admin-data' );

-		$customer_id = DataStore::get_customer_id_by_user_id( $customer->get_id() ); // This is the customer ID from lookup table.
+		$customer_id = DataStore::get_customer_id_by_user_id( $customer->get_id() );
+		// This is the customer ID from lookup table.

 		// Create 3 orders.
 		foreach ( range( 1, 3 ) as $i ) {
@@ -87,7 +88,8 @@ class WC_Admin_Tests_Reports_Customer extends WC_Unit_Test_Case {

 		WC_Helper_Queue::run_all_pending( 'wc-admin-data' );

-		$customer_id = DataStore::get_customer_id_by_user_id( $customer->get_id() ); // This is the customer ID from lookup table.
+		$customer_id = DataStore::get_customer_id_by_user_id( $customer->get_id() );
+		// This is the customer ID from lookup table.

 		// Customer should remain in lookup table after first order deleted.
 		$order1->delete( true );
@@ -193,6 +195,67 @@ class WC_Admin_Tests_Reports_Customer extends WC_Unit_Test_Case {
 		$this->assertNull( $rows[0]->user_id, 'Guest row user_id should remain NULL for normal registration.' );
 	}

+	/**
+	 * Test that get_or_create_customer_from_order() handles a plain WC_Order
+	 * (not the Overrides\Order subclass) without a fatal error.
+	 *
+	 * This is a regression test for the scenario described in issue #64338,
+	 * where get_customer_first_name/get_customer_last_name do not exist on plain WC_Order.
+	 *
+	 * @covers \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_or_create_customer_from_order
+	 */
+	public function test_get_or_create_customer_from_plain_wc_order() {
+		WC_Helper_Reports::reset_stats_dbs();
+
+		// Build a plain WC_Order directly, bypassing the woocommerce_order_class filter
+		// that normally swaps in the Overrides\Order subclass. This is the exact path
+		// that produced the production fatal.
+		$order = new WC_Order();
+		$order->set_billing_first_name( 'Jane' );
+		$order->set_billing_last_name( 'Doe' );
+		$order->set_billing_email( 'jane.doe.plain@example.com' );
+		$order->set_status( 'completed' );
+		$order->save();
+
+		$customer_id = \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_or_create_customer_from_order( $order );
+
+		$this->assertNotFalse( $customer_id, 'Should return a customer ID for a plain WC_Order without fataling.' );
+		$this->assertIsInt( $customer_id, 'Returned customer ID should be an integer.' );
+	}
+
+	/**
+	 * Test that get_customer_order_data_and_format() cascades through date_modified
+	 * and date_paid when date_created is null, and ultimately falls back to null
+	 * rather than stamping an incorrect current timestamp.
+	 *
+	 * @covers \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_customer_order_data_and_format
+	 */
+	public function test_date_last_active_falls_back_when_date_created_is_null() {
+		$order = $this->createMock( \Automattic\WooCommerce\Admin\Overrides\Order::class );
+		$order->method( 'get_id' )->willReturn( 0 );
+		$order->method( 'get_date_created' )->willReturn( null );
+		$order->method( 'get_date_modified' )->willReturn( null );
+		$order->method( 'get_date_paid' )->willReturn( null );
+		$order->method( 'get_user_id' )->willReturn( 0 );
+		$order->method( 'get_billing_email' )->willReturn( 'test@example.com' );
+		$order->method( 'get_billing_first_name' )->willReturn( 'Test' );
+		$order->method( 'get_billing_last_name' )->willReturn( 'User' );
+		$order->method( 'get_billing_city' )->willReturn( '' );
+		$order->method( 'get_billing_state' )->willReturn( '' );
+		$order->method( 'get_billing_postcode' )->willReturn( '' );
+		$order->method( 'get_billing_country' )->willReturn( '' );
+		$order->method( 'get_customer_first_name' )->willReturn( 'Test' );
+		$order->method( 'get_customer_last_name' )->willReturn( 'User' );
+
+		list( $data, ) = \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_customer_order_data_and_format( $order );
+
+		$this->assertNull(
+			$data['date_last_active'],
+			'date_last_active must be null (not the current timestamp) when all date fields are null.'
+		);
+	}
+
+
 	/**
 	 * Get a customer's record from the database.
 	 *