Commit 481932123f for woocommerce

commit 481932123f386279bdb3f71d95a2701220d3b7bb
Author: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Date:   Wed Nov 26 11:49:20 2025 +0900

    Fix duplicate recurring actions in OrdersScheduler and handle import mode transitions (#61907)

    * Refactor OrdersScheduler to support immediate and recurring imports

    - Introduced a conditional check for immediate import functionality, allowing for either immediate scheduling of imports or recurring batch processing based on the configuration.
    - Added a new action to handle changes to the immediate import option, ensuring proper scheduling of imports during configuration changes.
    - Updated the fallback date for cursor validation to the last 24 hours for improved accuracy.
    - Cleaned up legacy code related to immediate import checks and scheduling logic.

    * Refactor immediate import handling in OrdersScheduler

    - Reorganized the logic for scheduling immediate catchup batches during configuration changes to ensure no orders are missed.
    - Moved the scheduling of the immediate catchup batch to occur after unscheduling the recurring batch processor for clarity and improved flow.

    * Implement immediate import check in OrdersScheduler

    - Added a conditional check to return the order ID early if immediate import is not enabled, improving the clarity of the scheduling logic.
    - This change ensures that the function only processes orders when immediate import functionality is active, enhancing performance and maintainability.

    * Add changelog

    * Update plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * Update plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * Update plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * Update plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * Update plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * Update tests

    * Fix lint

    * Update plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * Add handling for immediate import option in OrdersScheduler

    - Introduced methods to handle addition and deletion of the immediate import option, ensuring proper state management.
    - Set a default value for the immediate import option to streamline its usage.
    - Updated the logic to check the immediate import option consistently across the class, enhancing maintainability.

    ---------

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/fix-order-scheduler-batch-import b/plugins/woocommerce/changelog/fix-order-scheduler-batch-import
new file mode 100644
index 0000000000..71630039d0
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-order-scheduler-batch-import
@@ -0,0 +1,5 @@
+Significance: patch
+Type: update
+Comment: Fix order schedule batch import - the feature is not released yet
+
+
diff --git a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
index 9a871f7694..d4a75fe3e3 100644
--- a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
+++ b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
@@ -59,6 +59,13 @@ class OrdersScheduler extends ImportScheduler {
 	 */
 	const IMMEDIATE_IMPORT_OPTION = 'woocommerce_analytics_immediate_import';

+	/**
+	 * Default value for the immediate import option.
+	 *
+	 * @var string
+	 */
+	const IMMEDIATE_IMPORT_OPTION_DEFAULT_VALUE = 'yes';
+
 	/**
 	 * Attach order lookup update hooks.
 	 *
@@ -69,14 +76,20 @@ class OrdersScheduler extends ImportScheduler {
 		\Automattic\WooCommerce\Admin\Overrides\Order::add_filters();
 		\Automattic\WooCommerce\Admin\Overrides\OrderRefund::add_filters();

-		// Legacy behavior: Schedule import immediately on order create/update/delete.
-		add_action( 'woocommerce_update_order', array( __CLASS__, 'possibly_schedule_import' ) );
-		add_filter( 'woocommerce_create_order', array( __CLASS__, 'possibly_schedule_import' ) );
-		add_action( 'woocommerce_refund_created', array( __CLASS__, 'possibly_schedule_import' ) );
-		add_action( 'woocommerce_schedule_import', array( __CLASS__, 'possibly_schedule_import' ) );
-
-		// Schedule recurring batch processor.
-		add_action( 'init', array( __CLASS__, 'schedule_recurring_batch_processor' ) );
+		if ( self::is_immediate_import_enabled() ) {
+			// Schedule import immediately on order create/update/delete.
+			add_action( 'woocommerce_update_order', array( __CLASS__, 'possibly_schedule_import' ) );
+			add_filter( 'woocommerce_create_order', array( __CLASS__, 'possibly_schedule_import' ) );
+			add_action( 'woocommerce_refund_created', array( __CLASS__, 'possibly_schedule_import' ) );
+			add_action( 'woocommerce_schedule_import', array( __CLASS__, 'possibly_schedule_import' ) );
+		} else {
+			// Schedule recurring batch processor.
+			add_action( 'action_scheduler_ensure_recurring_actions', array( __CLASS__, 'schedule_recurring_batch_processor' ) );
+		}
+		// Watch for changes to the immediate import option.
+		add_action( 'add_option_' . self::IMMEDIATE_IMPORT_OPTION, array( __CLASS__, 'handle_immediate_import_option_added' ), 10, 2 );
+		add_action( 'update_option_' . self::IMMEDIATE_IMPORT_OPTION, array( __CLASS__, 'handle_immediate_import_option_change' ), 10, 2 );
+		add_action( 'delete_option', array( __CLASS__, 'handle_immediate_import_option_before_delete' ), 10, 1 );

 		OrdersStatsDataStore::init();
 		CouponsDataStore::init();
@@ -357,52 +370,94 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
 	 * @internal
 	 */
 	public static function schedule_recurring_batch_processor() {
-		if ( self::is_immediate_import_enabled() ) {
-			// No need to schedule if immediate import is enabled.
-			if ( self::has_existing_jobs( 'process_pending_batch', array() ) ) {
-				$action_hook = self::get_action( 'process_pending_batch' );
-				as_unschedule_all_actions( $action_hook, array(), static::$group );
-			}
+		$action_hook = self::get_action( 'process_pending_batch' );
+		// 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';
+		if ( call_user_func( $has_scheduled_action, $action_hook ) ) {
 			return;
 		}
-		// Initialize last processed date if not set.
-		self::initialize_last_processed_date();

 		/**
 		 * Filters the interval for the recurring batch processor.
 		 *
-		 * @since 9.5.0
+		 * @since 10.4.0
 		 * @param int $interval Interval in seconds. Default 12 hours.
 		 */
 		$interval = apply_filters( 'woocommerce_analytics_import_interval', 12 * HOUR_IN_SECONDS );

-		$action_hook = self::get_action( 'process_pending_batch' );
+		as_schedule_recurring_action( time(), $interval, $action_hook, array(), static::$group, true );
+	}

-		// Schedule recurring action if not already scheduled.
-		if ( ! self::has_existing_jobs( 'process_pending_batch', array() ) ) {
-			self::queue()->schedule_recurring( time(), $interval, $action_hook, array(), static::$group );
+	/**
+	 * Handle changes to the immediate import option.
+	 *
+	 * When switching from batch processing to immediate import,
+	 * we need to run a final catchup batch to ensure no orders are missed.
+	 *
+	 * When switching from immediate import to batch processing,
+	 * we need to reschedule the recurring batch processor.
+	 *
+	 * @internal
+	 * @param mixed $old_value The old value of the option.
+	 * @param mixed $new_value The new value of the option.
+	 * @return void
+	 */
+	public static function handle_immediate_import_option_change( $old_value, $new_value ) {
+		// 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' );
+			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 ) );
+		} 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.
+			update_option( self::LAST_PROCESSED_ORDER_DATE_OPTION, gmdate( 'Y-m-d H:i:s', time() - MINUTE_IN_SECONDS ) );
+			update_option( self::LAST_PROCESSED_ORDER_ID_OPTION, 0 );
+
+			// Schedule the recurring batch processor.
+			self::schedule_recurring_batch_processor();
 		}
 	}

 	/**
-	 * Initialize the last processed date option if not set.
+	 * Handle addition of the immediate import option.
 	 *
 	 * @internal
+	 * @param string $option_name The name of the option that was added.
+	 * @param string $value The value of the option that was added.
+	 *
+	 * @return void
 	 */
-	private static function initialize_last_processed_date() {
-		if ( false !== get_option( self::LAST_PROCESSED_ORDER_DATE_OPTION, false ) ) {
-			return; // Already initialized.
+	public static function handle_immediate_import_option_added( $option_name, $value ) {
+		if ( self::IMMEDIATE_IMPORT_OPTION !== $option_name ) {
+			return;
 		}

-		/**
-		 * Add buffer to ensure orders created or updated during plugin activation, upgrade, or prior to import are accounted for.
-		 * Buffer in seconds. 10 minutes.
-		 */
-		$buffer_seconds = 10 * MINUTE_IN_SECONDS;
-		$start_date     = gmdate( 'Y-m-d H:i:s', time() - $buffer_seconds );
+		self::handle_immediate_import_option_change( self::IMMEDIATE_IMPORT_OPTION_DEFAULT_VALUE, $value );
+	}
+
+	/**
+	 * Handle deletion of the immediate import option.
+	 *
+	 * @internal
+	 * @param string $option_name The name of the option that was deleted.
+	 *
+	 * @return void
+	 */
+	public static function handle_immediate_import_option_before_delete( $option_name ) {
+		if ( self::IMMEDIATE_IMPORT_OPTION !== $option_name ) {
+			return;
+		}

-		update_option( self::LAST_PROCESSED_ORDER_DATE_OPTION, $start_date, false );
-		update_option( self::LAST_PROCESSED_ORDER_ID_OPTION, 0, false );
+		self::handle_immediate_import_option_change(
+			get_option( self::IMMEDIATE_IMPORT_OPTION, self::IMMEDIATE_IMPORT_OPTION_DEFAULT_VALUE ),
+			self::IMMEDIATE_IMPORT_OPTION_DEFAULT_VALUE,
+		);
 	}

 	/**
@@ -427,13 +482,15 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
 		}

 		// Load cursor position from options if not provided.
-		$cursor_date = $cursor_date ?? get_option( self::LAST_PROCESSED_ORDER_DATE_OPTION );
-		$cursor_id   = $cursor_id ?? (int) get_option( self::LAST_PROCESSED_ORDER_ID_OPTION, 0 );
+		// If the cursor date is not provided, use the last 24 hours as the default since `action_scheduler_ensure_recurring_actions` runs daily so 24 hours is enough.
+		$default_cursor_date = gmdate( 'Y-m-d H:i:s', strtotime( '-24 hours' ) );
+		$cursor_date         = $cursor_date ?? get_option( self::LAST_PROCESSED_ORDER_DATE_OPTION, $default_cursor_date );
+		$cursor_id           = $cursor_id ?? (int) get_option( self::LAST_PROCESSED_ORDER_ID_OPTION, 0 );

 		// Validate cursor date.
 		if ( ! $cursor_date || ! strtotime( $cursor_date ) ) {
 			$logger->error( 'Invalid cursor date: ' . $cursor_date, $context );
-			$cursor_date = gmdate( 'Y-m-d H:i:s', 0 ); // Fallback to the earliest possible date.
+			$cursor_date = $default_cursor_date;
 		}

 		$batch_size = self::get_batch_size( 'process_pending_batch' );
@@ -628,7 +685,6 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
 	 * @return bool
 	 */
 	private static function is_immediate_import_enabled(): bool {
-		$enable_immediate_import = get_option( self::IMMEDIATE_IMPORT_OPTION, true );
-		return filter_var( $enable_immediate_import, FILTER_VALIDATE_BOOLEAN );
+		return 'no' !== get_option( self::IMMEDIATE_IMPORT_OPTION, self::IMMEDIATE_IMPORT_OPTION_DEFAULT_VALUE );
 	}
 }
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 da84a01939..ec66777d33 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Schedulers/OrdersSchedulerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Schedulers/OrdersSchedulerTest.php
@@ -23,133 +23,194 @@ class OrdersSchedulerTest extends WC_Unit_Test_Case {
 		delete_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION );
 		delete_option( OrdersScheduler::LAST_PROCESSED_ORDER_ID_OPTION );
 		delete_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION );
+
+		// Clean up any scheduled actions.
+		$this->clear_scheduled_batch_processor();
 	}

 	/**
-	 * Test that batch processor is NOT scheduled when immediate import is enabled.
+	 * Test that batch processor is scheduled when called.
 	 */
-	public function test_batch_processor_not_scheduled_when_immediate_import_enabled() {
-		// Enable immediate import.
-		add_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, true );
+	public function test_batch_processor_scheduled() {
+		// Clear any existing scheduled actions.
+		$this->clear_scheduled_batch_processor();

 		OrdersScheduler::schedule_recurring_batch_processor();

-		// Verify the last processed date was NOT initialized (batch processor skipped).
-		$this->assertFalse(
-			get_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION ),
-			'Last processed date should not be initialized when immediate import is enabled'
+		// Verify the recurring action is scheduled.
+		$this->assertTrue(
+			$this->is_batch_processor_scheduled(),
+			'Batch processor should be scheduled'
 		);
 	}

 	/**
-	 * Test that batch processor IS scheduled when immediate import is disabled.
+	 * Test that batch processor is not scheduled twice.
 	 */
-	public function test_batch_processor_scheduled_when_immediate_import_disabled() {
-		// Disable immediate import (enable batch mode).
-		add_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, false );
+	public function test_batch_processor_not_scheduled_twice() {
+		// Clear any existing scheduled actions.
+		$this->clear_scheduled_batch_processor();

+		$action_hook = OrdersScheduler::get_action( 'process_pending_batch' );
+
+		// Schedule first time.
+		OrdersScheduler::schedule_recurring_batch_processor();
+		// Try to schedule again.
 		OrdersScheduler::schedule_recurring_batch_processor();

-		// Verify the last processed date WAS initialized (batch processor scheduled).
-		$this->assertNotFalse(
-			get_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION ),
-			'Last processed date should be initialized when batch mode is enabled'
+		// Verify it's still the same scheduled time (not rescheduled).
+		$second_scheduled = as_get_scheduled_actions(
+			array(
+				'hook'     => $action_hook,
+				'args'     => array(),
+				'group'    => OrdersScheduler::$group,
+				'status'   => 'pending',
+				'per_page' => 1,
+			),
+			ARRAY_A
 		);
+		$this->assertCount( 1, $second_scheduled, 'Batch processor should be scheduled once' );
 	}

 	/**
-	 * Test that the batch processor action is registered.
+	 * Test that import interval filter is applied.
 	 */
-	public function test_get_scheduler_actions_includes_batch_processor() {
-		$actions = OrdersScheduler::get_scheduler_actions();
+	public function test_import_interval_filter_is_applied() {
+		// Clear any existing scheduled actions.
+		$this->clear_scheduled_batch_processor();

-		$this->assertArrayHasKey(
-			'process_pending_batch',
-			$actions,
-			'Scheduler actions should include process_pending_batch'
+		$custom_interval = 6 * HOUR_IN_SECONDS;
+		$filter_called   = false;
+		add_filter(
+			'woocommerce_analytics_import_interval',
+			function () use ( $custom_interval, &$filter_called ) {
+				$filter_called = true;
+				return $custom_interval;
+			}
 		);

-		$this->assertEquals(
-			'wc-admin_process_pending_orders_batch',
-			$actions['process_pending_batch'],
-			'process_pending_batch action should map to correct hook name'
+		// This will trigger the filter.
+		OrdersScheduler::schedule_recurring_batch_processor();
+
+		// Verify filter was applied.
+		$this->assertTrue(
+			$filter_called,
+			'Import interval filter should be applied when scheduling batch processor'
 		);
 	}

 	/**
-	 * Test that last processed date is initialized correctly.
+	 * Test that handle_immediate_import_option_change unschedules batch processor when switching to immediate import.
 	 */
-	public function test_initialize_sets_last_processed_date() {
-		// Disable immediate import to enable batch mode.
-		add_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, false );
+	public function test_handle_immediate_import_option_change_unschedules_batch_when_enabling_immediate() {
+		// Clear any existing scheduled actions.
+		$this->clear_scheduled_batch_processor();

+		// Schedule the batch processor first.
 		OrdersScheduler::schedule_recurring_batch_processor();
+		$this->assertTrue(
+			$this->is_batch_processor_scheduled(),
+			'Batch processor should be scheduled initially'
+		);

-		$last_date = get_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION );
+		// Switch from batch processing ('no') to immediate import ('yes').
+		OrdersScheduler::handle_immediate_import_option_change( 'no', 'yes' );
+
+		// Verify the batch processor is unscheduled.
+		$this->assertFalse(
+			$this->is_batch_processor_scheduled(),
+			'Batch processor should be unscheduled when switching to immediate import'
+		);
+	}
+
+	/**
+	 * Test that handle_immediate_import_option_change schedules batch processor when switching from immediate import.
+	 */
+	public function test_handle_immediate_import_option_change_schedules_batch_when_disabling_immediate() {
+		// Clear any existing scheduled actions.
+		$this->clear_scheduled_batch_processor();
+
+		// Switch from immediate import ('yes') to batch processing ('no').
+		OrdersScheduler::handle_immediate_import_option_change( 'yes', 'no' );
+
+		// Verify the batch processor is scheduled.
+		$this->assertTrue(
+			$this->is_batch_processor_scheduled(),
+			'Batch processor should be scheduled when switching from immediate import to batch processing'
+		);

+		// Verify the last processed date is set to approximately 1 minute ago.
+		$last_date = get_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION );
 		$this->assertNotFalse(
 			$last_date,
-			'Last processed date option should be set after initialization'
+			'Last processed date should be set when switching to batch processing'
 		);

-		// Should be approximately 10 minutes ago (600 seconds).
-		$expected_timestamp = time() - ( 10 * MINUTE_IN_SECONDS );
+		$expected_timestamp = time() - MINUTE_IN_SECONDS;
 		$actual_timestamp   = strtotime( $last_date );

 		$this->assertEqualsWithDelta(
 			$expected_timestamp,
 			$actual_timestamp,
 			5,
-			'Last processed date should be approximately 10 minutes ago'
+			'Last processed date should be approximately 1 minute ago'
+		);
+
+		// Verify the last processed order ID is reset to 0.
+		$last_id = get_option( OrdersScheduler::LAST_PROCESSED_ORDER_ID_OPTION );
+		$this->assertEquals(
+			0,
+			$last_id,
+			'Last processed order ID should be reset to 0'
 		);
 	}

 	/**
-	 * Test that last processed date is not re-initialized if already set.
+	 * Test that handle_immediate_import_option_change does nothing for other transitions.
 	 */
-	public function test_initialize_does_not_overwrite_existing_date() {
-		$existing_date = '2024-01-01 12:00:00';
-		add_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION, $existing_date );
-
-		// Disable immediate import to enable batch mode.
-		add_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, false );
+	public function test_handle_immediate_import_option_change_ignores_other_transitions() {
+		// Clear any existing scheduled actions.
+		$this->clear_scheduled_batch_processor();

-		OrdersScheduler::schedule_recurring_batch_processor();
+		$action_hook = OrdersScheduler::get_action( 'process_pending_batch' );

-		$last_date = get_option( OrdersScheduler::LAST_PROCESSED_ORDER_DATE_OPTION );
+		// Test transition from 'yes' to 'yes' (no change).
+		OrdersScheduler::handle_immediate_import_option_change( 'yes', 'yes' );
+		$this->assertFalse(
+			$this->is_batch_processor_scheduled(),
+			'Batch processor should not be scheduled when option stays as immediate import'
+		);

+		// Test transition from 'no' to 'no' (no change).
+		OrdersScheduler::schedule_recurring_batch_processor();
+		$scheduled_time = as_next_scheduled_action( $action_hook );
+		OrdersScheduler::handle_immediate_import_option_change( 'no', 'no' );
 		$this->assertEquals(
-			$existing_date,
-			$last_date,
-			'Last processed date should not be overwritten if already set'
+			$scheduled_time,
+			as_next_scheduled_action( $action_hook ),
+			'Batch processor should remain scheduled when option stays as batch processing'
 		);
 	}

 	/**
-	 * Test that import interval filter is applied.
+	 * Clear any scheduled batch processor actions.
+	 *
+	 * @return void
 	 */
-	public function test_import_interval_filter_is_applied() {
-		$custom_interval = 6 * HOUR_IN_SECONDS;
-		$filter_called   = false;
-		add_filter(
-			'woocommerce_analytics_import_interval',
-			function () use ( $custom_interval, &$filter_called ) {
-				$filter_called = true;
-				return $custom_interval;
-			}
-		);
-
-		// Enable batch mode (disable immediate import).
-		add_option( OrdersScheduler::IMMEDIATE_IMPORT_OPTION, false );
-
-		// This will trigger the filter.
-		OrdersScheduler::schedule_recurring_batch_processor();
+	private function clear_scheduled_batch_processor(): void {
+		$action_hook = OrdersScheduler::get_action( 'process_pending_batch' );
+		if ( function_exists( 'as_unschedule_all_actions' ) ) {
+			as_unschedule_all_actions( $action_hook, array(), OrdersScheduler::$group );
+		}
+	}

-		// Verify filter was applied (we can't directly check ActionScheduler without complex mocking,
-		// but we can verify the filter is called by checking if it was applied).
-		$this->assertTrue(
-			$filter_called,
-			'Import interval filter should be applied during initialization'
-		);
+	/**
+	 * Check if the batch processor action is scheduled.
+	 *
+	 * @return bool
+	 */
+	private function is_batch_processor_scheduled(): bool {
+		$action_hook = OrdersScheduler::get_action( 'process_pending_batch' );
+		return function_exists( 'as_has_scheduled_action' ) ? as_has_scheduled_action( $action_hook, array(), OrdersScheduler::$group ) : (bool) as_next_scheduled_action( $action_hook, array(), OrdersScheduler::$group );
 	}
 }