Commit e86b2bc8be for woocommerce
commit e86b2bc8bedbd5901ec70570ab3997a058976344
Author: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Date: Fri Dec 5 18:33:13 2025 +0900
Add REST API endpoints for analytics batch import status and manual triggering (#62196)
* Add REST API Analytics Imports Controller for batch import management
This commit introduces a new class, AnalyticsImports, which handles requests for checking batch import status and triggering manual imports. It includes permission checks for user access and defines the necessary REST API routes for these functionalities. Additionally, the Init class is updated to register the new controller if the 'analytics-scheduled-import' feature is enabled.
* Add unit tests for AnalyticsImports API controller
This commit introduces a new test class, AnalyticsImportsTest, which includes comprehensive unit tests for the Analytics Imports API controller. The tests cover various scenarios such as checking the status of imports, triggering imports, and verifying user permissions. This addition ensures the functionality and reliability of the API endpoints related to batch import management.
* Add changelog
* Update plugins/woocommerce/tests/php/src/Admin/API/AnalyticsImportsTest.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Fix lint
* Clean up options
* Fix types
* Refactor has_manual_triggered_import_scheduled method to simplify scheduled action check
* Refactor permission checks in AnalyticsImports API controller to unify access control for analytics imports
* Refactor AnalyticsImports API to improve import status checks and update response structure
- Replaced manual_triggered_import_scheduled with import_in_progress_or_due in response.
- Updated methods to check for ongoing or soon-to-run imports.
- Adjusted test cases to reflect changes in response structure and import status logic.
* Fix tests
* Fix lint
* Fix schema
* Fix lint
* Fix phan errors
* Fix tests
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/wooa7s-793-backend-add-batch-status-and-manual-trigger-endpoints b/plugins/woocommerce/changelog/wooa7s-793-backend-add-batch-status-and-manual-trigger-endpoints
new file mode 100644
index 0000000000..d8134d8a7d
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooa7s-793-backend-add-batch-status-and-manual-trigger-endpoints
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add analytics import status and manual trigger endpoints
diff --git a/plugins/woocommerce/src/Admin/API/AnalyticsImports.php b/plugins/woocommerce/src/Admin/API/AnalyticsImports.php
new file mode 100644
index 0000000000..bd0c2b64e6
--- /dev/null
+++ b/plugins/woocommerce/src/Admin/API/AnalyticsImports.php
@@ -0,0 +1,301 @@
+<?php
+/**
+ * REST API Analytics Imports Controller
+ *
+ * Handles requests to get batch import status and trigger manual imports.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Admin\API;
+
+use WP_Error;
+use Automattic\WooCommerce\Internal\Admin\Schedulers\OrdersScheduler;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * REST API Analytics Imports Controller.
+ *
+ * @internal
+ */
+class AnalyticsImports extends \WC_REST_Data_Controller {
+ /**
+ * Endpoint namespace.
+ *
+ * @var string
+ */
+ protected $namespace = 'wc-analytics';
+
+ /**
+ * Route base.
+ *
+ * @var string
+ */
+ protected $rest_base = 'imports';
+
+ /**
+ * Register routes.
+ *
+ * @return void
+ */
+ public function register_routes(): void {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/status',
+ array(
+ array(
+ 'methods' => \WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_status' ),
+ 'permission_callback' => array( $this, 'permissions_check' ),
+ ),
+ 'schema' => array( $this, 'get_status_schema' ),
+ )
+ );
+
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/trigger',
+ array(
+ array(
+ 'methods' => \WP_REST_Server::CREATABLE,
+ 'callback' => array( $this, 'trigger_import' ),
+ 'permission_callback' => array( $this, 'permissions_check' ),
+ ),
+ 'schema' => array( $this, 'get_trigger_schema' ),
+ )
+ );
+ }
+
+ /**
+ * Check if a given request has access to analytics imports.
+ *
+ * @param \WP_REST_Request<array<string, mixed>> $request Full details about the request.
+ * @return WP_Error|boolean
+ */
+ public function permissions_check( $request ) {
+ if ( ! current_user_can( 'manage_woocommerce' ) ) {
+ return new WP_Error(
+ 'woocommerce_rest_cannot_access',
+ __( 'Sorry, you cannot access analytics imports.', 'woocommerce' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the current import status.
+ *
+ * @param \WP_REST_Request<array<string, mixed>> $request Full details about the request.
+ * @return \WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function get_status( $request ) {
+ $is_immediate_mode = $this->is_immediate_import_enabled();
+ $mode = $is_immediate_mode ? 'immediate' : 'scheduled';
+
+ $response = array(
+ 'mode' => $mode,
+ 'last_processed_date' => null,
+ 'next_scheduled' => null,
+ 'import_in_progress_or_due' => null,
+ );
+
+ // For scheduled mode, populate additional fields.
+ if ( ! $is_immediate_mode ) {
+ $last_processed_gmt = get_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION, null );
+ $response['last_processed_date'] = ( is_string( $last_processed_gmt ) && $last_processed_gmt ) ? get_date_from_gmt( $last_processed_gmt, 'Y-m-d H:i:s' ) : null;
+ $response['next_scheduled'] = $this->get_next_scheduled_time();
+ $response['import_in_progress_or_due'] = $this->is_import_in_progress_or_due();
+ }
+
+ return rest_ensure_response( $response );
+ }
+
+ /**
+ * Trigger a manual import.
+ *
+ * @param \WP_REST_Request<array<string, mixed>> $request Full details about the request.
+ * @return \WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function trigger_import( $request ) {
+ $is_immediate_mode = $this->is_immediate_import_enabled();
+
+ // Return error if in immediate mode.
+ if ( $is_immediate_mode ) {
+ return new WP_Error(
+ 'woocommerce_rest_analytics_import_immediate_mode',
+ __( 'Manual import is not available in immediate mode. Imports happen automatically.', 'woocommerce' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Check if an import is already in progress or due to run soon.
+ if ( $this->is_import_in_progress_or_due() ) {
+ return new WP_Error(
+ 'woocommerce_rest_analytics_import_in_progress',
+ __( 'A batch import is already in progress or scheduled to run soon. Please wait for it to complete before triggering a new import.', 'woocommerce' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Trigger the batch import immediately by rescheduling the recurring processor.
+ // This unschedules the current recurring action and reschedules it to run now.
+ $action_hook = OrdersScheduler::get_action( OrdersScheduler::PROCESS_PENDING_ORDERS_BATCH_ACTION );
+ if ( ! is_string( $action_hook ) ) {
+ return new WP_Error(
+ 'woocommerce_rest_analytics_import_invalid_action',
+ __( 'Invalid action hook for batch import.', 'woocommerce' ),
+ array( 'status' => 500 )
+ );
+ }
+ WC()->queue()->cancel_all( $action_hook, array(), (string) OrdersScheduler::$group );
+ OrdersScheduler::schedule_recurring_batch_processor();
+
+ return rest_ensure_response(
+ array(
+ 'success' => true,
+ 'message' => __( 'Batch import triggered successfully.', 'woocommerce' ),
+ )
+ );
+ }
+
+ /**
+ * Check if immediate import is enabled.
+ *
+ * @return bool
+ */
+ private function is_immediate_import_enabled() {
+ return 'no' !== get_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, OrdersScheduler::IMMEDIATE_IMPORT_OPTION_DEFAULT_VALUE );
+ }
+
+ /**
+ * Get the next scheduled time for the batch processor.
+ *
+ * @return string|null Datetime string in site timezone or null if not scheduled.
+ */
+ private function get_next_scheduled_time() {
+ $action_hook = OrdersScheduler::get_action( OrdersScheduler::PROCESS_PENDING_ORDERS_BATCH_ACTION );
+ if ( ! is_string( $action_hook ) ) {
+ return null;
+ }
+ $next_time = WC()->queue()->get_next( $action_hook, array(), (string) OrdersScheduler::$group );
+
+ if ( ! $next_time ) {
+ return null;
+ }
+
+ // Convert UTC timestamp to site timezone.
+ return get_date_from_gmt( $next_time->format( 'Y-m-d H:i:s' ), 'Y-m-d H:i:s' );
+ }
+
+ /**
+ * Get the schema for the status endpoint, conforming to JSON Schema.
+ *
+ * @return array
+ */
+ public function get_status_schema() {
+ $schema = array(
+ '$schema' => 'https://json-schema.org/draft-04/schema#',
+ 'title' => 'analytics_import_status',
+ 'type' => 'object',
+ 'properties' => array(
+ 'mode' => array(
+ 'type' => 'string',
+ 'enum' => array( 'scheduled', 'immediate' ),
+ 'description' => __( 'Current import mode.', 'woocommerce' ),
+ 'context' => array( 'view' ),
+ 'readonly' => true,
+ ),
+ 'last_processed_date' => array(
+ 'type' => array( 'string', 'null' ),
+ 'description' => __( 'Last processed order date (null in immediate mode).', 'woocommerce' ),
+ 'context' => array( 'view' ),
+ 'readonly' => true,
+ ),
+ 'next_scheduled' => array(
+ 'type' => array( 'string', 'null' ),
+ 'description' => __( 'Next scheduled import time (null in immediate mode).', 'woocommerce' ),
+ 'context' => array( 'view' ),
+ 'readonly' => true,
+ ),
+ 'import_in_progress_or_due' => array(
+ 'type' => array( 'boolean', 'null' ),
+ 'description' => __( 'Whether a batch import is currently running or scheduled to run within the next minute (null in immediate mode).', 'woocommerce' ),
+ 'context' => array( 'view' ),
+ 'readonly' => true,
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Get the schema for the trigger endpoint, conforming to JSON Schema.
+ *
+ * @return array
+ */
+ public function get_trigger_schema() {
+ $schema = array(
+ '$schema' => 'https://json-schema.org/draft-04/schema#',
+ 'title' => 'analytics_import_trigger',
+ 'type' => 'object',
+ 'properties' => array(
+ 'success' => array(
+ 'type' => 'boolean',
+ 'description' => __( 'Whether the trigger was successful.', 'woocommerce' ),
+ 'context' => array( 'view' ),
+ 'readonly' => true,
+ ),
+ 'message' => array(
+ 'type' => 'string',
+ 'description' => __( 'Result message.', 'woocommerce' ),
+ 'context' => array( 'view' ),
+ 'readonly' => true,
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Check if a batch import is currently in progress or due to run soon.
+ *
+ * @return bool True if a batch import is in progress or scheduled to run within the next minute, false otherwise.
+ */
+ private function is_import_in_progress_or_due() {
+ $hook = OrdersScheduler::get_action( OrdersScheduler::PROCESS_PENDING_ORDERS_BATCH_ACTION );
+ if ( ! is_string( $hook ) ) {
+ return false;
+ }
+
+ // Check for actions with 'in-progress' status.
+ $in_progress_actions = WC()->queue()->search(
+ array(
+ 'hook' => $hook,
+ 'status' => 'in-progress',
+ 'per_page' => 1,
+ ),
+ 'ids'
+ );
+
+ if ( ! empty( $in_progress_actions ) ) {
+ return true;
+ }
+
+ // Check if the next scheduled import is due within 1 minute.
+ $next_scheduled = WC()->queue()->get_next( $hook, array(), (string) OrdersScheduler::$group );
+ if ( $next_scheduled ) {
+ $time_until_next = $next_scheduled->getTimestamp() - time();
+ // Consider it "due" if it's scheduled to run within the next 60 seconds.
+ if ( $time_until_next <= MINUTE_IN_SECONDS ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php
index 7c8b75ed82..ffb33a0786 100644
--- a/plugins/woocommerce/src/Admin/API/Init.php
+++ b/plugins/woocommerce/src/Admin/API/Init.php
@@ -186,6 +186,10 @@ class Init {
'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\Controller',
);
+ if ( Features::is_enabled( 'analytics-scheduled-import' ) ) {
+ $analytics_controllers[] = 'Automattic\WooCommerce\Admin\API\AnalyticsImports';
+ }
+
// The performance indicators controllerq must be registered last, after other /stats endpoints have been registered.
$analytics_controllers[] = 'Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators\Controller';
}
diff --git a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
index 52e8f4a3a2..f3370ccec4 100644
--- a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
+++ b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
@@ -67,6 +67,13 @@ class OrdersScheduler extends ImportScheduler {
*/
const IMMEDIATE_IMPORT_OPTION_DEFAULT_VALUE = 'yes';
+ /**
+ * Action name for the order batch import.
+ *
+ * @var string
+ */
+ const PROCESS_PENDING_ORDERS_BATCH_ACTION = 'process_pending_batch';
+
/**
* Attach order lookup update hooks.
*
@@ -127,7 +134,7 @@ class OrdersScheduler extends ImportScheduler {
return array_merge(
parent::get_scheduler_actions(),
array(
- 'process_pending_batch' => 'wc-admin_process_pending_orders_batch',
+ self::PROCESS_PENDING_ORDERS_BATCH_ACTION => 'wc-admin_process_pending_orders_batch',
)
);
}
@@ -142,7 +149,7 @@ class OrdersScheduler extends ImportScheduler {
return array_merge(
parent::get_batch_sizes(),
array(
- 'process_pending_batch' => 100,
+ self::PROCESS_PENDING_ORDERS_BATCH_ACTION => 100,
)
);
}
@@ -196,8 +203,8 @@ class OrdersScheduler extends ImportScheduler {
"SELECT COUNT(*) FROM {$wpdb->posts}
WHERE post_type IN ( 'shop_order', 'shop_order_refund' )
AND post_status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' )
- {$where_clause}"
- ); // phpcs:ignore unprepared SQL ok.
+ {$where_clause}" // phpcs:ignore unprepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared SQL ok.
+ );
$order_ids = absint( $count ) > 0 ? $wpdb->get_col(
$wpdb->prepare(
@@ -374,7 +381,7 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
* @internal
*/
public static function schedule_recurring_batch_processor() {
- $action_hook = self::get_action( 'process_pending_batch' );
+ $action_hook = self::get_action( self::PROCESS_PENDING_ORDERS_BATCH_ACTION );
// The most efficient way to check for an existing action is to use `as_has_scheduled_action`, but in unusual
// cases where another plugin has loaded a very old version of Action Scheduler, it may not be available to us.
$has_scheduled_action = function_exists( 'as_has_scheduled_action' ) ? 'as_has_scheduled_action' : 'as_next_scheduled_action';
@@ -405,12 +412,12 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
// If switching from batch processing to immediate import.
if ( 'no' === $old_value && 'yes' === $new_value ) {
// Unschedule the recurring batch processor.
- $action_hook = self::get_action( 'process_pending_batch' );
+ $action_hook = self::get_action( self::PROCESS_PENDING_ORDERS_BATCH_ACTION );
as_unschedule_all_actions( $action_hook, array(), static::$group );
// Schedule an immediate catchup batch to process all orders up to now.
// This ensures no orders are missed during the transition.
- self::schedule_action( 'process_pending_batch', array( null, null ) );
+ self::schedule_action( self::PROCESS_PENDING_ORDERS_BATCH_ACTION, array( null, null ) );
} elseif ( 'yes' === $old_value && 'no' === $new_value ) {
// Switching from immediate import to batch processing.
// Set the last processed order date to now with 1 minute buffer to ensure no orders are missed.
@@ -491,7 +498,7 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
$cursor_date = $default_cursor_date;
}
- $batch_size = self::get_batch_size( 'process_pending_batch' );
+ $batch_size = self::get_batch_size( self::PROCESS_PENDING_ORDERS_BATCH_ACTION );
$logger->info(
sprintf( 'Starting batch import. Cursor: %s (ID: %d), batch size: %d', $cursor_date, $cursor_id, $batch_size ),
@@ -505,6 +512,9 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
if ( empty( $orders ) ) {
$logger->info( 'No orders to process', $context );
+ // Update the cursor position to the start time of the batch so that the next batch will start from that point.
+ update_option( self::LAST_PROCESSED_ORDER_DATE_OPTION, gmdate( 'Y-m-d H:i:s', (int) $start_time ), false );
+ update_option( self::LAST_PROCESSED_ORDER_ID_OPTION, 0, false );
return;
}
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/AnalyticsImportsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/AnalyticsImportsTest.php
new file mode 100644
index 0000000000..2af63b1683
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/API/AnalyticsImportsTest.php
@@ -0,0 +1,274 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\API;
+
+use Automattic\WooCommerce\Internal\Admin\Schedulers\OrdersScheduler;
+use WC_REST_Unit_Test_Case;
+use WP_REST_Request;
+
+/**
+ * AnalyticsImports API controller test.
+ *
+ * @class AnalyticsImportsTest
+ */
+class AnalyticsImportsTest extends WC_REST_Unit_Test_Case {
+ /**
+ * Endpoint.
+ *
+ * @var string
+ */
+ const ENDPOINT = '/wc-analytics/imports';
+
+ /**
+ * Administrator user.
+ *
+ * @var int
+ */
+ protected $admin_user;
+
+ /**
+ * Shop manager user.
+ *
+ * @var int
+ */
+ protected $shop_manager_user;
+
+ /**
+ * Customer user.
+ *
+ * @var int
+ */
+ protected $customer_user;
+
+ /**
+ * Set up.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ // Create test users.
+ $this->admin_user = $this->factory->user->create(
+ array(
+ 'role' => 'administrator',
+ )
+ );
+
+ $this->shop_manager_user = $this->factory->user->create(
+ array(
+ 'role' => 'shop_manager',
+ )
+ );
+
+ $this->customer_user = $this->factory->user->create(
+ array(
+ 'role' => 'customer',
+ )
+ );
+
+ // Clear any scheduled actions.
+ $this->clear_scheduled_actions();
+ }
+
+ /**
+ * Tear down.
+ */
+ public function tearDown(): void {
+ $this->clear_scheduled_actions();
+ delete_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION );
+ delete_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION );
+ parent::tearDown();
+ }
+
+ /**
+ * Clear all scheduled batch import actions.
+ */
+ private function clear_scheduled_actions() {
+ $hook = OrdersScheduler::get_action( OrdersScheduler::PROCESS_PENDING_ORDERS_BATCH_ACTION );
+ as_unschedule_all_actions( $hook );
+ }
+
+ /**
+ * Test status endpoint returns correct mode for immediate import.
+ *
+ * @return void
+ */
+ public function test_status_returns_immediate_mode(): void {
+ wp_set_current_user( $this->admin_user );
+
+ // Set to immediate mode.
+ update_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, 'yes' );
+
+ $request = new WP_REST_Request( 'GET', self::ENDPOINT . '/status' );
+ $response = $this->server->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status() );
+ $this->assertArrayHasKey( 'mode', $data );
+ $this->assertSame( 'immediate', $data['mode'] );
+ $this->assertArrayHasKey( 'last_processed_date', $data );
+ $this->assertNull( $data['last_processed_date'] );
+ $this->assertArrayHasKey( 'next_scheduled', $data );
+ $this->assertNull( $data['next_scheduled'] );
+ $this->assertArrayHasKey( 'import_in_progress_or_due', $data );
+ $this->assertNull( $data['import_in_progress_or_due'] );
+ }
+
+ /**
+ * Test status endpoint returns correct mode for scheduled import.
+ *
+ * @return void
+ */
+ public function test_status_returns_scheduled_mode(): void {
+ wp_set_current_user( $this->admin_user );
+
+ // Set to scheduled mode.
+ update_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, 'no' );
+ update_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION, '2025-11-26 05:30:00' );
+
+ $request = new WP_REST_Request( 'GET', self::ENDPOINT . '/status' );
+ $response = $this->server->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status() );
+ $this->assertArrayHasKey( 'mode', $data );
+ $this->assertSame( 'scheduled', $data['mode'] );
+ $this->assertArrayHasKey( 'last_processed_date', $data );
+ $this->assertIsString( $data['last_processed_date'] );
+ $this->assertArrayHasKey( 'import_in_progress_or_due', $data );
+ $this->assertIsBool( $data['import_in_progress_or_due'] );
+ }
+
+ /**
+ * Test status endpoint converts datetime to site timezone.
+ *
+ * @return void
+ */
+ public function test_status_converts_datetime_to_site_timezone(): void {
+ wp_set_current_user( $this->admin_user );
+
+ // Set to scheduled mode.
+ update_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, 'no' );
+
+ // Set last processed date in GMT.
+ $gmt_date = '2025-11-26 05:30:00';
+ update_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION, $gmt_date );
+
+ $request = new WP_REST_Request( 'GET', self::ENDPOINT . '/status' );
+ $response = $this->server->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status() );
+
+ // Verify the date was converted from GMT to site timezone.
+ $expected_date = get_date_from_gmt( $gmt_date, 'Y-m-d H:i:s' );
+ $this->assertSame( $expected_date, $data['last_processed_date'] );
+ }
+
+ /**
+ * Test status endpoint requires manage_woocommerce capability.
+ *
+ * @return void
+ */
+ public function test_status_requires_permission(): void {
+ wp_set_current_user( $this->customer_user );
+
+ $request = new WP_REST_Request( 'GET', self::ENDPOINT . '/status' );
+ $response = $this->server->dispatch( $request );
+
+ $this->assertSame( 403, $response->get_status() );
+ }
+
+ /**
+ * Test shop manager can access status endpoint.
+ *
+ * @return void
+ */
+ public function test_shop_manager_can_access_status(): void {
+ wp_set_current_user( $this->shop_manager_user );
+
+ $request = new WP_REST_Request( 'GET', self::ENDPOINT . '/status' );
+ $response = $this->server->dispatch( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+ }
+
+ /**
+ * Test trigger endpoint successfully triggers batch import.
+ *
+ * @return void
+ */
+ public function test_trigger_successfully_triggers_import(): void {
+ wp_set_current_user( $this->admin_user );
+
+ // Set to scheduled mode.
+ update_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, 'no' );
+ // Clear any scheduled actions that may have been created when setting the option.
+ $this->clear_scheduled_actions();
+
+ $request = new WP_REST_Request( 'POST', self::ENDPOINT . '/trigger' );
+ $response = $this->server->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status() );
+ $this->assertArrayHasKey( 'success', $data );
+ $this->assertTrue( $data['success'] );
+ $this->assertArrayHasKey( 'message', $data );
+ $this->assertIsString( $data['message'] );
+ }
+
+ /**
+ * Test trigger endpoint returns error in immediate mode.
+ *
+ * @return void
+ */
+ public function test_trigger_fails_in_immediate_mode(): void {
+ wp_set_current_user( $this->admin_user );
+
+ // Set to immediate mode.
+ update_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, 'yes' );
+
+ $request = new WP_REST_Request( 'POST', self::ENDPOINT . '/trigger' );
+ $response = $this->server->dispatch( $request );
+
+ $this->assertSame( 400, $response->get_status() );
+
+ $data = $response->get_data();
+ $this->assertArrayHasKey( 'code', $data );
+ $this->assertSame( 'woocommerce_rest_analytics_import_immediate_mode', $data['code'] );
+ }
+
+ /**
+ * Test trigger endpoint requires manage_woocommerce capability.
+ *
+ * @return void
+ */
+ public function test_trigger_requires_permission(): void {
+ wp_set_current_user( $this->customer_user );
+
+ $request = new WP_REST_Request( 'POST', self::ENDPOINT . '/trigger' );
+ $response = $this->server->dispatch( $request );
+
+ $this->assertSame( 403, $response->get_status() );
+ }
+
+ /**
+ * Test shop manager can trigger import.
+ *
+ * @return void
+ */
+ public function test_shop_manager_can_trigger_import(): void {
+ wp_set_current_user( $this->shop_manager_user );
+
+ // Set to scheduled mode.
+ update_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, 'no' );
+ // Clear any scheduled actions that may have been created when setting the option.
+ $this->clear_scheduled_actions();
+
+ $request = new WP_REST_Request( 'POST', self::ENDPOINT . '/trigger' );
+ $response = $this->server->dispatch( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+ }
+}