initial commit
This commit is contained in:
273
packages/woocommerce-admin/src/Schedulers/CustomersScheduler.php
Normal file
273
packages/woocommerce-admin/src/Schedulers/CustomersScheduler.php
Normal file
@ -0,0 +1,273 @@
|
||||
<?php
|
||||
/**
|
||||
* Customer syncing related functions and actions.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Schedulers;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
|
||||
use \Automattic\WooCommerce\Admin\Schedulers\OrdersScheduler;
|
||||
|
||||
/**
|
||||
* CustomersScheduler Class.
|
||||
*/
|
||||
class CustomersScheduler extends ImportScheduler {
|
||||
/**
|
||||
* Slug to identify the scheduler.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $name = 'customers';
|
||||
|
||||
/**
|
||||
* Attach customer lookup update hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'woocommerce_new_customer', array( __CLASS__, 'schedule_import' ) );
|
||||
add_action( 'woocommerce_update_customer', array( __CLASS__, 'schedule_import' ) );
|
||||
add_action( 'updated_user_meta', array( __CLASS__, 'schedule_import_via_last_active' ), 10, 3 );
|
||||
add_action( 'woocommerce_privacy_remove_order_personal_data', array( __CLASS__, 'schedule_anonymize' ) );
|
||||
add_action( 'delete_user', array( __CLASS__, 'schedule_user_delete' ) );
|
||||
add_action( 'remove_user_from_blog', array( __CLASS__, 'schedule_user_delete' ) );
|
||||
|
||||
CustomersDataStore::init();
|
||||
parent::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add customer dependencies.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_dependencies() {
|
||||
return array(
|
||||
'delete_batch_init' => OrdersScheduler::get_action( 'delete_batch_init' ),
|
||||
'anonymize' => self::get_action( 'import' ),
|
||||
'delete_user' => self::get_action( 'import' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer IDs and total count that need to be synced.
|
||||
*
|
||||
* @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 customers.
|
||||
*/
|
||||
public static function get_items( $limit = 10, $page = 1, $days = false, $skip_existing = false ) {
|
||||
$customer_roles = apply_filters( 'woocommerce_analytics_import_customer_roles', array( 'customer' ) );
|
||||
$query_args = array(
|
||||
'fields' => 'ID',
|
||||
'orderby' => 'ID',
|
||||
'order' => 'ASC',
|
||||
'number' => $limit,
|
||||
'paged' => $page,
|
||||
'role__in' => $customer_roles,
|
||||
);
|
||||
|
||||
if ( is_int( $days ) ) {
|
||||
$query_args['date_query'] = array(
|
||||
'after' => gmdate( '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 (object) array(
|
||||
'total' => $customer_query->get_total(),
|
||||
'ids' => $customer_query->get_results(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of rows imported.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get_total_imported() {
|
||||
global $wpdb;
|
||||
return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_customer_lookup" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available scheduling actions.
|
||||
* Used to determine action hook names and clear events.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_scheduler_actions() {
|
||||
$actions = parent::get_scheduler_actions();
|
||||
$actions['anonymize'] = 'wc-admin_anonymize_' . static::$name;
|
||||
$actions['delete_user'] = 'wc-admin_delete_user_' . static::$name;
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule import.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function schedule_import( $user_id ) {
|
||||
self::schedule_action( 'import', array( $user_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an import if the "last active" meta value was changed.
|
||||
* Function expects to be hooked into the `updated_user_meta` action.
|
||||
*
|
||||
* @param int $meta_id ID of updated metadata entry.
|
||||
* @param int $user_id ID of the user being updated.
|
||||
* @param string $meta_key Meta key being updated.
|
||||
*/
|
||||
public static function schedule_import_via_last_active( $meta_id, $user_id, $meta_key ) {
|
||||
if ( 'wc_last_active' === $meta_key ) {
|
||||
self::schedule_import( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action to anonymize a single Order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @return void
|
||||
*/
|
||||
public static function schedule_anonymize( $order ) {
|
||||
if ( is_a( $order, 'WC_Order' ) ) {
|
||||
// Postpone until any pending updates are completed.
|
||||
self::schedule_action( 'anonymize', array( $order->get_id() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action to delete a single User.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function schedule_user_delete( $user_id ) {
|
||||
if ( (int) $user_id > 0 && ! doing_action( 'wp_uninitialize_site' ) ) {
|
||||
// Postpone until any pending updates are completed.
|
||||
self::schedule_action( 'delete_user', array( $user_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a single customer.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function import( $user_id ) {
|
||||
CustomersDataStore::update_registered_customer( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of customers.
|
||||
*
|
||||
* @param int $batch_size Number of items to delete.
|
||||
* @return void
|
||||
*/
|
||||
public static function delete( $batch_size ) {
|
||||
global $wpdb;
|
||||
|
||||
$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 ) {
|
||||
CustomersDataStore::delete_customer( $customer_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymize the customer data for a single order.
|
||||
*
|
||||
* @param int $order_id Order id.
|
||||
* @return void
|
||||
*/
|
||||
public static function anonymize( $order_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$customer_id = $wpdb->get_var(
|
||||
$wpdb->prepare( "SELECT customer_id FROM {$wpdb->prefix}wc_order_stats WHERE order_id = %d", $order_id )
|
||||
);
|
||||
|
||||
if ( ! $customer_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Long form query because $wpdb->update rejects [deleted].
|
||||
$deleted_text = __( '[deleted]', 'woocommerce' );
|
||||
$updated = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"UPDATE {$wpdb->prefix}wc_customer_lookup
|
||||
SET
|
||||
user_id = NULL,
|
||||
username = %s,
|
||||
first_name = %s,
|
||||
last_name = %s,
|
||||
email = %s,
|
||||
country = '',
|
||||
postcode = %s,
|
||||
city = %s,
|
||||
state = %s
|
||||
WHERE
|
||||
customer_id = %d",
|
||||
array(
|
||||
$deleted_text,
|
||||
$deleted_text,
|
||||
$deleted_text,
|
||||
'deleted@site.invalid',
|
||||
$deleted_text,
|
||||
$deleted_text,
|
||||
$deleted_text,
|
||||
$customer_id,
|
||||
)
|
||||
)
|
||||
);
|
||||
// If the customer row was anonymized, flush the cache.
|
||||
if ( $updated ) {
|
||||
ReportsCache::invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the customer data for a single user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function delete_user( $user_id ) {
|
||||
CustomersDataStore::delete_customer_by_user_id( $user_id );
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* Import related abstract functions.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Schedulers;
|
||||
|
||||
interface ImportInterface {
|
||||
/**
|
||||
* Get items based on query and return IDs along with total available.
|
||||
*
|
||||
* @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 items.
|
||||
*/
|
||||
public static function get_items( $limit, $page, $days, $skip_existing );
|
||||
|
||||
/**
|
||||
* Get total number of items already imported.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public static function get_total_imported();
|
||||
|
||||
}
|
179
packages/woocommerce-admin/src/Schedulers/ImportScheduler.php
Normal file
179
packages/woocommerce-admin/src/Schedulers/ImportScheduler.php
Normal file
@ -0,0 +1,179 @@
|
||||
<?php
|
||||
/**
|
||||
* Import related functions and actions.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Schedulers;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
|
||||
use \Automattic\WooCommerce\Admin\Schedulers\SchedulerTraits;
|
||||
|
||||
/**
|
||||
* ImportScheduler class.
|
||||
*/
|
||||
abstract class ImportScheduler implements ImportInterface {
|
||||
/**
|
||||
* Import stats option name.
|
||||
*/
|
||||
const IMPORT_STATS_OPTION = 'woocommerce_admin_import_stats';
|
||||
|
||||
/**
|
||||
* Scheduler traits.
|
||||
*/
|
||||
use SchedulerTraits {
|
||||
get_batch_sizes as get_scheduler_batch_sizes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
'search' => 'import',
|
||||
'group' => self::$group,
|
||||
)
|
||||
);
|
||||
if ( empty( $pending_jobs ) ) {
|
||||
$in_progress = self::queue()->search(
|
||||
array(
|
||||
'status' => 'in-progress',
|
||||
'per_page' => 1,
|
||||
'search' => 'import',
|
||||
'group' => self::$group,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return ! empty( $pending_jobs ) || ! empty( $in_progress );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get batch sizes.
|
||||
*
|
||||
* @retun array
|
||||
*/
|
||||
public static function get_batch_sizes() {
|
||||
return array_merge(
|
||||
self::get_scheduler_batch_sizes(),
|
||||
array(
|
||||
'delete' => 10,
|
||||
'import' => 25,
|
||||
'queue' => 100,
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available scheduling actions.
|
||||
* Used to determine action hook names and clear events.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_scheduler_actions() {
|
||||
return array(
|
||||
'import_batch_init' => 'wc-admin_import_batch_init_' . static::$name,
|
||||
'import_batch' => 'wc-admin_import_batch_' . static::$name,
|
||||
'delete_batch_init' => 'wc-admin_delete_batch_init_' . static::$name,
|
||||
'delete_batch' => 'wc-admin_delete_batch_' . static::$name,
|
||||
'import' => 'wc-admin_import_' . static::$name,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue the imports into multiple batches.
|
||||
*
|
||||
* @param integer|boolean $days Number of days to import.
|
||||
* @param boolean $skip_existing Skip exisiting records.
|
||||
*/
|
||||
public static function import_batch_init( $days, $skip_existing ) {
|
||||
$batch_size = static::get_batch_size( 'import' );
|
||||
$items = static::get_items( 1, 1, $days, $skip_existing );
|
||||
|
||||
if ( 0 === $items->total ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$num_batches = ceil( $items->total / $batch_size );
|
||||
|
||||
self::queue_batches( 1, $num_batches, 'import_batch', array( $days, $skip_existing ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a batch of items to update.
|
||||
*
|
||||
* @param int $batch_number Batch number to import (essentially a query page number).
|
||||
* @param int|bool $days Number of days to import.
|
||||
* @param bool $skip_existing Skip exisiting records.
|
||||
* @return void
|
||||
*/
|
||||
public static function import_batch( $batch_number, $days, $skip_existing ) {
|
||||
$batch_size = static::get_batch_size( 'import' );
|
||||
|
||||
$properties = array(
|
||||
'batch_number' => $batch_number,
|
||||
'batch_size' => $batch_size,
|
||||
'type' => static::$name,
|
||||
);
|
||||
wc_admin_record_tracks_event( 'import_job_start', $properties );
|
||||
|
||||
// When we are skipping already imported items, the table of items to import gets smaller in
|
||||
// every batch, so we want to always import the first page.
|
||||
$page = $skip_existing ? 1 : $batch_number;
|
||||
$items = static::get_items( $batch_size, $page, $days, $skip_existing );
|
||||
|
||||
foreach ( $items->ids as $id ) {
|
||||
static::import( $id );
|
||||
}
|
||||
|
||||
$import_stats = get_option( self::IMPORT_STATS_OPTION, array() );
|
||||
$imported_count = absint( $import_stats[ static::$name ]['imported'] ) + count( $items->ids );
|
||||
$import_stats[ static::$name ]['imported'] = $imported_count;
|
||||
update_option( self::IMPORT_STATS_OPTION, $import_stats );
|
||||
|
||||
$properties['imported_count'] = $imported_count;
|
||||
|
||||
wc_admin_record_tracks_event( 'import_job_complete', $properties );
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue item deletion in batches.
|
||||
*/
|
||||
public static function delete_batch_init() {
|
||||
global $wpdb;
|
||||
$batch_size = static::get_batch_size( 'delete' );
|
||||
$count = static::get_total_imported();
|
||||
|
||||
if ( 0 === $count ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$num_batches = ceil( $count / $batch_size );
|
||||
|
||||
self::queue_batches( 1, $num_batches, 'delete_batch' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch by passing the count to be deleted to the child delete method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function delete_batch() {
|
||||
wc_admin_record_tracks_event( 'delete_import_data_job_start', array( 'type' => static::$name ) );
|
||||
|
||||
$batch_size = static::get_batch_size( 'delete' );
|
||||
static::delete( $batch_size );
|
||||
|
||||
ReportsCache::invalidate();
|
||||
|
||||
wc_admin_record_tracks_event( 'delete_import_data_job_complete', array( 'type' => static::$name ) );
|
||||
}
|
||||
}
|
106
packages/woocommerce-admin/src/Schedulers/MailchimpScheduler.php
Normal file
106
packages/woocommerce-admin/src/Schedulers/MailchimpScheduler.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Schedulers;
|
||||
|
||||
/**
|
||||
* Class MailchimpScheduler
|
||||
*
|
||||
* @package Automattic\WooCommerce\Admin\Schedulers
|
||||
*/
|
||||
class MailchimpScheduler {
|
||||
|
||||
const SUBSCRIBE_ENDPOINT = 'https://woocommerce.com/wp-json/wccom/v1/subscribe';
|
||||
const SUBSCRIBE_ENDPOINT_DEV = 'http://woocommerce.test/wp-json/wccom/v1/subscribe';
|
||||
|
||||
const SUBSCRIBED_OPTION_NAME = 'woocommerce_onboarding_subscribed_to_mailchimp';
|
||||
|
||||
const LOGGER_CONTEXT = 'mailchimp_scheduler';
|
||||
|
||||
/**
|
||||
* The logger instance.
|
||||
*
|
||||
* @var \WC_Logger_Interface|null
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* MailchimpScheduler constructor.
|
||||
*
|
||||
* @param \WC_Logger_Interface|null $logger Logger instance.
|
||||
*/
|
||||
public function __construct( \WC_Logger_Interface $logger = null ) {
|
||||
if ( null === $logger ) {
|
||||
$logger = wc_get_logger();
|
||||
}
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to subscribe store_email to MailChimp.
|
||||
*/
|
||||
public function run() {
|
||||
// Abort if we've already subscribed to MailChimp.
|
||||
if ( 'yes' === get_option( self::SUBSCRIBED_OPTION_NAME ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$profile_data = get_option( 'woocommerce_onboarding_profile' );
|
||||
|
||||
if ( ! isset( $profile_data['is_agree_marketing'] ) || false === $profile_data['is_agree_marketing'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Abort if store_email doesn't exist.
|
||||
if ( ! isset( $profile_data['store_email'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = $this->make_request( $profile_data['store_email'] );
|
||||
|
||||
if ( is_wp_error( $response ) || ! isset( $response['body'] ) ) {
|
||||
$this->logger->error(
|
||||
'Error getting a response from Mailchimp API.',
|
||||
array( 'source' => self::LOGGER_CONTEXT )
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
$body = json_decode( $response['body'] );
|
||||
if ( isset( $body->success ) && true === $body->success ) {
|
||||
update_option( self::SUBSCRIBED_OPTION_NAME, 'yes' );
|
||||
return true;
|
||||
} else {
|
||||
$this->logger->error(
|
||||
// phpcs:ignore
|
||||
'Incorrect response from Mailchimp API with: ' . print_r( $body, true ),
|
||||
array( 'source' => self::LOGGER_CONTEXT )
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP request to the API.
|
||||
*
|
||||
* @param string $store_email Email address to subscribe.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function make_request( $store_email ) {
|
||||
if ( 'development' === constant( 'WP_ENVIRONMENT_TYPE' ) ) {
|
||||
$subscribe_endpoint = self::SUBSCRIBE_ENDPOINT_DEV;
|
||||
} else {
|
||||
$subscribe_endpoint = self::SUBSCRIBE_ENDPOINT;
|
||||
}
|
||||
|
||||
return wp_remote_post(
|
||||
$subscribe_endpoint,
|
||||
array(
|
||||
'method' => 'POST',
|
||||
'body' => array(
|
||||
'email' => $store_email,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
199
packages/woocommerce-admin/src/Schedulers/OrdersScheduler.php
Normal file
199
packages/woocommerce-admin/src/Schedulers/OrdersScheduler.php
Normal file
@ -0,0 +1,199 @@
|
||||
<?php
|
||||
/**
|
||||
* Order syncing related functions and actions.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Schedulers;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore as TaxesDataStore;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
|
||||
use \Automattic\WooCommerce\Admin\Schedulers\CustomersScheduler;
|
||||
|
||||
/**
|
||||
* OrdersScheduler Class.
|
||||
*/
|
||||
class OrdersScheduler extends ImportScheduler {
|
||||
/**
|
||||
* Slug to identify the scheduler.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $name = 'orders';
|
||||
|
||||
/**
|
||||
* Attach order lookup update hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
// Activate WC_Order extension.
|
||||
\Automattic\WooCommerce\Admin\Overrides\Order::add_filters();
|
||||
\Automattic\WooCommerce\Admin\Overrides\OrderRefund::add_filters();
|
||||
|
||||
// Order and refund data must be run on these hooks to ensure meta data is set.
|
||||
add_action( 'save_post', array( __CLASS__, 'possibly_schedule_import' ) );
|
||||
add_action( 'woocommerce_refund_created', array( __CLASS__, 'possibly_schedule_import' ) );
|
||||
|
||||
OrdersStatsDataStore::init();
|
||||
CouponsDataStore::init();
|
||||
ProductsDataStore::init();
|
||||
TaxesDataStore::init();
|
||||
|
||||
parent::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add customer dependencies.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_dependencies() {
|
||||
return array(
|
||||
'import_batch_init' => CustomersScheduler::get_action( 'import_batch_init' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the order/refund IDs and total count that need to be synced.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public static function get_items( $limit = 10, $page = 1, $days = false, $skip_existing = false ) {
|
||||
global $wpdb;
|
||||
$where_clause = '';
|
||||
$offset = $page > 1 ? ( $page - 1 ) * $limit : 0;
|
||||
|
||||
if ( is_int( $days ) ) {
|
||||
$days_ago = gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) );
|
||||
$where_clause .= " AND post_date_gmt >= '{$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
|
||||
)";
|
||||
}
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
"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.
|
||||
|
||||
$order_ids = absint( $count ) > 0 ? $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID 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}
|
||||
ORDER BY post_date_gmt ASC
|
||||
LIMIT %d
|
||||
OFFSET %d",
|
||||
$limit,
|
||||
$offset
|
||||
)
|
||||
) : array(); // phpcs:ignore unprepared SQL ok.
|
||||
|
||||
return (object) array(
|
||||
'total' => absint( $count ),
|
||||
'ids' => $order_ids,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of rows imported.
|
||||
*/
|
||||
public static function get_total_imported() {
|
||||
global $wpdb;
|
||||
return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_order_stats" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule this import if the post is an order or refund.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
*/
|
||||
public static function possibly_schedule_import( $post_id ) {
|
||||
if ( 'shop_order' !== get_post_type( $post_id ) && 'woocommerce_refund_created' !== current_filter() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::schedule_action( 'import', array( $post_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a single order or refund to update lookup tables for.
|
||||
* If an error is encountered in one of the updates, a retry action is scheduled.
|
||||
*
|
||||
* @param int $order_id Order or refund ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function import( $order_id ) {
|
||||
$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;
|
||||
}
|
||||
|
||||
$results = array(
|
||||
OrdersStatsDataStore::sync_order( $order_id ),
|
||||
ProductsDataStore::sync_order_products( $order_id ),
|
||||
CouponsDataStore::sync_order_coupons( $order_id ),
|
||||
TaxesDataStore::sync_order_taxes( $order_id ),
|
||||
CustomersDataStore::sync_order_customer( $order_id ),
|
||||
);
|
||||
|
||||
if ( 'shop_order' === $type ) {
|
||||
$order_refunds = $order->get_refunds();
|
||||
|
||||
foreach ( $order_refunds as $refund ) {
|
||||
OrdersStatsDataStore::sync_order( $refund->get_id() );
|
||||
}
|
||||
}
|
||||
|
||||
ReportsCache::invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of orders.
|
||||
*
|
||||
* @param int $batch_size Number of items to delete.
|
||||
* @return void
|
||||
*/
|
||||
public static function delete( $batch_size ) {
|
||||
global $wpdb;
|
||||
|
||||
$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 ) {
|
||||
OrdersStatsDataStore::delete_order( $order_id );
|
||||
}
|
||||
}
|
||||
}
|
373
packages/woocommerce-admin/src/Schedulers/SchedulerTraits.php
Normal file
373
packages/woocommerce-admin/src/Schedulers/SchedulerTraits.php
Normal file
@ -0,0 +1,373 @@
|
||||
<?php
|
||||
/**
|
||||
* Traits for scheduling actions and dependencies.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Schedulers;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* SchedulerTraits class.
|
||||
*/
|
||||
trait SchedulerTraits {
|
||||
/**
|
||||
* Action scheduler group.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public static $group = 'wc-admin-data';
|
||||
|
||||
/**
|
||||
* Queue instance.
|
||||
*
|
||||
* @var WC_Queue_Interface
|
||||
*/
|
||||
protected static $queue = null;
|
||||
|
||||
/**
|
||||
* Add all actions as hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
foreach ( self::get_actions() as $action_name => $action_hook ) {
|
||||
$method = new \ReflectionMethod( static::class, $action_name );
|
||||
add_action( $action_hook, array( static::class, 'do_action_or_reschedule' ), 10, $method->getNumberOfParameters() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default scheduler actions for batching and scheduling actions.
|
||||
*/
|
||||
public static function get_default_scheduler_actions() {
|
||||
return array(
|
||||
'schedule_action' => 'wc-admin_schedule_action_' . static::$name,
|
||||
'queue_batches' => 'wc-admin_queue_batches_' . static::$name,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the actions for this specific scheduler.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_scheduler_actions() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available scheduling actions.
|
||||
* Used to determine action hook names and clear events.
|
||||
*/
|
||||
public static function get_actions() {
|
||||
return array_merge(
|
||||
static::get_default_scheduler_actions(),
|
||||
static::get_scheduler_actions()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an action tag name from the action name.
|
||||
*
|
||||
* @param string $action_name The action name.
|
||||
* @return string|null
|
||||
*/
|
||||
public static function get_action( $action_name ) {
|
||||
$actions = static::get_actions();
|
||||
return isset( $actions[ $action_name ] ) ? $actions[ $action_name ] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of actions and dependencies as key => value pairs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_dependencies() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dependencies associated with an action.
|
||||
*
|
||||
* @param string $action_name The action slug.
|
||||
* @return string|null
|
||||
*/
|
||||
public static function get_dependency( $action_name ) {
|
||||
$dependencies = static::get_dependencies();
|
||||
return isset( $dependencies[ $action_name ] ) ? $dependencies[ $action_name ] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch action size.
|
||||
*/
|
||||
public static function get_batch_sizes() {
|
||||
return array(
|
||||
'queue_batches' => 100,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the batch size for an action.
|
||||
*
|
||||
* @param string $action Single batch action name.
|
||||
* @return int Batch size.
|
||||
*/
|
||||
public static function get_batch_size( $action ) {
|
||||
$batch_sizes = static::get_batch_sizes();
|
||||
$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( 'woocommerce_analytics_regenerate_batch_size', $batch_size, static::$name, $action );
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten multidimensional arrays to store for scheduling.
|
||||
*
|
||||
* @param array $args Argument array.
|
||||
* @return string
|
||||
*/
|
||||
public static function flatten_args( $args ) {
|
||||
$flattened = array();
|
||||
|
||||
foreach ( $args as $arg ) {
|
||||
if ( is_array( $arg ) ) {
|
||||
$flattened[] = self::flatten_args( $arg );
|
||||
} else {
|
||||
$flattened[] = $arg;
|
||||
}
|
||||
}
|
||||
|
||||
$string = '[' . implode( ',', $flattened ) . ']';
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if existing jobs exist for an action and arguments.
|
||||
*
|
||||
* @param string $action_name Action name.
|
||||
* @param array $args Array of arguments to pass to action.
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_existing_jobs( $action_name, $args ) {
|
||||
$existing_jobs = self::queue()->search(
|
||||
array(
|
||||
'status' => 'pending',
|
||||
'per_page' => 1,
|
||||
'claimed' => false,
|
||||
'hook' => static::get_action( $action_name ),
|
||||
'search' => self::flatten_args( $args ),
|
||||
'group' => self::$group,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $existing_jobs ) {
|
||||
$existing_job = current( $existing_jobs );
|
||||
|
||||
// Bail out if there's a pending single action, or a pending scheduled actions.
|
||||
if (
|
||||
( static::get_action( $action_name ) === $existing_job->get_hook() ) ||
|
||||
(
|
||||
static::get_action( 'schedule_action' ) === $existing_job->get_hook() &&
|
||||
in_array( self::get_action( $action_name ), $existing_job->get_args(), true )
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next blocking job for an action.
|
||||
*
|
||||
* @param string $action_name Action name.
|
||||
* @return false|ActionScheduler_Action
|
||||
*/
|
||||
public static function get_next_blocking_job( $action_name ) {
|
||||
$dependency = self::get_dependency( $action_name );
|
||||
|
||||
if ( ! $dependency ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$blocking_jobs = self::queue()->search(
|
||||
array(
|
||||
'status' => 'pending',
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'per_page' => 1,
|
||||
'search' => $dependency, // search is used instead of hook to find queued batch creation.
|
||||
'group' => static::$group,
|
||||
)
|
||||
);
|
||||
|
||||
$next_job_schedule = null;
|
||||
|
||||
if ( is_array( $blocking_jobs ) ) {
|
||||
foreach ( $blocking_jobs as $blocking_job ) {
|
||||
$next_job_schedule = self::get_next_action_time( $blocking_job );
|
||||
|
||||
// Ensure that the next schedule is a DateTime (it can be null).
|
||||
if ( is_a( $next_job_schedule, 'DateTime' ) ) {
|
||||
return $blocking_job;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for blocking jobs and reschedule if any exist.
|
||||
*/
|
||||
public static function do_action_or_reschedule() {
|
||||
$action_hook = current_action();
|
||||
$action_name = array_search( $action_hook, static::get_actions(), true );
|
||||
$args = func_get_args();
|
||||
|
||||
// Check if any blocking jobs exist and schedule after they've completed
|
||||
// or schedule to run now if no blocking jobs exist.
|
||||
$blocking_job = static::get_next_blocking_job( $action_name );
|
||||
if ( $blocking_job ) {
|
||||
$after = new \DateTime();
|
||||
self::queue()->schedule_single(
|
||||
self::get_next_action_time( $blocking_job )->getTimestamp() + 5,
|
||||
$action_hook,
|
||||
$args,
|
||||
static::$group
|
||||
);
|
||||
} else {
|
||||
call_user_func_array( array( static::class, $action_name ), $args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DateTime for the next scheduled time an action should run.
|
||||
* This function allows backwards compatibility with Action Scheduler < v3.0.
|
||||
*
|
||||
* @param \ActionScheduler_Action $action Action.
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public static function get_next_action_time( $action ) {
|
||||
if ( method_exists( $action->get_schedule(), 'get_next' ) ) {
|
||||
$after = new \DateTime();
|
||||
$next_job_schedule = $action->get_schedule()->get_next( $after );
|
||||
} else {
|
||||
$next_job_schedule = $action->get_schedule()->next();
|
||||
}
|
||||
|
||||
return $next_job_schedule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action to run and check for dependencies.
|
||||
*
|
||||
* @param string $action_name Action name.
|
||||
* @param array $args Array of arguments to pass to action.
|
||||
*/
|
||||
public static function schedule_action( $action_name, $args = array() ) {
|
||||
// Check for existing jobs and bail if they already exist.
|
||||
if ( static::has_existing_jobs( $action_name, $args ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$action_hook = static::get_action( $action_name );
|
||||
if ( ! $action_hook ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
// Skip scheduling if Action Scheduler tables have not been initialized.
|
||||
! get_option( 'schema-ActionScheduler_StoreSchema' ) ||
|
||||
apply_filters( 'woocommerce_analytics_disable_action_scheduling', false )
|
||||
) {
|
||||
call_user_func_array( array( static::class, $action_name ), $args );
|
||||
return;
|
||||
}
|
||||
|
||||
self::queue()->schedule_single( time() + 5, $action_hook, $args, static::$group );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param array $action_args Action arguments.
|
||||
* @return void
|
||||
*/
|
||||
public static function queue_batches( $range_start, $range_end, $single_batch_action, $action_args = array() ) {
|
||||
$batch_size = static::get_batch_size( 'queue_batches' );
|
||||
$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 = (int) ceil( $range_size / $batch_size );
|
||||
|
||||
for ( $i = 0; $i < $batch_size; $i++ ) {
|
||||
$batch_start = (int) ( $range_start + ( $i * $chunk_size ) );
|
||||
$batch_end = (int) min( $range_end, $range_start + ( $chunk_size * ( $i + 1 ) ) - 1 );
|
||||
|
||||
if ( $batch_start > $range_end ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::schedule_action(
|
||||
'queue_batches',
|
||||
array( $batch_start, $batch_end, $single_batch_action, $action_args )
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Otherwise, queue the single batches.
|
||||
for ( $i = $range_start; $i <= $range_end; $i++ ) {
|
||||
$batch_action_args = array_merge( array( $i ), $action_args );
|
||||
self::schedule_action( $single_batch_action, $batch_action_args );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all queued actions.
|
||||
*/
|
||||
public static function clear_queued_actions() {
|
||||
if ( version_compare( \ActionScheduler_Versions::instance()->latest_version(), '3.0', '>=' ) ) {
|
||||
\ActionScheduler::store()->cancel_actions_by_group( static::$group );
|
||||
} else {
|
||||
$actions = static::get_actions();
|
||||
foreach ( $actions as $action ) {
|
||||
self::queue()->cancel_all( $action, null, static::$group );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user