'idle', 'cursor' => '', 'done' => 0, 'total' => 0, 'started' => '', 'finished' => '', 'run' => 0, ) ); } /** * Starts a full rebuild in the background (or restarts one that is running). * * @return bool False when WooCommerce's scheduler is not available. */ public static function start() { if ( ! function_exists( 'as_enqueue_async_action' ) ) { return false; } as_unschedule_all_actions( self::BATCH_HOOK, array(), self::GROUP ); $run = (int) self::state()['run'] + 1; update_option( self::STATE, array( 'run' => $run, 'status' => 'running', 'cursor' => '', 'done' => 0, 'total' => 0, 'started' => gmdate( 'Y-m-d H:i:s' ), 'finished' => '', ), false ); as_enqueue_async_action( self::BATCH_HOOK, array(), self::GROUP ); return true; } /** * Processes one page of customers and queues the next. * * @return array State after the batch. */ public static function run_batch() { $s = self::state(); if ( 'running' !== $s['status'] ) { return $s; } $rows = Orders::customers_page( $s['cursor'], self::BATCH_SIZE ); Store::upsert( $rows, gmdate( 'Y-m-d H:i:s' ), (int) $s['run'] ); if ( count( $rows ) < self::BATCH_SIZE ) { Store::prune_except( (int) $s['run'] ); $s['status'] = 'complete'; $s['finished'] = gmdate( 'Y-m-d H:i:s' ); } else { $s['cursor'] = end( $rows )['customer_key']; } $s['done'] += count( $rows ); update_option( self::STATE, $s, false ); /** * Fires after each rebuild batch. Scoring hooks in here once the rebuild completes. * * @param array $state Rebuild state. */ do_action( 'statbird_rebuild_batch_done', $s ); if ( 'running' === $s['status'] && function_exists( 'as_enqueue_async_action' ) && ! self::running_in_cli() ) { as_enqueue_async_action( self::BATCH_HOOK, array(), self::GROUP ); } return $s; } /** * WP-CLI drives batches itself in a loop, without the queue. * * @return bool */ private static function running_in_cli() { return defined( 'WP_CLI' ) && WP_CLI && ! doing_action( self::BATCH_HOOK ); } /** * Queues a refresh for the customer an order belongs to. Several changes to the same * customer in one request (status change, then refund) become one refresh. * * @param int|\WC_Order $order Order or order ID. */ public static function order_changed( $order ) { $order = is_object( $order ) ? $order : wc_get_order( $order ); if ( ! $order || 'shop_order' !== $order->get_type() ) { return; } $key = Orders::key_for_order( $order ); if ( '' === $key ) { return; } if ( function_exists( 'as_enqueue_async_action' ) ) { if ( ! as_has_scheduled_action( self::REFRESH, array( $key ), self::GROUP ) ) { as_enqueue_async_action( self::REFRESH, array( $key ), self::GROUP ); } } else { self::refresh( $key ); } } /** * After a refund is deleted, refresh its parent order's customer. * * @param int $refund_id Refund ID. * @param int $order_id Parent order ID. */ public static function refund_deleted( $refund_id, $order_id ) { self::order_changed( (int) $order_id ); } /** * Recomputes one customer from their orders. * * @param string $key Identity key from Orders::key_for_order(). */ public static function refresh( $key ) { $row = Orders::customer( $key ); if ( $row ) { Store::upsert( array( $row ), gmdate( 'Y-m-d H:i:s' ), (int) self::state()['run'] ); } else { Store::delete( md5( $key ) ); } // A guest who has since registered: their guest orders now belong to the account. if ( 0 === strpos( $key, 'u' ) ) { $user = get_userdata( (int) substr( $key, 1 ) ); if ( $user && $user->user_email ) { Store::delete( md5( 'e' . strtolower( trim( $user->user_email ) ) ) ); } } /** * Fires after one customer's purchase facts are refreshed. * * @param string $customer_key MD5 key. * @param array|null $row New aggregate, or null when removed. */ do_action( 'statbird_customer_refreshed', md5( $key ), $row ); } }