2019-02-08 20:52:38 +00:00
< ? php
/**
* Report table sync related functions and actions .
*
* @ package WooCommerce Admin / Classes
*/
defined ( 'ABSPATH' ) || exit ;
/**
* WC_Admin_Reports_Sync Class .
*/
class WC_Admin_Reports_Sync {
/**
* Action hook for reducing a range of batches down to single actions .
*/
const QUEUE_BATCH_ACTION = 'wc-admin_queue_batches' ;
/**
* Action hook for queuing an action after another is complete .
*/
const QUEUE_DEPEDENT_ACTION = 'wc-admin_queue_dependent_action' ;
/**
2019-04-15 08:07:43 +00:00
* Action hook for importing a batch of customers .
2019-02-08 20:52:38 +00:00
*/
2019-04-15 08:07:43 +00:00
const CUSTOMERS_IMPORT_BATCH_ACTION = 'wc-admin_import_customers_batch' ;
2019-02-08 20:52:38 +00:00
/**
2019-04-15 08:07:43 +00:00
* Action hook for initializing the orders lookup batch creation .
2019-02-08 20:52:38 +00:00
*/
2019-04-15 08:07:43 +00:00
const CUSTOMERS_DELETE_BATCH_INIT = 'wc-admin_delete_customers_batch_init' ;
/**
2019-04-30 19:03:46 +00:00
* Action hook for deleting a batch of customers .
2019-04-15 08:07:43 +00:00
*/
const CUSTOMERS_DELETE_BATCH_ACTION = 'wc-admin_delete_customers_batch' ;
/**
* Action hook for importing a batch of orders .
*/
const ORDERS_IMPORT_BATCH_ACTION = 'wc-admin_import_orders_batch' ;
2019-02-08 20:52:38 +00:00
/**
* Action hook for initializing the orders lookup batch creation .
*/
2019-04-15 08:07:43 +00:00
const ORDERS_IMPORT_BATCH_INIT = 'wc-admin_orders_lookup_import_batch_init' ;
/**
* Action hook for initializing the orders lookup batch deletion .
*/
const ORDERS_DELETE_BATCH_INIT = 'wc-admin_orders_lookup_delete_batch_init' ;
2019-02-08 20:52:38 +00:00
/**
2019-04-15 08:07:43 +00:00
* Action hook for deleting a batch of orders .
2019-02-08 20:52:38 +00:00
*/
2019-04-15 08:07:43 +00:00
const ORDERS_DELETE_BATCH_ACTION = 'wc-admin_delete_orders_batch' ;
/**
* Action hook for importing a batch of orders .
*/
const SINGLE_ORDER_IMPORT_ACTION = 'wc-admin_import_order' ;
2019-02-08 20:52:38 +00:00
2019-02-21 19:44:35 +00:00
/**
2019-02-28 21:47:58 +00:00
* Action scheduler group .
2019-02-21 19:44:35 +00:00
*/
const QUEUE_GROUP = 'wc-admin-data' ;
2019-02-08 20:52:38 +00:00
/**
* Queue instance .
*
* @ var WC_Queue_Interface
*/
protected static $queue = null ;
/**
* Get queue instance .
*
* @ return WC_Queue_Interface
*/
public static function queue () {
if ( is_null ( self :: $queue ) ) {
self :: $queue = WC () -> queue ();
}
return self :: $queue ;
}
/**
* Set queue instance .
*
* @ param WC_Queue_Interface $queue Queue instance .
*/
public static function set_queue ( $queue ) {
self :: $queue = $queue ;
}
/**
* Hook in sync methods .
*/
public static function init () {
// Initialize syncing hooks.
add_action ( 'wp_loaded' , array ( __CLASS__ , 'orders_lookup_update_init' ) );
// Initialize scheduled action handlers.
2019-04-12 05:52:44 +00:00
add_action ( self :: QUEUE_BATCH_ACTION , array ( __CLASS__ , 'queue_batches' ), 10 , 4 );
2019-02-08 20:52:38 +00:00
add_action ( self :: QUEUE_DEPEDENT_ACTION , array ( __CLASS__ , 'queue_dependent_action' ), 10 , 3 );
2019-04-15 08:07:43 +00:00
add_action ( self :: CUSTOMERS_IMPORT_BATCH_ACTION , array ( __CLASS__ , 'customer_lookup_import_batch' ), 10 , 3 );
add_action ( self :: CUSTOMERS_DELETE_BATCH_INIT , array ( __CLASS__ , 'customer_lookup_delete_batch_init' ) );
add_action ( self :: CUSTOMERS_DELETE_BATCH_ACTION , array ( __CLASS__ , 'customer_lookup_delete_batch' ) );
add_action ( self :: ORDERS_IMPORT_BATCH_ACTION , array ( __CLASS__ , 'orders_lookup_import_batch' ), 10 , 4 );
add_action ( self :: ORDERS_IMPORT_BATCH_INIT , array ( __CLASS__ , 'orders_lookup_import_batch_init' ), 10 , 3 );
add_action ( self :: ORDERS_DELETE_BATCH_ACTION , array ( __CLASS__ , 'orders_lookup_delete_batch' ), 10 , 4 );
add_action ( self :: ORDERS_DELETE_BATCH_INIT , array ( __CLASS__ , 'orders_lookup_delete_batch_init' ), 10 , 3 );
add_action ( self :: SINGLE_ORDER_IMPORT_ACTION , array ( __CLASS__ , 'orders_lookup_import_order' ) );
2019-02-08 20:52:38 +00:00
}
/**
* Regenerate data for reports .
2019-04-10 09:51:12 +00:00
*
2019-04-15 08:07:43 +00:00
* @ param int | bool $days Number of days to import .
2019-04-10 09:51:12 +00:00
* @ param bool $skip_existing Skip exisiting records .
* @ return string
2019-02-08 20:52:38 +00:00
*/
2019-04-10 09:51:12 +00:00
public static function regenerate_report_data ( $days , $skip_existing ) {
2019-06-13 15:32:58 +00:00
if ( self :: is_importing () ) {
return new WP_Error ( 'wc_admin_import_in_progress' , __ ( 'An import is already in progress. Please allow the previous import to complete before beginning a new one.' , 'woocommerce-admin' ) );
}
2019-05-13 02:30:07 +00:00
self :: reset_import_stats ( $days , $skip_existing );
2019-04-15 08:07:43 +00:00
self :: customer_lookup_import_batch_init ( $days , $skip_existing );
self :: queue_dependent_action ( self :: ORDERS_IMPORT_BATCH_INIT , array ( $days , $skip_existing ), self :: CUSTOMERS_IMPORT_BATCH_ACTION );
2019-03-06 05:32:05 +00:00
2019-03-13 17:14:02 +00:00
return __ ( 'Report table data is being rebuilt. Please allow some time for data to fully populate.' , 'woocommerce-admin' );
2019-02-08 20:52:38 +00:00
}
2019-05-13 02:30:07 +00:00
/**
* Update the import stat totals and counts .
*
* @ param int | bool $days Number of days to import .
* @ param bool $skip_existing Skip exisiting records .
*/
public static function reset_import_stats ( $days , $skip_existing ) {
$totals = self :: get_import_totals ( $days , $skip_existing );
update_option ( 'wc_admin_import_customers_count' , 0 );
update_option ( 'wc_admin_import_orders_count' , 0 );
update_option ( 'wc_admin_import_customers_total' , $totals [ 'customers' ] );
update_option ( 'wc_admin_import_orders_total' , $totals [ 'orders' ] );
// Update imported from date if older than previous.
$previous_import_date = get_option ( 'wc_admin_imported_from_date' );
$current_import_date = $days ? date ( 'Y-m-d 00:00:00' , time () - ( DAY_IN_SECONDS * $days ) ) : - 1 ;
if ( ! $previous_import_date || - 1 === $current_import_date || new DateTime ( $previous_import_date ) > new DateTime ( $current_import_date ) ) {
update_option ( 'wc_admin_imported_from_date' , $current_import_date );
}
}
/**
* Get the import totals for customers and orders .
*
* @ param int | bool $days Number of days to import .
* @ param bool $skip_existing Skip exisiting records .
* @ return array
*/
public static function get_import_totals ( $days , $skip_existing ) {
$orders = self :: get_orders ( 1 , 1 , $days , $skip_existing );
2019-06-11 12:47:53 +00:00
$customer_roles = apply_filters ( 'woocommerce_admin_import_customer_roles' , array ( 'customer' ) );
2019-05-13 02:30:07 +00:00
$customer_query = self :: get_user_ids_for_batch (
$days ,
$skip_existing ,
array (
2019-06-11 12:47:53 +00:00
'fields' => 'ID' ,
'number' => 1 ,
'role__in' => $customer_roles ,
2019-05-13 02:30:07 +00:00
)
);
return array (
'customers' => $customer_query -> get_total (),
'orders' => $orders -> total ,
);
}
/**
* Returns true if an import is in progress .
*
* @ return bool
*/
public static function is_importing () {
$pending_jobs = self :: queue () -> search (
array (
'status' => 'pending' ,
'per_page' => 1 ,
'claimed' => false ,
2019-06-11 12:47:53 +00:00
'search' => 'import' ,
2019-05-13 02:30:07 +00:00
'group' => self :: QUEUE_GROUP ,
)
);
return ! empty ( $pending_jobs );
}
2019-02-28 21:47:58 +00:00
/**
* Clears all queued actions .
*/
public static function clear_queued_actions () {
2019-03-09 01:41:59 +00:00
$store = ActionScheduler :: store ();
if ( is_a ( $store , 'WC_Admin_ActionScheduler_WPPostStore' ) ) {
// If we're using our data store, call our bespoke deletion method.
2019-05-13 02:30:07 +00:00
$action_types = array (
self :: QUEUE_BATCH_ACTION ,
self :: QUEUE_DEPEDENT_ACTION ,
self :: CUSTOMERS_IMPORT_BATCH_ACTION ,
self :: CUSTOMERS_DELETE_BATCH_INIT ,
self :: CUSTOMERS_DELETE_BATCH_ACTION ,
self :: ORDERS_IMPORT_BATCH_ACTION ,
self :: ORDERS_IMPORT_BATCH_INIT ,
self :: ORDERS_DELETE_BATCH_INIT ,
self :: ORDERS_DELETE_BATCH_ACTION ,
self :: SINGLE_ORDER_IMPORT_ACTION ,
);
$store -> clear_pending_wcadmin_actions ( $action_types );
2019-03-09 01:41:59 +00:00
} else {
self :: queue () -> cancel_all ( null , array (), self :: QUEUE_GROUP );
2019-02-28 21:47:58 +00:00
}
}
2019-02-08 20:52:38 +00:00
/**
2019-04-15 08:07:43 +00:00
* Delete all data for reports .
*
* @ return string
*/
public static function delete_report_data () {
// Cancel all pending import jobs.
self :: clear_queued_actions ();
// Delete orders in batches.
self :: queue () -> schedule_single ( time () + 5 , self :: ORDERS_DELETE_BATCH_INIT , array (), self :: QUEUE_GROUP );
// Delete customers after order data is deleted.
self :: queue_dependent_action ( self :: CUSTOMERS_DELETE_BATCH_INIT , array (), self :: ORDERS_DELETE_BATCH_INIT );
2019-06-11 12:47:53 +00:00
// Delete import options.
delete_option ( 'wc_admin_import_customers_count' );
delete_option ( 'wc_admin_import_orders_count' );
delete_option ( 'wc_admin_import_customers_total' );
delete_option ( 'wc_admin_import_orders_total' );
delete_option ( 'wc_admin_imported_from_date' );
2019-04-15 08:07:43 +00:00
return __ ( 'Report table data is being deleted.' , 'woocommerce-admin' );
}
/**
* Schedule an action to import a single Order .
2019-02-08 20:52:38 +00:00
*
* @ param int $order_id Order ID .
* @ return void
*/
2019-04-15 08:07:43 +00:00
public static function schedule_single_order_import ( $order_id ) {
2019-05-10 06:39:25 +00:00
if ( 'shop_order' !== get_post_type ( $order_id ) && 'woocommerce_refund_created' !== current_filter () ) {
2019-02-08 20:52:38 +00:00
return ;
}
2019-02-20 02:05:11 +00:00
if ( apply_filters ( 'woocommerce_disable_order_scheduling' , false ) ) {
2019-04-15 08:07:43 +00:00
self :: orders_lookup_import_order ( $order_id );
2019-02-20 02:05:11 +00:00
return ;
}
2019-02-08 20:52:38 +00:00
// This can get called multiple times for a single order, so we look
// for existing pending jobs for the same order to avoid duplicating efforts.
$existing_jobs = self :: queue () -> search (
array (
'status' => 'pending' ,
'per_page' => 1 ,
'claimed' => false ,
'search' => " [ { $order_id } ] " ,
2019-02-21 19:44:35 +00:00
'group' => self :: QUEUE_GROUP ,
2019-02-08 20:52:38 +00:00
)
);
if ( $existing_jobs ) {
$existing_job = current ( $existing_jobs );
// Bail out if there's a pending single order action, or a pending dependent action.
if (
2019-04-15 08:07:43 +00:00
( self :: SINGLE_ORDER_IMPORT_ACTION === $existing_job -> get_hook () ) ||
2019-02-08 20:52:38 +00:00
(
self :: QUEUE_DEPEDENT_ACTION === $existing_job -> get_hook () &&
2019-05-13 16:48:22 +00:00
in_array ( self :: SINGLE_ORDER_IMPORT_ACTION , $existing_job -> get_args (), true )
2019-02-08 20:52:38 +00:00
)
) {
return ;
}
}
2019-04-29 23:07:15 +00:00
// We want to ensure that customer lookup updates are scheduled before order updates.
2019-04-15 08:07:43 +00:00
self :: queue_dependent_action ( self :: SINGLE_ORDER_IMPORT_ACTION , array ( $order_id ), self :: CUSTOMERS_IMPORT_BATCH_ACTION );
2019-02-08 20:52:38 +00:00
}
/**
* Attach order lookup update hooks .
*/
public static function orders_lookup_update_init () {
// Activate WC_Order extension.
WC_Admin_Order :: add_filters ();
2019-05-10 06:39:25 +00:00
WC_Admin_Order_Refund :: add_filters ();
2019-02-08 20:52:38 +00:00
2019-05-10 06:39:25 +00:00
// Order and refund data must be run on these hooks to ensure meta data is set.
2019-04-15 08:07:43 +00:00
add_action ( 'save_post' , array ( __CLASS__ , 'schedule_single_order_import' ) );
2019-05-10 06:39:25 +00:00
add_action ( 'woocommerce_refund_created' , array ( __CLASS__ , 'schedule_single_order_import' ) );
2019-02-08 20:52:38 +00:00
WC_Admin_Reports_Orders_Stats_Data_Store :: init ();
WC_Admin_Reports_Customers_Data_Store :: init ();
WC_Admin_Reports_Coupons_Data_Store :: init ();
WC_Admin_Reports_Products_Data_Store :: init ();
WC_Admin_Reports_Taxes_Data_Store :: init ();
}
/**
* Init order / product lookup tables update ( in batches ) .
2019-04-10 09:51:12 +00:00
*
2019-04-15 08:07:43 +00:00
* @ param integer | boolean $days Number of days to import .
2019-04-10 09:51:12 +00:00
* @ param boolean $skip_existing Skip exisiting records .
2019-02-08 20:52:38 +00:00
*/
2019-04-15 08:07:43 +00:00
public static function orders_lookup_import_batch_init ( $days , $skip_existing ) {
$batch_size = self :: get_batch_size ( self :: ORDERS_IMPORT_BATCH_ACTION );
2019-04-10 09:51:12 +00:00
$orders = self :: get_orders ( 1 , 1 , $days , $skip_existing );
2019-02-08 20:52:38 +00:00
2019-04-10 09:51:12 +00:00
if ( 0 === $orders -> total ) {
2019-02-08 20:52:38 +00:00
return ;
}
2019-04-10 09:51:12 +00:00
$num_batches = ceil ( $orders -> total / $batch_size );
2019-02-08 20:52:38 +00:00
2019-04-15 08:07:43 +00:00
self :: queue_batches ( 1 , $num_batches , self :: ORDERS_IMPORT_BATCH_ACTION , array ( $days , $skip_existing ) );
2019-02-08 20:52:38 +00:00
}
/**
2019-05-14 10:37:09 +00:00
* Get the order / refund IDs and total count that need to be synced .
2019-02-08 20:52:38 +00:00
*
2019-04-10 09:51:12 +00:00
* @ param int $limit Number of records to retrieve .
* @ param int $page Page number .
* @ param int | bool $days Number of days prior to current date to limit search results .
* @ param bool $skip_existing Skip already imported orders .
2019-02-08 20:52:38 +00:00
*/
2019-04-10 09:51:12 +00:00
public static function get_orders ( $limit = 10 , $page = 1 , $days = false , $skip_existing = false ) {
global $wpdb ;
$where_clause = '' ;
2019-06-11 12:47:53 +00:00
$offset = $page > 1 ? ( $page - 1 ) * $limit : 0 ;
2019-04-10 09:51:12 +00:00
2019-06-11 12:47:53 +00:00
if ( is_int ( $days ) ) {
2019-04-17 07:57:51 +00:00
$days_ago = date ( 'Y-m-d 00:00:00' , time () - ( DAY_IN_SECONDS * $days ) );
2019-04-10 09:51:12 +00:00
$where_clause .= " AND post_date >= ' { $days_ago } ' " ;
}
if ( $skip_existing ) {
$where_clause .= " AND NOT EXISTS (
SELECT 1 FROM { $wpdb -> prefix } wc_order_stats
WHERE { $wpdb -> prefix } wc_order_stats . order_id = { $wpdb -> posts } . ID
) " ;
}
2019-04-17 07:50:46 +00:00
$count = $wpdb -> get_var (
" SELECT COUNT(*) FROM { $wpdb -> posts }
2019-05-14 10:37:09 +00:00
WHERE post_type IN ( 'shop_order' , 'shop_order_refund' )
2019-05-20 18:05:14 +00:00
AND post_status NOT IN ( 'auto-draft' , 'trash' )
2019-04-17 07:50:46 +00:00
{ $where_clause } "
); // WPCS: unprepared SQL ok.
$order_ids = absint ( $count ) > 0 ? $wpdb -> get_col (
2019-04-10 09:51:12 +00:00
$wpdb -> prepare (
" SELECT ID FROM { $wpdb -> posts }
2019-05-14 10:37:09 +00:00
WHERE post_type IN ( 'shop_order' , 'shop_order_refund' )
2019-05-20 18:05:14 +00:00
AND post_status NOT IN ( 'auto-draft' , 'trash' )
2019-04-10 09:51:12 +00:00
{ $where_clause }
ORDER BY post_date ASC
LIMIT % d
OFFSET % d " ,
$limit ,
$offset
2019-02-08 20:52:38 +00:00
)
2019-04-17 07:50:46 +00:00
) : array (); // WPCS: unprepared SQL ok.
2019-04-10 09:51:12 +00:00
return ( object ) array (
'total' => absint ( $count ),
'order_ids' => $order_ids ,
2019-02-08 20:52:38 +00:00
);
2019-04-10 09:51:12 +00:00
}
2019-02-08 20:52:38 +00:00
2019-04-10 09:51:12 +00:00
/**
2019-04-15 08:07:43 +00:00
* Imports a batch of orders to update ( stats and products ) .
2019-04-10 09:51:12 +00:00
*
2019-04-15 08:07:43 +00:00
* @ param int $batch_number Batch number to import ( essentially a query page number ) .
* @ param int | bool $days Number of days to import .
2019-04-10 09:51:12 +00:00
* @ param bool $skip_existing Skip exisiting records .
* @ return void
*/
2019-04-15 08:07:43 +00:00
public static function orders_lookup_import_batch ( $batch_number , $days , $skip_existing ) {
$batch_size = self :: get_batch_size ( self :: ORDERS_IMPORT_BATCH_ACTION );
2019-06-11 17:55:10 +00:00
$properties = array (
'batch_number' => $batch_number ,
'batch_size' => $batch_size ,
'type' => 'order' ,
);
self :: record_event ( 'import_job_start' , $properties );
2019-06-12 21:51:56 +00:00
// When we are skipping already imported orders, the table of orders to import gets smaller in
// every batch, so we want to always import the first page.
$page = $skip_existing ? 1 : $batch_number ;
$orders = self :: get_orders ( $batch_size , $page , $days , $skip_existing );
2019-04-10 09:51:12 +00:00
foreach ( $orders -> order_ids as $order_id ) {
2019-04-15 08:07:43 +00:00
self :: orders_lookup_import_order ( $order_id );
2019-02-08 20:52:38 +00:00
}
2019-05-13 02:30:07 +00:00
$imported_count = get_option ( 'wc_admin_import_orders_count' , 0 );
update_option ( 'wc_admin_import_orders_count' , $imported_count + count ( $orders -> order_ids ) );
2019-06-11 17:55:10 +00:00
$properties [ 'imported_count' ] = $imported_count ;
self :: record_event ( 'import_job_complete' , $properties );
2019-02-08 20:52:38 +00:00
}
/**
2019-05-10 06:39:25 +00:00
* Imports a single order or refund to update lookup tables for .
2019-02-08 20:52:38 +00:00
* If an error is encountered in one of the updates , a retry action is scheduled .
*
2019-05-10 06:39:25 +00:00
* @ param int $order_id Order or refund ID .
2019-02-08 20:52:38 +00:00
* @ return void
*/
2019-04-15 08:07:43 +00:00
public static function orders_lookup_import_order ( $order_id ) {
2019-06-14 00:19:01 +00:00
$order = wc_get_order ( $order_id );
// If the order isn't found for some reason, skip the sync.
if ( ! $order ) {
return ;
}
$type = $order -> get_type ();
// If the order isn't the right type, skip sync.
if ( 'shop_order' !== $type && 'shop_order_refund' !== $type ) {
return ;
}
// If the order has no id or date created, skip sync.
if ( ! $order -> get_id () || ! $order -> get_date_created () ) {
return ;
}
2019-02-08 20:52:38 +00:00
$result = array_sum (
array (
WC_Admin_Reports_Orders_Stats_Data_Store :: sync_order ( $order_id ),
WC_Admin_Reports_Products_Data_Store :: sync_order_products ( $order_id ),
WC_Admin_Reports_Coupons_Data_Store :: sync_order_coupons ( $order_id ),
WC_Admin_Reports_Taxes_Data_Store :: sync_order_taxes ( $order_id ),
)
);
// If all updates were either skipped or successful, we're done.
// The update methods return -1 for skip, or a boolean success indicator.
if ( 4 === absint ( $result ) ) {
return ;
}
// Otherwise assume an error occurred and reschedule.
2019-04-15 08:07:43 +00:00
self :: schedule_single_order_import ( $order_id );
2019-02-08 20:52:38 +00:00
}
/**
* Returns the batch size for regenerating reports .
* Note : can differ per batch action .
*
* @ param string $action Single batch action name .
* @ return int Batch size .
*/
public static function get_batch_size ( $action ) {
$batch_sizes = array (
2019-04-15 08:07:43 +00:00
self :: QUEUE_BATCH_ACTION => 100 ,
self :: CUSTOMERS_IMPORT_BATCH_ACTION => 25 ,
self :: CUSTOMERS_DELETE_BATCH_ACTION => 25 ,
self :: ORDERS_IMPORT_BATCH_ACTION => 10 ,
self :: ORDERS_DELETE_BATCH_ACTION => 10 ,
2019-02-08 20:52:38 +00:00
);
$batch_size = isset ( $batch_sizes [ $action ] ) ? $batch_sizes [ $action ] : 25 ;
/**
* Filter the batch size for regenerating a report table .
*
* @ param int $batch_size Batch size .
* @ param string $action Batch action name .
*/
return apply_filters ( 'wc_admin_report_regenerate_batch_size' , $batch_size , $action );
}
/**
* Queue a large number of batch jobs , respecting the batch size limit .
* Reduces a range of batches down to " single batch " jobs .
*
* @ param int $range_start Starting batch number .
* @ param int $range_end Ending batch number .
* @ param string $single_batch_action Action to schedule for a single batch .
2019-04-10 09:51:12 +00:00
* @ param array $action_args Action arguments .
2019-02-08 20:52:38 +00:00
* @ return void
*/
2019-04-10 09:51:12 +00:00
public static function queue_batches ( $range_start , $range_end , $single_batch_action , $action_args = array () ) {
2019-02-08 20:52:38 +00:00
$batch_size = self :: get_batch_size ( self :: QUEUE_BATCH_ACTION );
$range_size = 1 + ( $range_end - $range_start );
$action_timestamp = time () + 5 ;
if ( $range_size > $batch_size ) {
// If the current batch range is larger than a single batch,
// split the range into $queue_batch_size chunks.
$chunk_size = ceil ( $range_size / $batch_size );
for ( $i = 0 ; $i < $batch_size ; $i ++ ) {
$batch_start = $range_start + ( $i * $chunk_size );
$batch_end = min ( $range_end , $range_start + ( $chunk_size * ( $i + 1 ) ) - 1 );
self :: queue () -> schedule_single (
$action_timestamp ,
self :: QUEUE_BATCH_ACTION ,
2019-04-10 09:51:12 +00:00
array ( $batch_start , $batch_end , $single_batch_action , $action_args ),
2019-02-21 19:44:35 +00:00
self :: QUEUE_GROUP
2019-02-08 20:52:38 +00:00
);
}
} else {
// Otherwise, queue the single batches.
for ( $i = $range_start ; $i <= $range_end ; $i ++ ) {
2019-04-12 05:52:44 +00:00
$batch_action_args = array_merge ( array ( $i ), $action_args );
self :: queue () -> schedule_single ( $action_timestamp , $single_batch_action , $batch_action_args , self :: QUEUE_GROUP );
2019-02-08 20:52:38 +00:00
}
}
}
/**
* Queue an action to run after another .
*
* @ param string $action Action to run after prerequisite .
* @ param array $action_args Action arguments .
* @ param string $prerequisite_action Prerequisite action .
*/
public static function queue_dependent_action ( $action , $action_args , $prerequisite_action ) {
$blocking_jobs = self :: queue () -> search (
array (
'status' => 'pending' ,
'orderby' => 'date' ,
'order' => 'DESC' ,
'per_page' => 1 ,
'claimed' => false ,
'search' => $prerequisite_action , // search is used instead of hook to find queued batch creation.
2019-02-21 19:44:35 +00:00
'group' => self :: QUEUE_GROUP ,
2019-02-08 20:52:38 +00:00
)
);
$next_job_schedule = null ;
$blocking_job_hook = null ;
if ( $blocking_jobs ) {
$blocking_job = current ( $blocking_jobs );
$blocking_job_hook = $blocking_job -> get_hook ();
$next_job_schedule = $blocking_job -> get_schedule () -> next ();
}
// Eliminate the false positive scenario where the blocking job is
// actually another queued dependent action awaiting the same prerequisite.
// Also, ensure that the next schedule is a DateTime (it can be null).
if (
is_a ( $next_job_schedule , 'DateTime' ) &&
( self :: QUEUE_DEPEDENT_ACTION !== $blocking_job_hook )
) {
self :: queue () -> schedule_single (
$next_job_schedule -> getTimestamp () + 5 ,
self :: QUEUE_DEPEDENT_ACTION ,
2019-02-21 19:44:35 +00:00
array ( $action , $action_args , $prerequisite_action ),
self :: QUEUE_GROUP
2019-02-08 20:52:38 +00:00
);
} else {
2019-02-21 19:44:35 +00:00
self :: queue () -> schedule_single ( time () + 5 , $action , $action_args , self :: QUEUE_GROUP );
2019-02-08 20:52:38 +00:00
}
}
2019-04-29 23:07:15 +00:00
/**
* Exclude users that already exist in our customer lookup table .
*
* Meant to be hooked into 'pre_user_query' action .
*
* @ param WP_User_Query $wp_user_query WP_User_Query to modify .
*/
public static function exclude_existing_customers_from_query ( $wp_user_query ) {
global $wpdb ;
$wp_user_query -> query_where .= " AND NOT EXISTS (
SELECT ID FROM { $wpdb -> prefix } wc_customer_lookup
WHERE { $wpdb -> prefix } wc_customer_lookup . user_id = { $wpdb -> users } . ID
) " ;
}
/**
* Retrieve user IDs given import criteria .
*
* @ param int | bool $days Number of days to process .
* @ param bool $skip_existing Skip exisiting records .
* @ param array $query_args Optional . WP_User_Query args .
* @ return WP_User_Query
*/
public static function get_user_ids_for_batch ( $days , $skip_existing , $query_args = array () ) {
if ( ! is_array ( $query_args ) ) {
$query_args = array ();
}
2019-06-11 12:47:53 +00:00
if ( is_int ( $days ) ) {
2019-04-29 23:07:15 +00:00
$query_args [ 'date_query' ] = array (
'after' => date ( 'Y-m-d 00:00:00' , time () - ( DAY_IN_SECONDS * $days ) ),
);
}
if ( $skip_existing ) {
add_action ( 'pre_user_query' , array ( __CLASS__ , 'exclude_existing_customers_from_query' ) );
}
$customer_query = new WP_User_Query ( $query_args );
remove_action ( 'pre_user_query' , array ( __CLASS__ , 'exclude_existing_customers_from_query' ) );
return $customer_query ;
}
2019-02-08 20:52:38 +00:00
/**
* Init customer lookup table update ( in batches ) .
2019-04-29 23:07:15 +00:00
*
* @ param int | bool $days Number of days to process .
2019-06-11 12:47:53 +00:00
* @ param bool $skip_existing Skip existing records .
2019-02-08 20:52:38 +00:00
*/
2019-04-15 08:07:43 +00:00
public static function customer_lookup_import_batch_init ( $days , $skip_existing ) {
$batch_size = self :: get_batch_size ( self :: CUSTOMERS_IMPORT_BATCH_ACTION );
2019-05-13 16:47:16 +00:00
$customer_roles = apply_filters ( 'woocommerce_admin_import_customer_roles' , array ( 'customer' ) );
2019-04-29 23:07:15 +00:00
$customer_query = self :: get_user_ids_for_batch (
$days ,
$skip_existing ,
2019-02-08 20:52:38 +00:00
array (
2019-05-13 16:47:16 +00:00
'fields' => 'ID' ,
'number' => 1 ,
'role__in' => $customer_roles ,
2019-02-08 20:52:38 +00:00
)
);
$total_customers = $customer_query -> get_total ();
if ( 0 === $total_customers ) {
return ;
}
2019-03-13 17:14:02 +00:00
$num_batches = ceil ( $total_customers / $batch_size );
2019-02-08 20:52:38 +00:00
2019-04-15 08:07:43 +00:00
self :: queue_batches ( 1 , $num_batches , self :: CUSTOMERS_IMPORT_BATCH_ACTION , array ( $days , $skip_existing ) );
2019-02-08 20:52:38 +00:00
}
/**
* Process a batch of customers to update .
*
2019-04-29 23:07:15 +00:00
* @ param int $batch_number Batch number to process ( essentially a query page number ) .
* @ param int | bool $days Number of days to process .
* @ param bool $skip_existing Skip exisiting records .
2019-02-08 20:52:38 +00:00
* @ return void
*/
2019-04-15 08:07:43 +00:00
public static function customer_lookup_import_batch ( $batch_number , $days , $skip_existing ) {
2019-06-11 17:55:10 +00:00
$batch_size = self :: get_batch_size ( self :: CUSTOMERS_IMPORT_BATCH_ACTION );
$properties = array (
'batch_number' => $batch_number ,
'batch_size' => $batch_size ,
'type' => 'customer' ,
);
self :: record_event ( 'import_job_start' , $properties );
2019-05-13 16:47:16 +00:00
$customer_roles = apply_filters ( 'woocommerce_admin_import_customer_roles' , array ( 'customer' ) );
2019-06-12 21:58:20 +00:00
// When we are skipping already imported customers, the table of customers to import gets smaller in
2019-06-12 21:51:56 +00:00
// every batch, so we want to always import the first page.
$page = $skip_existing ? 1 : $batch_number ;
2019-04-29 23:07:15 +00:00
$customer_query = self :: get_user_ids_for_batch (
$days ,
$skip_existing ,
2019-02-08 20:52:38 +00:00
array (
2019-05-13 16:47:16 +00:00
'fields' => 'ID' ,
'orderby' => 'ID' ,
'order' => 'ASC' ,
'number' => $batch_size ,
2019-06-12 21:51:56 +00:00
'paged' => $page ,
2019-05-13 16:47:16 +00:00
'role__in' => $customer_roles ,
2019-02-08 20:52:38 +00:00
)
);
$customer_ids = $customer_query -> get_results ();
foreach ( $customer_ids as $customer_id ) {
// @todo Schedule single customer update if this fails?
WC_Admin_Reports_Customers_Data_Store :: update_registered_customer ( $customer_id );
}
2019-05-13 02:30:07 +00:00
$imported_count = get_option ( 'wc_admin_import_customers_count' , 0 );
update_option ( 'wc_admin_import_customers_count' , $imported_count + count ( $customer_ids ) );
2019-06-11 17:55:10 +00:00
$properties [ 'imported_count' ] = $imported_count ;
self :: record_event ( 'import_job_complete' , $properties );
2019-02-08 20:52:38 +00:00
}
2019-04-15 08:07:43 +00:00
/**
* Delete customer lookup table rows ( in batches ) .
*/
public static function customer_lookup_delete_batch_init () {
global $wpdb ;
$batch_size = self :: get_batch_size ( self :: CUSTOMERS_DELETE_BATCH_ACTION );
$count = $wpdb -> get_var ( " SELECT COUNT(*) FROM { $wpdb -> prefix } wc_customer_lookup " );
if ( 0 === $count ) {
return ;
}
$num_batches = ceil ( $count / $batch_size );
self :: queue_batches ( 1 , $num_batches , self :: CUSTOMERS_DELETE_BATCH_ACTION );
}
/**
* Delete a batch of customers .
*/
2019-04-30 19:03:46 +00:00
public static function customer_lookup_delete_batch () {
2019-04-15 08:07:43 +00:00
global $wpdb ;
2019-06-11 17:55:10 +00:00
self :: record_event ( 'delete_import_data_job_start' , array ( 'type' => 'customer' ) );
2019-04-15 08:07:43 +00:00
$batch_size = self :: get_batch_size ( self :: CUSTOMERS_DELETE_BATCH_ACTION );
$customer_ids = $wpdb -> get_col (
$wpdb -> prepare (
" SELECT customer_id FROM { $wpdb -> prefix } wc_customer_lookup ORDER BY customer_id ASC LIMIT %d " ,
$batch_size
)
);
foreach ( $customer_ids as $customer_id ) {
WC_Admin_Reports_Customers_Data_Store :: delete_customer ( $customer_id );
}
2019-06-11 17:55:10 +00:00
self :: record_event ( 'delete_import_data_job_complete' , array ( 'type' => 'customer' ) );
2019-04-15 08:07:43 +00:00
}
/**
* Delete orders lookup table rows ( in batches ) .
*/
public static function orders_lookup_delete_batch_init () {
global $wpdb ;
$batch_size = self :: get_batch_size ( self :: ORDERS_DELETE_BATCH_ACTION );
$count = $wpdb -> get_var ( " SELECT COUNT(*) FROM { $wpdb -> prefix } wc_order_stats " );
if ( 0 === $count ) {
return ;
}
$num_batches = ceil ( $count / $batch_size );
self :: queue_batches ( 1 , $num_batches , self :: ORDERS_DELETE_BATCH_ACTION );
}
/**
* Delete a batch of orders .
*
* @ return void
*/
public static function orders_lookup_delete_batch () {
global $wpdb ;
2019-06-11 17:55:10 +00:00
self :: record_event ( 'delete_import_data_job_start' , array ( 'type' => 'order' ) );
2019-04-15 08:07:43 +00:00
$batch_size = self :: get_batch_size ( self :: ORDERS_DELETE_BATCH_ACTION );
$order_ids = $wpdb -> get_col (
$wpdb -> prepare (
" SELECT order_id FROM { $wpdb -> prefix } wc_order_stats ORDER BY order_id ASC LIMIT %d " ,
$batch_size
)
);
foreach ( $order_ids as $order_id ) {
WC_Admin_Reports_Orders_Stats_Data_Store :: delete_order ( $order_id );
}
2019-06-11 17:55:10 +00:00
self :: record_event ( 'delete_import_data_job_complete' , array ( 'type' => 'order' ) );
2019-04-15 08:07:43 +00:00
}
2019-06-11 17:55:10 +00:00
/**
* Record an event using Tracks .
*
* @ internal WooCommerce core only includes Tracks in admin , not the REST API , so we need to include it .
* @ param string $event_name Event name for tracks .
* @ param array $properties Properties to pass along with event .
*/
protected static function record_event ( $event_name , $properties = array () ) {
if ( ! class_exists ( 'WC_Tracks' ) ) {
if ( ! defined ( 'WC_ABSPATH' ) || ! file_exists ( WC_ABSPATH . 'includes/tracks/class-wc-tracks.php' ) ) {
return ;
}
include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks.php' ;
include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-event.php' ;
include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-client.php' ;
include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-footer-pixel.php' ;
include_once WC_ABSPATH . 'includes/tracks/class-wc-site-tracking.php' ;
}
WC_Tracks :: record_event ( $event_name , $properties );
}
2019-02-08 20:52:38 +00:00
}
WC_Admin_Reports_Sync :: init ();