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