'none', 'since' => 0, 'until' => 0, 'page' => 1, 'checked' => 0, 'stamped' => 0, 'orders' => 0, 'not_human' => 0, 'revenue' => 0.0, 'finished' => 0, 'touched' => 0, ) ); } /** * Runs the first batch now and queues the rest. If the background runner never * fires (WP-Cron or loopback requests off), opening the Agent Stamp page moves * the scan on, so it never stays "running" for good. */ public static function start() { if ( 'running' === self::state()['status'] ) { return; } update_option( self::OPTION, array( 'status' => 'running' ) + self::fresh(), false ); if ( ! function_exists( 'as_enqueue_async_action' ) ) { while ( self::run_batch( false ) ) { continue; } return; } self::run_batch( true ); } /** * One batch when the background runner has been quiet for 30 seconds. */ private static function nudge() { $state = self::state(); if ( 'running' === $state['status'] && time() - (int) $state['touched'] >= 30 ) { self::run_batch( true ); } } public static function handle_start() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You cannot start this scan.', 'agent-stamp' ), 403 ); } check_admin_referer( 'agent_stamp_scan' ); self::start(); wp_safe_redirect( AgentStamp_Admin::url( 'orders' ) ); exit; } /** * One batch. Queues the next one until the last page. * * @param bool $chain Queue the next batch in the background. * @return bool True when more batches remain. */ public static function run_batch( $chain = true ) { $state = self::state(); if ( 'running' !== $state['status'] || ! function_exists( 'wc_get_orders' ) ) { return false; } // add_option is an atomic insert; a transient read followed by write is not. $lock = 'agent_stamp_scan_lock'; $expires = (int) get_option( $lock, 0 ); if ( $expires && $expires < time() ) { delete_option( $lock ); } if ( ! add_option( $lock, time() + 10 * MINUTE_IN_SECONDS, '', false ) ) { return false; } try { return self::process_batch( $chain ); } finally { delete_option( $lock ); } } private static function process_batch( $chain ) { $state = self::state(); if ( 'running' !== $state['status'] ) { return false; } $orders = wc_get_orders( array( 'limit' => self::BATCH, 'paged' => $state['page'], 'date_created' => $state['since'] . '...' . $state['until'], 'return' => 'objects', 'status' => array_keys( wc_get_order_statuses() ), 'type' => 'shop_order', 'orderby' => 'ID', 'order' => 'ASC', ) ); $now = time(); foreach ( $orders as $order ) { $state['checked']++; if ( '' === (string) $order->get_meta( '_agent_stamp_verdict' ) ) { $result = AgentStamp_History::classify_saved( (string) $order->get_customer_user_agent(), (string) $order->get_created_via(), $now ); if ( null !== $result ) { $order->update_meta_data( '_agent_stamp_verdict', $result['verdict'] ); $order->update_meta_data( '_agent_stamp_name', $result['name'] ); $order->update_meta_data( '_agent_stamp_confidence', $result['confidence'] ); $order->update_meta_data( '_agent_stamp_detail', $result['detail'] ); $order->update_meta_data( '_agent_stamp_ua', substr( (string) $order->get_customer_user_agent(), 0, 1024 ) ); $order->update_meta_data( '_agent_stamp_source', 'history' ); $order->save_meta_data(); $state['stamped']++; } } $verdict = (string) $order->get_meta( '_agent_stamp_verdict' ); if ( '' === $verdict || in_array( $order->get_status(), array( 'failed', 'cancelled', 'checkout-draft' ), true ) ) { continue; } $state['orders']++; if ( 'human' !== $verdict ) { $state['not_human']++; $state['revenue'] += (float) $order->get_total(); } } $state['touched'] = $now; $more = count( $orders ) === self::BATCH; if ( $more ) { $state['page']++; } else { $state['status'] = 'done'; $state['finished'] = $now; } update_option( self::OPTION, $state, false ); if ( $more && $chain && function_exists( 'as_enqueue_async_action' ) ) { as_enqueue_async_action( self::HOOK, array(), 'agent-stamp' ); } return $more; } /** * Settings page section: the button, progress, or the headline. */ public static function render() { self::nudge(); $state = self::state(); echo '
' . esc_html( sprintf( /* translators: %d: orders checked so far */ __( 'Scanning the last 12 months. %d orders checked so far. Reload this page to see progress.', 'agent-stamp' ), $state['checked'] ) ) . '
'; return; } if ( 'done' === $state['status'] ) { echo '' . esc_html( sprintf( /* translators: 1: not-human orders, 2: all stamped orders, 3: revenue */ __( 'In the last 12 months, %1$d of %2$d orders were not a person. Revenue from them: %3$s.', 'agent-stamp' ), $state['not_human'], $state['orders'], wp_strip_all_tags( wc_price( $state['revenue'] ) ) ) ) . '
'; echo '' . esc_html__( 'Past orders are stamped from the user-agent WooCommerce saved. A signature and checkout page signals were not saved then, so past orders are never stamped signed, Likely agent, or Unattended.', 'agent-stamp' ) . '
'; } else { echo '' . esc_html__( 'Stamp the orders placed before Agent Stamp was active, from the user-agent WooCommerce saved on each one. It runs in the background.', 'agent-stamp' ) . '
'; } echo ''; } /** * Stamps past orders from the saved user-agent. * * ## EXAMPLES * * wp agent-stamp scan */ public static function cli() { update_option( self::OPTION, array( 'status' => 'running' ) + self::fresh(), false ); while ( self::run_batch( false ) ) { continue; } $state = self::state(); WP_CLI::success( sprintf( '%d orders checked, %d stamped. %d of %d orders in the last 12 months were not a person.', $state['checked'], $state['stamped'], $state['not_human'], $state['orders'] ) ); } /** * @return array