Commit 450556248bf for woocommerce

commit 450556248bf0abb3e076dc822486de0b60b18665
Author: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Date:   Mon Mar 9 10:33:25 2026 +0800

    Exclude test orders from analytics data (#63550)

    * feat: exclude test orders from analytics data

    Add is_test_order() check in OrdersScheduler::import() and
    OrdersStatsDataStore::update() to prevent WCPay test orders
    (with _wcpay_mode = 'test') from entering analytics lookup tables.

    Extensible via woocommerce_analytics_is_test_order filter.
    Refund orders check their parent order's meta.

    WOOA7S-1069

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Update phpstan-baseline.neon

    * fix: update PHPDoc param descriptions for get_num_items_sold and get_net_total

    * fix: suppress PHPCS MissingShort for inline PHPStan type hint

    * fix: add short description to inline @var docblock to fix PHPCS lint

    * fix: update phpstan-baseline.neon for resolved DataStore type errors

    * fix: add use stdClass import to resolve PHPStan namespace errors

    * Regenerate baseline

    * fix: address code review feedback for test order exclusion

    - Add @since 10.7.0 and refund behavior docs to is_test_order()
    - Fix filter @since placement and add use-case context
    - Add debug logging when test orders are skipped
    - Add defense-in-depth comment in DataStore::update()
    - Convert test docblocks to @testdox with void return types
    - Add integration tests for DataStore update/sync_order guards
    - Add orphan refund and filter parent order contract tests

    * fix: add instanceof guard in is_test_order()

    Validate input is WC_Abstract_Order before dereferencing,
    and use instanceof for wc_get_order() return check.

    * fix: resolve PHPCS lint errors in DataStore and OrdersSchedulerTest

    * Fix lint

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/63550-wooa7s-1069-remove-test-orders-from-analytics-data b/plugins/woocommerce/changelog/63550-wooa7s-1069-remove-test-orders-from-analytics-data
new file mode 100644
index 00000000000..c1ae907e318
--- /dev/null
+++ b/plugins/woocommerce/changelog/63550-wooa7s-1069-remove-test-orders-from-analytics-data
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Exclude WCPay test orders from analytics data to prevent test transactions from polluting reports.
\ No newline at end of file
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 1285a474439..d1bb250ae20 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -19146,7 +19146,6 @@ parameters:
 			count: 1
 			path: includes/emails/class-wc-email-customer-pos-refunded-order.php

-
 		-
 			message: '#^Method WC_Email_Customer_Processing_Order\:\:trigger\(\) has no return type specified\.$#'
 			identifier: missingType.return
@@ -43732,114 +43731,18 @@ parameters:
 			count: 4
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php

-		-
-			message: '#^@param WC_Order \$order does not accept actual type of parameter\: Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\|WC_Order\.$#'
-			identifier: parameter.phpDocType
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
 		-
 			message: '#^Access to property \$intervals on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WP_Error\.$#'
 			identifier: class.notFound
 			count: 3
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php

-		-
-			message: '#^Access to property \$intervals on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\stdClass\.$#'
-			identifier: class.notFound
-			count: 7
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Access to property \$totals on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\stdClass\.$#'
-			identifier: class.notFound
-			count: 2
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
 		-
 			message: '#^Binary operation "\*" between \-1 and string results in an error\.$#'
 			identifier: binaryOp.invalid
 			count: 1
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php

-		-
-			message: '#^Call to method get_date_completed\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 2
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_date_created\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 3
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_date_paid\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 2
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_id\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 3
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_meta\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_parent_id\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 2
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_report_customer_id\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_shipping_total\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_status\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_total\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_total_tax\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method get_type\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Call to method is_returning_customer\(\) on an unknown class Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:assign_report_columns\(\) has no return type specified\.$#'
 			identifier: missingType.return
@@ -43865,7 +43768,7 @@ parameters:
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php

 		-
-			message: '#^Method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_noncached_data\(\) should return Automattic\\WooCommerce\\Admin\\API\\Reports\\stdClass\|Automattic\\WooCommerce\\Admin\\API\\Reports\\WP_Error but returns Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\stdClass\|Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WP_Error\|\(object\{totals\: null, intervals\: array, total\: int, pages\: int, page_no\: int\}&stdClass\)\.$#'
+			message: '#^Method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_noncached_data\(\) should return Automattic\\WooCommerce\\Admin\\API\\Reports\\stdClass\|Automattic\\WooCommerce\\Admin\\API\\Reports\\WP_Error but returns Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WP_Error\|stdClass\.$#'
 			identifier: return.type
 			count: 1
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
@@ -43877,19 +43780,13 @@ parameters:
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php

 		-
-			message: '#^Method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_noncached_stats_data\(\) has invalid return type Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\stdClass\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_noncached_stats_data\(\) should return Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\stdClass\|Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WP_Error but returns Automattic\\WooCommerce\\Admin\\API\\Reports\\stdClass\.$#'
+			message: '#^Method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_noncached_stats_data\(\) should return Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WP_Error\|stdClass but returns Automattic\\WooCommerce\\Admin\\API\\Reports\\stdClass\.$#'
 			identifier: return.type
 			count: 1
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php

 		-
-			message: '#^Method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_noncached_stats_data\(\) should return Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\stdClass\|Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WP_Error but returns WP_Error\.$#'
+			message: '#^Method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_noncached_stats_data\(\) should return Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WP_Error\|stdClass but returns WP_Error\.$#'
 			identifier: return.type
 			count: 2
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
@@ -43937,7 +43834,7 @@ parameters:
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php

 		-
-			message: '#^Parameter \#1 \$data of method Automattic\\WooCommerce\\Admin\\API\\Reports\\Segmenter\:\:add_intervals_segments\(\) expects Automattic\\WooCommerce\\Admin\\API\\Reports\\stdClass, Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\stdClass\|Automattic\\WooCommerce\\Admin\\API\\Reports\\stdClass given\.$#'
+			message: '#^Parameter \#1 \$data of method Automattic\\WooCommerce\\Admin\\API\\Reports\\Segmenter\:\:add_intervals_segments\(\) expects Automattic\\WooCommerce\\Admin\\API\\Reports\\stdClass, Automattic\\WooCommerce\\Admin\\API\\Reports\\stdClass\|stdClass given\.$#'
 			identifier: argument.type
 			count: 1
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
@@ -43949,59 +43846,11 @@ parameters:
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php

 		-
-			message: '#^Parameter \#1 \$order of static method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_net_total\(\) expects WC_Order, Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\|WC_Order given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Parameter \#1 \$order of static method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_net_total\(\) expects WC_Order, WC_Order\|WC_Order_Refund given\.$#'
+			message: '#^Parameter \#5 \$data of method Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStore\:\:fill_in_missing_intervals\(\) expects Automattic\\WooCommerce\\Admin\\API\\Reports\\stdClass, stdClass given\.$#'
 			identifier: argument.type
 			count: 1
 			path: src/Admin/API/Reports/Orders/Stats/DataStore.php

-		-
-			message: '#^Parameter \#1 \$order of static method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_num_items_sold\(\) expects WC_Order, Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\|WC_Order given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Parameter \#1 \$order of static method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_num_items_sold\(\) expects WC_Order, WC_Order\|WC_Order_Refund given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Parameter \#1 \$order of static method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:update\(\) expects Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\|WC_Order, WC_Order\|WC_Order_Refund given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Parameter \#3 \$data of method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_noncached_stats_data\(\) expects Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\stdClass, object\{totals\: null, intervals\: array, total\: int, pages\: int, page_no\: int\}&stdClass given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Parameter \#5 \$data of method Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStore\:\:fill_in_missing_intervals\(\) expects Automattic\\WooCommerce\\Admin\\API\\Reports\\stdClass, Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\stdClass given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Parameter \$data of method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:get_noncached_stats_data\(\) has invalid type Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\stdClass\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
-		-
-			message: '#^Parameter \$order of method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore\:\:update\(\) has invalid type Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\WC_Order_Refund\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/Stats/DataStore.php
-
 		-
 			message: '#^Variable \$segments might not be defined\.$#'
 			identifier: variable.undefined
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 ab450f144ec..93e6cadadc6 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
@@ -14,10 +14,15 @@ 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\API\Reports\Customers\DataStore as CustomersDataStore;
+use Automattic\WooCommerce\Internal\Admin\Schedulers\OrdersScheduler;
 use Automattic\WooCommerce\Utilities\OrderUtil;
 use Automattic\WooCommerce\Admin\API\Reports\StatsDataStoreTrait;
 use Automattic\WooCommerce\Utilities\FeaturesUtil;
+use stdClass;
 use WC_Order;
+use WC_Order_Refund;
+use Automattic\WooCommerce\Admin\Overrides\Order;
+use Automattic\WooCommerce\Admin\Overrides\OrderRefund;

 /**
  * API\Reports\Orders\Stats\DataStore.
@@ -501,6 +506,11 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 			return -1;
 		}

+		/**
+		 * The order instance to be synchronized.
+		 *
+		 * @var false|Order|OrderRefund $order Order instance (Override classes registered via OrdersScheduler::init()).
+		 */
 		$order = wc_get_order( $post_id );
 		if ( ! $order ) {
 			return -1;
@@ -512,7 +522,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 	/**
 	 * Update the database with stats data.
 	 *
-	 * @param WC_Order|WC_Order_Refund $order Order or refund to update row for.
+	 * @param Order|OrderRefund $order Order or refund to update row for.
 	 * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
 	 */
 	public static function update( $order ) {
@@ -523,6 +533,13 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 			return -1;
 		}

+		// Exclude test orders (e.g., WCPay test mode) from analytics stats.
+		// Defense-in-depth: also checked in OrdersScheduler::import(), but
+		// update() can be called directly outside of the import flow.
+		if ( OrdersScheduler::is_test_order( $order ) ) {
+			return -1;
+		}
+
 		$format = array(
 			'%d',
 			'%d',
@@ -568,7 +585,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 		 * Filters order stats data.
 		 *
 		 * @param array $data Data written to order stats lookup table.
-		 * @param WC_Order $order  Order object.
+		 * @param Order|OrderRefund $order  Order object.
 		 *
 		 * @since 4.0.0
 		 */
@@ -653,7 +670,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 	/**
 	 * Get number of items sold among all orders.
 	 *
-	 * @param WC_Order $order WC_Order object.
+	 * @param WC_Order|WC_Order_Refund $order WC_Order or WC_Order_Refund object.
 	 * @return int
 	 */
 	protected static function get_num_items_sold( $order ) {
@@ -670,7 +687,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 	/**
 	 * Get the net amount from an order without shipping, tax, or refunds.
 	 *
-	 * @param WC_Order $order WC_Order object.
+	 * @param WC_Order|WC_Order_Refund $order WC_Order or WC_Order_Refund object.
 	 * @return float
 	 */
 	protected static function get_net_total( $order ) {
diff --git a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
index c6fa2384b2d..577f57c7746 100644
--- a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
+++ b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
@@ -346,6 +346,15 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
 			return;
 		}

+		// Skip test orders (e.g., WCPay test mode) from analytics.
+		if ( self::is_test_order( $order ) ) {
+			wc_get_logger()->debug(
+				sprintf( 'Skipping test order #%d from analytics import.', $order_id ),
+				array( 'source' => 'wc-analytics-order-import' )
+			);
+			return;
+		}
+
 		$results = array(
 			OrdersStatsDataStore::sync_order( $order_id ),
 			ProductsDataStore::sync_order_products( $order_id ),
@@ -684,6 +693,48 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
 		);
 	}

+	/**
+	 * Check if an order is a test order that should be excluded from analytics.
+	 *
+	 * For refunds, the parent order is checked instead, since refunds do not
+	 * carry the test mode metadata directly.
+	 *
+	 * @param \WC_Abstract_Order $order Order object.
+	 * @return bool
+	 *
+	 * @since 10.7.0
+	 */
+	public static function is_test_order( $order ) {
+		if ( ! $order instanceof \WC_Abstract_Order ) {
+			return false;
+		}
+
+		// For refunds, check the parent order.
+		$check_order = $order;
+		if ( 'shop_order_refund' === $order->get_type() ) {
+			$check_order = wc_get_order( $order->get_parent_id() );
+			if ( ! $check_order instanceof \WC_Abstract_Order ) {
+				return false;
+			}
+		}
+
+		$is_test = 'test' === $check_order->get_meta( '_wcpay_mode' );
+
+		/**
+		 * Filter whether an order is a test order excluded from analytics.
+		 *
+		 * Use this filter to customize test order detection beyond the default
+		 * WCPay test mode check, e.g., to exclude orders from other payment
+		 * gateways' test/sandbox modes.
+		 *
+		 * @param bool               $is_test Whether the order is a test order.
+		 * @param \WC_Abstract_Order $order   The order being checked (for refunds, this is the parent order).
+		 *
+		 * @since 10.7.0
+		 */
+		return apply_filters( 'woocommerce_analytics_is_test_order', $is_test, $check_order );
+	}
+
 	/**
 	 * Delete a batch of orders.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Schedulers/OrdersSchedulerTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Schedulers/OrdersSchedulerTest.php
index a92b32ca4a9..f9191930378 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Schedulers/OrdersSchedulerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Schedulers/OrdersSchedulerTest.php
@@ -4,6 +4,7 @@ declare( strict_types=1 );
 namespace Automattic\WooCommerce\Tests\Internal\Admin\Schedulers;

 use Automattic\WooCommerce\Internal\Admin\Schedulers\OrdersScheduler;
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
 use WC_Unit_Test_Case;
 use Automattic\WooCommerce\Admin\Features\Features;

@@ -205,6 +206,187 @@ class OrdersSchedulerTest extends WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * @testdox Should identify order with _wcpay_mode test as a test order.
+	 */
+	public function test_is_test_order_with_wcpay_test_mode(): void {
+		$order = \WC_Helper_Order::create_order();
+		$order->update_meta_data( '_wcpay_mode', 'test' );
+		$order->save();
+
+		$this->assertTrue( OrdersScheduler::is_test_order( $order ) );
+	}
+
+	/**
+	 * @testdox Should not identify a normal order as a test order.
+	 */
+	public function test_is_test_order_with_normal_order(): void {
+		$order = \WC_Helper_Order::create_order();
+
+		$this->assertFalse( OrdersScheduler::is_test_order( $order ) );
+	}
+
+	/**
+	 * @testdox Should not identify an order with _wcpay_mode live as a test order.
+	 */
+	public function test_is_test_order_with_wcpay_live_mode(): void {
+		$order = \WC_Helper_Order::create_order();
+		$order->update_meta_data( '_wcpay_mode', 'live' );
+		$order->save();
+
+		$this->assertFalse( OrdersScheduler::is_test_order( $order ) );
+	}
+
+	/**
+	 * @testdox Should identify a refund of a test order as a test order.
+	 */
+	public function test_is_test_order_with_refund_of_test_order(): void {
+		$order = \WC_Helper_Order::create_order();
+		$order->update_meta_data( '_wcpay_mode', 'test' );
+		$order->save();
+
+		$refund = wc_create_refund(
+			array(
+				'order_id' => $order->get_id(),
+				'amount'   => 10,
+				'reason'   => 'Test refund',
+			)
+		);
+
+		$this->assertTrue( OrdersScheduler::is_test_order( $refund ) );
+	}
+
+	/**
+	 * @testdox Should not identify a refund of a normal order as a test order.
+	 */
+	public function test_is_test_order_with_refund_of_normal_order(): void {
+		$order = \WC_Helper_Order::create_order();
+
+		$refund = wc_create_refund(
+			array(
+				'order_id' => $order->get_id(),
+				'amount'   => 10,
+				'reason'   => 'Test refund',
+			)
+		);
+
+		$this->assertFalse( OrdersScheduler::is_test_order( $refund ) );
+	}
+
+	/**
+	 * @testdox Should allow the woocommerce_analytics_is_test_order filter to mark a normal order as a test order.
+	 */
+	public function test_is_test_order_filter_can_override(): void {
+		$order = \WC_Helper_Order::create_order();
+
+		// Order has no _wcpay_mode meta, so default is false.
+		$this->assertFalse( OrdersScheduler::is_test_order( $order ) );
+
+		// Override via filter to mark it as test.
+		add_filter( 'woocommerce_analytics_is_test_order', '__return_true' );
+
+		$this->assertTrue( OrdersScheduler::is_test_order( $order ) );
+
+		remove_filter( 'woocommerce_analytics_is_test_order', '__return_true' );
+	}
+
+	/**
+	 * @testdox Should allow the woocommerce_analytics_is_test_order filter to include a test order in analytics.
+	 */
+	public function test_is_test_order_filter_can_allow_test_order(): void {
+		$order = \WC_Helper_Order::create_order();
+		$order->update_meta_data( '_wcpay_mode', 'test' );
+		$order->save();
+
+		// Default is true because _wcpay_mode is 'test'.
+		$this->assertTrue( OrdersScheduler::is_test_order( $order ) );
+
+		// Override via filter to allow it.
+		add_filter( 'woocommerce_analytics_is_test_order', '__return_false' );
+
+		$this->assertFalse( OrdersScheduler::is_test_order( $order ) );
+
+		remove_filter( 'woocommerce_analytics_is_test_order', '__return_false' );
+	}
+
+	/**
+	 * @testdox Should return false for a refund whose parent order has been deleted.
+	 */
+	public function test_is_test_order_with_orphaned_refund(): void {
+		$order = \WC_Helper_Order::create_order();
+		$order->update_meta_data( '_wcpay_mode', 'test' );
+		$order->save();
+
+		$refund = wc_create_refund(
+			array(
+				'order_id' => $order->get_id(),
+				'amount'   => 10,
+				'reason'   => 'Test refund',
+			)
+		);
+
+		// Delete the parent order to create an orphaned refund.
+		$order->delete( true );
+
+		$this->assertFalse( OrdersScheduler::is_test_order( $refund ) );
+	}
+
+	/**
+	 * @testdox Should pass the parent order to the filter when checking a refund.
+	 */
+	public function test_is_test_order_filter_receives_parent_order_for_refund(): void {
+		$order = \WC_Helper_Order::create_order();
+		$order->save();
+
+		$refund = wc_create_refund(
+			array(
+				'order_id' => $order->get_id(),
+				'amount'   => 10,
+				'reason'   => 'Test refund',
+			)
+		);
+
+		$received_order  = null;
+		$filter_callback = function ( $is_test, $filter_order ) use ( &$received_order ) {
+			$received_order = $filter_order;
+			return $is_test;
+		};
+		add_filter( 'woocommerce_analytics_is_test_order', $filter_callback, 10, 2 );
+
+		OrdersScheduler::is_test_order( $refund );
+
+		$this->assertNotNull( $received_order );
+		$this->assertEquals( $order->get_id(), $received_order->get_id() );
+
+		remove_filter( 'woocommerce_analytics_is_test_order', $filter_callback );
+	}
+
+	/**
+	 * @testdox Should return -1 from DataStore update when given a test order.
+	 */
+	public function test_datastore_update_skips_test_order(): void {
+		$order = \WC_Helper_Order::create_order();
+		$order->update_meta_data( '_wcpay_mode', 'test' );
+		$order->save();
+
+		$result = OrdersStatsDataStore::update( $order );
+
+		$this->assertSame( -1, $result );
+	}
+
+	/**
+	 * @testdox Should return -1 from DataStore sync_order when given a test order ID.
+	 */
+	public function test_datastore_sync_order_skips_test_order(): void {
+		$order = \WC_Helper_Order::create_order();
+		$order->update_meta_data( '_wcpay_mode', 'test' );
+		$order->save();
+
+		$result = OrdersStatsDataStore::sync_order( $order->get_id() );
+
+		$this->assertSame( -1, $result );
+	}
+
 	/**
 	 * Clear any scheduled batch processor actions.
 	 *