HPOS: Add background sync (#39952)

* HPOS: Add a recurring event to check for unsynced orders

Adds an event that will get scheduled when HPOS data sync gets enabled.
The event will run every 6 hours and check for unsynced orders. If it
finds any, it will queue up the  data syncing batch processor.

Fixes #39626

* Add button to sync orders immediately

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

* Wrap AS function calls to avoid errors in unit tests

* Use WC_Queue methods instead of AS functions

* Add a filter to customize the sync check time interval

* Ensure retrieved pending sync count is not a cached value

* Change sync button style to link

* Formatting fixes

* Fix unit test

* Add BatchProcessingController dependency to DataSynchronizer

* Add background sync functionality to DataSynchronizer

* Tweaks to watchdog scheduling in batch processor

* Adds a filter to modify the amount of time the watchdog schedule
  gets delayed when calling the scheduler using `$with_delay`
* Adds a check to ensure the scheduler doesn't overwrite an existing
  scheduled event. This is because the scheduler was getting called
  multiple times in a request, first without `$with_delay` (so the event
  would run right away) and then again with `$with_delay`, so that the
  event would then be delayed by an hour. The result was that the event
  was always scheduled for an hour later, even when we want it to run
  right away.

* Improve query arg for running sync via UI

* Remove sync check from COTController, add message about background sync

* Abstract the removal of background sync events

* Remove accidental test code

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

* Ensure bg sync event is not scheduled when in continuous mode

* Add missing @since comments on new filter hooks

* Add caching to check for scheduled bg sync events

* Use constants for bg sync modes

* Add unit tests

* Switch from init hook to shutdown

* Switch bg sync settings from filters to options

* Update unit tests

* phpcs fix

* Tweak "Background sync is enabled" message

Only show this if it is enabled while real-time sync is disabled.
Otherwise the message could be confusing to users who just check the
"Compatibility mode" box, since background sync doesn't have any UI.

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Corey McKrill 2023-09-27 01:14:23 -07:00 committed by GitHub
parent 7b9abcc79a
commit c752c60fd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 416 additions and 49 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Add a background sync that can run independently of the normal real-time HPOS data sync. Also add a button on the Features screen to trigger an order sync manually.

View File

@ -99,14 +99,27 @@ class BatchProcessingController {
* @param bool $unique Whether to make the action unique. * @param bool $unique Whether to make the action unique.
*/ */
private function schedule_watchdog_action( bool $with_delay = false, bool $unique = false ): void { private function schedule_watchdog_action( bool $with_delay = false, bool $unique = false ): void {
$time = $with_delay ? time() + HOUR_IN_SECONDS : time(); $time = time();
as_schedule_single_action( if ( $with_delay ) {
$time, /**
self::WATCHDOG_ACTION_NAME, * Modify the delay interval for the batch processor's watchdog events.
array(), *
self::ACTION_GROUP, * @since 8.2.0
$unique *
); * @param int $delay Time, in seconds, before the watchdog process will run. Defaults to 3600 (1 hour).
*/
$time += apply_filters( 'woocommerce_batch_processor_watchdog_delay_seconds', HOUR_IN_SECONDS );
}
if ( ! as_has_scheduled_action( self::WATCHDOG_ACTION_NAME ) ) {
as_schedule_single_action(
$time,
self::WATCHDOG_ACTION_NAME,
array(),
self::ACTION_GROUP,
$unique
);
}
} }
/** /**

View File

@ -12,6 +12,7 @@ use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\Features\FeaturesController; use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\PluginUtil; use Automattic\WooCommerce\Utilities\PluginUtil;
use ActionScheduler;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
@ -27,6 +28,8 @@ class CustomOrdersTableController {
use AccessiblePrivateMethods; use AccessiblePrivateMethods;
private const SYNC_QUERY_ARG = 'wc_hpos_sync_now';
/** /**
* The name of the option for enabling the usage of the custom orders tables * The name of the option for enabling the usage of the custom orders tables
*/ */
@ -116,10 +119,11 @@ class CustomOrdersTableController {
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 999, 1 ); self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 999, 1 );
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 ); self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 ); self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 );
self::add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'handle_data_sync_option_changed' ), 10, 1 );
self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 ); self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 );
self::add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'handle_feature_enabled_changed' ), 10, 2 ); self::add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'handle_feature_enabled_changed' ), 10, 2 );
self::add_action( 'woocommerce_feature_setting', array( $this, 'get_hpos_feature_setting' ), 10, 2 ); self::add_action( 'woocommerce_feature_setting', array( $this, 'get_hpos_feature_setting' ), 10, 2 );
self::add_action( 'woocommerce_sections_advanced', array( $this, 'sync_now' ) );
self::add_filter( 'removable_query_args', array( $this, 'register_removable_query_arg' ) );
} }
/** /**
@ -326,32 +330,34 @@ class CustomOrdersTableController {
} }
/** /**
* Handler for the all settings updated hook. * Callback to trigger a sync immediately by clicking a button on the Features screen.
* *
* @param string $feature_id Feature ID. * @return void
*/ */
private function handle_data_sync_option_changed( string $feature_id ) { private function sync_now() {
if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION !== $feature_id ) { $section = filter_input( INPUT_GET, 'section' );
if ( 'features' !== $section ) {
return; return;
} }
$data_sync_is_enabled = $this->data_synchronizer->data_sync_is_enabled();
if ( ! $this->data_synchronizer->check_orders_table_exists() ) { if ( filter_input( INPUT_GET, self::SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
$this->data_synchronizer->create_database_tables();
}
// Enabling/disabling the sync implies starting/stopping it too, if needed.
// We do this check here, and not in process_pre_update_option, so that if for some reason
// the setting is enabled but no sync is in process, sync will start by just saving the
// settings even without modifying them (and the opposite: sync will be stopped if for
// some reason it was ongoing while it was disabled).
if ( $data_sync_is_enabled ) {
$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class ); $this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
} else {
$this->batch_processing_controller->remove_processor( DataSynchronizer::class );
} }
} }
/**
* Tell WP Admin to remove the sync query arg from the URL.
*
* @param array $query_args The query args that are removable.
*
* @return array
*/
private function register_removable_query_arg( $query_args ) {
$query_args[] = self::SYNC_QUERY_ARG;
return $query_args;
}
/** /**
* Handle the 'woocommerce_feature_enabled_changed' action, * Handle the 'woocommerce_feature_enabled_changed' action,
* if the custom orders table feature is enabled create the database tables if they don't exist. * if the custom orders table feature is enabled create the database tables if they don't exist.
@ -474,24 +480,45 @@ class CustomOrdersTableController {
*/ */
private function get_hpos_setting_for_sync( $sync_status ) { private function get_hpos_setting_for_sync( $sync_status ) {
$sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) ); $sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) );
$sync_enabled = get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION ); $sync_enabled = $this->data_synchronizer->data_sync_is_enabled();
$sync_message = ''; $sync_message = array();
if ( ! $sync_enabled && $this->data_synchronizer->background_sync_is_enabled() ) {
$sync_message[] = __( 'Background sync is enabled.', 'woocommerce' );
}
if ( $sync_in_progress && $sync_status['current_pending_count'] > 0 ) { if ( $sync_in_progress && $sync_status['current_pending_count'] > 0 ) {
$sync_message = sprintf( $sync_message[] = sprintf(
// translators: %d: number of pending orders. // translators: %d: number of pending orders.
__( 'Currently syncing orders... %d pending', 'woocommerce' ), __( 'Currently syncing orders... %d pending', 'woocommerce' ),
$sync_status['current_pending_count'] $sync_status['current_pending_count']
); );
} elseif ( $sync_status['current_pending_count'] > 0 ) { } elseif ( $sync_status['current_pending_count'] > 0 ) {
$sync_message = sprintf( $sync_now_url = add_query_arg(
// translators: %d: number of pending orders. array(
_n( self::SYNC_QUERY_ARG => true,
'%d order pending to be synchronized. You can switch order data storage <strong>only when the posts and orders tables are in sync</strong>.',
'%d orders pending to be synchronized. You can switch order data storage <strong>only when the posts and orders tables are in sync</strong>.',
$sync_status['current_pending_count'],
'woocommerce'
), ),
$sync_status['current_pending_count'], wc_get_container()->get( FeaturesController::class )->get_features_page_url()
);
$sync_message[] = wp_kses_data( __(
'You can switch order data storage <strong>only when the posts and orders tables are in sync</strong>.',
'woocommerce'
) );
$sync_message[] = sprintf(
'<a href="%1$s" class="button button-link">%2$s</a>',
esc_url( $sync_now_url ),
sprintf(
// translators: %d: number of pending orders.
_n(
'Sync %s pending order',
'Sync %s pending orders',
$sync_status['current_pending_count'],
'woocommerce'
),
number_format_i18n( $sync_status['current_pending_count'] )
)
); );
} }
@ -500,8 +527,8 @@ class CustomOrdersTableController {
'title' => '', 'title' => '',
'type' => 'checkbox', 'type' => 'checkbox',
'desc' => __( 'Enable compatibility mode (synchronizes orders to the posts table).', 'woocommerce' ), 'desc' => __( 'Enable compatibility mode (synchronizes orders to the posts table).', 'woocommerce' ),
'value' => $sync_enabled, 'value' => $sync_enabled ? 'yes' : 'no',
'desc_tip' => $sync_message, 'desc_tip' => implode( '<br />', $sync_message ),
'row_class' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, 'row_class' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
); );
} }

View File

@ -8,7 +8,7 @@ namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\WooCommerce\Caches\OrderCache; use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Caches\OrderCacheController; use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController; use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface; use Automattic\WooCommerce\Internal\BatchProcessing\{ BatchProcessingController, BatchProcessorInterface };
use Automattic\WooCommerce\Internal\Features\FeaturesController; use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
@ -45,6 +45,13 @@ class DataSynchronizer implements BatchProcessorInterface {
public const ID_TYPE_DELETED_FROM_ORDERS_TABLE = 3; public const ID_TYPE_DELETED_FROM_ORDERS_TABLE = 3;
public const ID_TYPE_DELETED_FROM_POSTS_TABLE = 4; public const ID_TYPE_DELETED_FROM_POSTS_TABLE = 4;
public const BACKGROUND_SYNC_MODE_OPTION = 'woocommerce_custom_orders_table_background_sync_mode';
public const BACKGROUND_SYNC_INTERVAL_OPTION = 'woocommerce_custom_orders_table_background_sync_interval';
public const BACKGROUND_SYNC_MODE_INTERVAL = 'interval';
public const BACKGROUND_SYNC_MODE_CONTINUOUS = 'continuous';
public const BACKGROUND_SYNC_MODE_OFF = 'off';
public const BACKGROUND_SYNC_EVENT_HOOK = 'woocommerce_custom_orders_table_background_sync';
/** /**
* The data store object to use. * The data store object to use.
* *
@ -80,6 +87,13 @@ class DataSynchronizer implements BatchProcessorInterface {
*/ */
private $order_cache_controller; private $order_cache_controller;
/**
* The batch processing controller.
*
* @var BatchProcessingController
*/
private $batch_processing_controller;
/** /**
* Class constructor. * Class constructor.
*/ */
@ -89,6 +103,13 @@ class DataSynchronizer implements BatchProcessorInterface {
self::add_action( 'woocommerce_refund_created', array( $this, 'handle_updated_order' ), 100 ); self::add_action( 'woocommerce_refund_created', array( $this, 'handle_updated_order' ), 100 );
self::add_action( 'woocommerce_update_order', array( $this, 'handle_updated_order' ), 100 ); self::add_action( 'woocommerce_update_order', array( $this, 'handle_updated_order' ), 100 );
self::add_action( 'wp_scheduled_auto_draft_delete', array( $this, 'delete_auto_draft_orders' ), 9 ); self::add_action( 'wp_scheduled_auto_draft_delete', array( $this, 'delete_auto_draft_orders' ), 9 );
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
self::add_filter( 'added_option', array( $this, 'process_added_option' ), 999, 2 );
self::add_filter( 'deleted_option', array( $this, 'process_deleted_option' ), 999 );
self::add_action( self::BACKGROUND_SYNC_EVENT_HOOK, array( $this, 'handle_interval_background_sync' ) );
if ( self::BACKGROUND_SYNC_MODE_CONTINUOUS === $this->get_background_sync_mode() ) {
self::add_action( 'shutdown', array( $this, 'handle_continuous_background_sync' ) );
}
self::add_filter( 'woocommerce_feature_description_tip', array( $this, 'handle_feature_description_tip' ), 10, 3 ); self::add_filter( 'woocommerce_feature_description_tip', array( $this, 'handle_feature_description_tip' ), 10, 3 );
} }
@ -101,6 +122,7 @@ class DataSynchronizer implements BatchProcessorInterface {
* @param PostsToOrdersMigrationController $posts_to_cot_migrator The posts to COT migration class to use. * @param PostsToOrdersMigrationController $posts_to_cot_migrator The posts to COT migration class to use.
* @param LegacyProxy $legacy_proxy The legacy proxy instance to use. * @param LegacyProxy $legacy_proxy The legacy proxy instance to use.
* @param OrderCacheController $order_cache_controller The order cache controller instance to use. * @param OrderCacheController $order_cache_controller The order cache controller instance to use.
* @param BatchProcessingController $batch_processing_controller The batch processing controller to use.
* @internal * @internal
*/ */
final public function init( final public function init(
@ -108,13 +130,15 @@ class DataSynchronizer implements BatchProcessorInterface {
DatabaseUtil $database_util, DatabaseUtil $database_util,
PostsToOrdersMigrationController $posts_to_cot_migrator, PostsToOrdersMigrationController $posts_to_cot_migrator,
LegacyProxy $legacy_proxy, LegacyProxy $legacy_proxy,
OrderCacheController $order_cache_controller OrderCacheController $order_cache_controller,
BatchProcessingController $batch_processing_controller
) { ) {
$this->data_store = $data_store; $this->data_store = $data_store;
$this->database_util = $database_util; $this->database_util = $database_util;
$this->posts_to_cot_migrator = $posts_to_cot_migrator; $this->posts_to_cot_migrator = $posts_to_cot_migrator;
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' ); $this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
$this->order_cache_controller = $order_cache_controller; $this->order_cache_controller = $order_cache_controller;
$this->batch_processing_controller = $batch_processing_controller;
} }
/** /**
@ -171,7 +195,7 @@ class DataSynchronizer implements BatchProcessorInterface {
} }
/** /**
* Is the data sync between old and new tables currently enabled? * Is the real-time data sync between old and new tables currently enabled?
* *
* @return bool * @return bool
*/ */
@ -179,6 +203,181 @@ class DataSynchronizer implements BatchProcessorInterface {
return 'yes' === get_option( self::ORDERS_DATA_SYNC_ENABLED_OPTION ); return 'yes' === get_option( self::ORDERS_DATA_SYNC_ENABLED_OPTION );
} }
/**
* Get the current background data sync mode.
*
* @return string
*/
public function get_background_sync_mode(): string {
$default = $this->data_sync_is_enabled() ? self::BACKGROUND_SYNC_MODE_INTERVAL : self::BACKGROUND_SYNC_MODE_OFF;
return get_option( self::BACKGROUND_SYNC_MODE_OPTION, $default );
}
/**
* Is the background data sync between old and new tables currently enabled?
*
* @return bool
*/
public function background_sync_is_enabled(): bool {
$enabled_modes = array( self::BACKGROUND_SYNC_MODE_INTERVAL, self::BACKGROUND_SYNC_MODE_CONTINUOUS );
$mode = $this->get_background_sync_mode();
return in_array( $mode, $enabled_modes, true );
}
/**
* Process an option change for specific keys.
*
* @param string $option_key The option key.
* @param string $old_value The previous value.
* @param string $new_value The new value.
*
* @return void
*/
private function process_updated_option( $option_key, $old_value, $new_value ) {
$sync_option_keys = array( self::ORDERS_DATA_SYNC_ENABLED_OPTION, self::BACKGROUND_SYNC_MODE_OPTION );
if ( ! in_array( $option_key, $sync_option_keys, true ) || $new_value === $old_value ) {
return;
}
if ( self::BACKGROUND_SYNC_MODE_OPTION === $option_key ) {
$mode = $new_value;
} else {
$mode = $this->get_background_sync_mode();
}
switch ( $mode ) {
case self::BACKGROUND_SYNC_MODE_INTERVAL:
$this->schedule_background_sync();
break;
case self::BACKGROUND_SYNC_MODE_CONTINUOUS:
case self::BACKGROUND_SYNC_MODE_OFF:
default:
$this->unschedule_background_sync();
break;
}
if ( self::ORDERS_DATA_SYNC_ENABLED_OPTION === $option_key ) {
if ( ! $this->check_orders_table_exists() ) {
$this->create_database_tables();
}
if ( $this->data_sync_is_enabled() ) {
$this->batch_processing_controller->enqueue_processor( self::class );
} else {
$this->batch_processing_controller->remove_processor( self::class );
}
}
}
/**
* Process an option change when the key didn't exist before.
*
* @param string $option_key The option key.
* @param string $value The new value.
*
* @return void
*/
private function process_added_option( $option_key, $value ) {
$this->process_updated_option( $option_key, false, $value );
}
/**
* Process an option deletion for specific keys.
*
* @param string $option_key The option key.
*
* @return void
*/
private function process_deleted_option( $option_key ) {
if ( self::BACKGROUND_SYNC_MODE_OPTION !== $option_key ) {
return;
}
$this->unschedule_background_sync();
$this->batch_processing_controller->remove_processor( self::class );
}
/**
* Get the time interval, in seconds, between background syncs.
*
* @return int
*/
public function get_background_sync_interval(): int {
$interval = filter_var(
get_option( self::BACKGROUND_SYNC_INTERVAL_OPTION, HOUR_IN_SECONDS ),
FILTER_VALIDATE_INT,
array(
'options' => array(
'default' => HOUR_IN_SECONDS,
),
)
);
return $interval;
}
/**
* Schedule an event to run background sync when the mode is set to interval.
*
* @return void
*/
private function schedule_background_sync() {
$interval = $this->get_background_sync_interval();
// Calling Action Scheduler directly because WC_Action_Queue doesn't support the unique parameter yet.
as_schedule_recurring_action(
time() + $interval,
$interval,
self::BACKGROUND_SYNC_EVENT_HOOK,
array(),
'',
true
);
}
/**
* Remove any pending background sync events.
*
* @return void
*/
private function unschedule_background_sync() {
WC()->queue()->cancel_all( self::BACKGROUND_SYNC_EVENT_HOOK );
}
/**
* Callback to check for pending syncs and enqueue the background data sync processor when in interval mode.
*
* @return void
*/
private function handle_interval_background_sync() {
if ( self::BACKGROUND_SYNC_MODE_INTERVAL !== $this->get_background_sync_mode() ) {
$this->unschedule_background_sync();
return;
}
$pending_count = $this->get_total_pending_count();
if ( $pending_count > 0 ) {
$this->batch_processing_controller->enqueue_processor( self::class );
}
}
/**
* Callback to keep the background data sync processor enqueued when in continuous mode.
*
* @return void
*/
private function handle_continuous_background_sync() {
if ( self::BACKGROUND_SYNC_MODE_CONTINUOUS !== $this->get_background_sync_mode() ) {
$this->batch_processing_controller->remove_processor( self::class );
return;
}
// This method already checks if a processor is enqueued before adding it to avoid duplication.
$this->batch_processing_controller->enqueue_processor( self::class );
}
/** /**
* Get the current sync process status. * Get the current sync process status.
* The information is meaningful only if pending_data_sync_is_in_progress return true. * The information is meaningful only if pending_data_sync_is_in_progress return true.

View File

@ -58,6 +58,7 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
PostsToOrdersMigrationController::class, PostsToOrdersMigrationController::class,
LegacyProxy::class, LegacyProxy::class,
OrderCacheController::class, OrderCacheController::class,
BatchProcessingController::class,
) )
); );
$this->share( OrdersTableRefundDataStore::class )->addArguments( array( OrdersTableDataStoreMeta::class, DatabaseUtil::class, LegacyProxy::class ) ); $this->share( OrdersTableRefundDataStore::class )->addArguments( array( OrdersTableDataStoreMeta::class, DatabaseUtil::class, LegacyProxy::class ) );

View File

@ -1053,7 +1053,7 @@ class FeaturesController {
* *
* @return string * @return string
*/ */
private function get_features_page_url(): string { public function get_features_page_url(): string {
return admin_url( 'admin.php?page=wc-settings&tab=advanced&section=features' ); return admin_url( 'admin.php?page=wc-settings&tab=advanced&section=features' );
} }

View File

@ -580,6 +580,129 @@ class DataSynchronizerTests extends HposTestCase {
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- This is a test. // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- This is a test.
$sync_setting = apply_filters( 'woocommerce_feature_setting', array(), $this->sut::ORDERS_DATA_SYNC_ENABLED_OPTION ); $sync_setting = apply_filters( 'woocommerce_feature_setting', array(), $this->sut::ORDERS_DATA_SYNC_ENABLED_OPTION );
$this->assertEquals( $sync_setting['value'], 'no' ); $this->assertEquals( $sync_setting['value'], 'no' );
$this->assertTrue( str_contains( $sync_setting['desc_tip'], '1 order pending to be synchronized' ) ); $this->assertTrue( str_contains( $sync_setting['desc_tip'], 'Sync 1 pending order' ) );
}
/**
* Delete an order directly from the wc_orders table so that the usual hooks and filters don't run.
*
* @param int $order_id The ID of the order to delete.
*
* @return void
*/
private function direct_delete_cot_order( $order_id ) {
global $wpdb;
$wpdb->delete(
OrdersTableDataStore::get_orders_table_name(),
array( 'id' => $order_id ),
array( '%d' )
);
}
/**
* @testDox When data sync is enabled, there should be a background sync process scheduled, and running it should
* enqueue the DataSynchronizer batch processor when there are pending orders.
*/
public function test_bg_sync_while_sync_enabled() {
$reflection = new ReflectionClass( get_class( $this->sut ) );
$process_method = $reflection->getMethod( 'process_updated_option' );
$process_method->setAccessible( true );
$this->toggle_cot_authoritative( false );
$this->assertFalse( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
$this->assertFalse( as_has_scheduled_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK ) );
$this->enable_cot_sync();
$process_method->invoke( $this->sut, $this->sut::ORDERS_DATA_SYNC_ENABLED_OPTION, false, 'yes' );
$this->assertTrue( as_has_scheduled_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK ) );
$this->assertTrue( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
wc_get_container()->get( BatchProcessingController::class )->remove_processor( DataSynchronizer::class );
$this->assertFalse( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- This is a test.
do_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK );
$this->assertFalse( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
$cot_order = OrderHelper::create_complex_data_store_order();
$this->direct_delete_cot_order( $cot_order->get_id() );
$this->assertEquals( 1, $this->sut->get_total_pending_count() );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- This is a test.
do_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK );
$this->assertTrue( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
$this->disable_cot_sync();
$process_method->invoke( $this->sut, $this->sut::ORDERS_DATA_SYNC_ENABLED_OPTION, 'yes', 'no' );
$this->assertFalse( as_has_scheduled_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK ) );
}
/**
* @testDox When data sync is disabled, but background sync is enabled, there should be a background sync process
* scheduled, and running it should enqueue the DataSynchronizer batch processor when there are pending orders.
*/
public function test_bg_sync_while_sync_disabled_interval_mode() {
$reflection = new ReflectionClass( get_class( $this->sut ) );
$process_method = $reflection->getMethod( 'process_updated_option' );
$process_method->setAccessible( true );
$this->toggle_cot_authoritative( false );
$this->disable_cot_sync();
$process_method->invoke( $this->sut, $this->sut::ORDERS_DATA_SYNC_ENABLED_OPTION, false, 'no' );
$this->assertFalse( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
$this->assertFalse( as_has_scheduled_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK ) );
update_option( $this->sut::BACKGROUND_SYNC_MODE_OPTION, $this->sut::BACKGROUND_SYNC_MODE_INTERVAL );
$this->assertTrue( as_has_scheduled_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK ) );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- This is a test.
do_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK );
$this->assertFalse( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
$this->enable_cot_sync();
$cot_order = OrderHelper::create_complex_data_store_order();
$this->disable_cot_sync();
$this->direct_delete_cot_order( $cot_order->get_id() );
$this->assertEquals( 1, $this->sut->get_total_pending_count() );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- This is a test.
do_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK );
$this->assertTrue( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
update_option( $this->sut::BACKGROUND_SYNC_MODE_OPTION, $this->sut::BACKGROUND_SYNC_MODE_OFF );
$this->assertFalse( as_has_scheduled_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK ) );
}
/**
* @testDox When data sync is disabled, but background sync is enabled and set to continuous mode, there should not
* be a background sync process scheduled, but the DataSynchronizer batch processor should be enqueued anyway.
*/
public function test_bg_sync_while_sync_disabled_continuous_mode() {
$reflection = new ReflectionClass( get_class( $this->sut ) );
$process_method = $reflection->getMethod( 'process_updated_option' );
$process_method->setAccessible( true );
$handler_method = $reflection->getMethod( 'handle_continuous_background_sync' );
$handler_method->setAccessible( true );
$this->toggle_cot_authoritative( false );
$this->disable_cot_sync();
$process_method->invoke( $this->sut, $this->sut::ORDERS_DATA_SYNC_ENABLED_OPTION, false, 'no' );
$this->assertFalse( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
$this->assertFalse( as_has_scheduled_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK ) );
update_option( $this->sut::BACKGROUND_SYNC_MODE_OPTION, $this->sut::BACKGROUND_SYNC_MODE_CONTINUOUS );
$handler_method->invoke( $this->sut );
$this->assertTrue( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
$this->assertFalse( as_has_scheduled_action( $this->sut::BACKGROUND_SYNC_EVENT_HOOK ) );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- This is a test.
do_action( wc_get_container()->get( BatchProcessingController::class )::PROCESS_SINGLE_BATCH_ACTION_NAME, get_class( $this->sut ) );
$this->assertFalse( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
$handler_method->invoke( $this->sut );
$this->assertTrue( wc_get_container()->get( BatchProcessingController::class )->is_enqueued( DataSynchronizer::class ) );
} }
} }