snapshotter = $snapshotter; $this->scheduler = $scheduler; $this->logger = $logger; } /** * Register the batch action handler. */ public function register_hooks(): void { add_action( self::BATCH_HOOK, array( $this, 'run_batch' ), 10, 0 ); } /** * Current backfill state. * * @return array */ public function state(): array { $state = get_option( self::STATE_OPTION, array() ); if ( ! is_array( $state ) || empty( $state ) ) { return array( 'status' => 'idle', 'processed' => 0, 'total' => 0, ); } return $state; } /** * Begin a backfill from a cutoff date. * * @param string $cutoff Y-m-d date (store timezone); orders on/after are included. * @return array|\WP_Error */ public function start( string $cutoff ) { $current = $this->state(); if ( 'running' === ( $current['status'] ?? '' ) ) { return new \WP_Error( 'profit_engine_backfill_running', __( 'A backfill is already in progress.', 'profit-engine' ), array( 'status' => 409 ) ); } $cutoff_ts = $cutoff ? strtotime( $cutoff . ' 00:00:00' ) : 0; if ( ! $cutoff_ts ) { $cutoff_ts = strtotime( '-1 year' ); } $total = $this->count_orders( $cutoff_ts ); $state = array( 'status' => 'running', 'cutoff' => gmdate( 'Y-m-d', $cutoff_ts ), 'cutoff_ts' => $cutoff_ts, 'page' => 1, 'processed' => 0, 'written' => 0, 'total' => $total, 'started_at' => gmdate( 'Y-m-d H:i:s' ), ); update_option( self::STATE_OPTION, $state, false ); $this->scheduler->enqueue_async( self::BATCH_HOOK ); return $state; } /** * Cancel an in-progress backfill. */ public function cancel(): array { $this->scheduler->cancel( self::BATCH_HOOK ); $state = $this->state(); $state['status'] = 'cancelled'; update_option( self::STATE_OPTION, $state, false ); return $state; } /** * Process one batch, then enqueue the next until complete. */ public function run_batch(): void { $state = $this->state(); if ( 'running' !== ( $state['status'] ?? '' ) ) { return; } $page = (int) $state['page']; $ids = $this->order_ids( (int) $state['cutoff_ts'], $page, self::BATCH_SIZE ); if ( empty( $ids ) ) { $state['status'] = 'complete'; $state['finished_at'] = gmdate( 'Y-m-d H:i:s' ); update_option( self::STATE_OPTION, $state, false ); $this->logger->info( 'Backfill complete.', array( 'processed' => $state['processed'], 'written' => $state['written'], ) ); return; } foreach ( $ids as $order_id ) { $order = wc_get_order( $order_id ); if ( $order instanceof WC_Order ) { $state['written'] += $this->snapshotter->snapshot_order( $order, 'backfill' ); } ++$state['processed']; } ++$state['page']; update_option( self::STATE_OPTION, $state, false ); // Chain the next batch. $this->scheduler->enqueue_async( self::BATCH_HOOK ); } /** * Count candidate orders for progress reporting. * * @param int $cutoff_ts Cutoff timestamp. */ private function count_orders( int $cutoff_ts ): int { $result = wc_get_orders( array( 'status' => $this->wc_statuses(), 'date_created' => '>=' . $cutoff_ts, 'limit' => 1, 'paginate' => true, 'return' => 'ids', ) ); return (int) $result->total; } /** * A page of candidate order IDs. * * @param int $cutoff_ts Cutoff timestamp. * @param int $page Page (1-based). * @param int $per_page Page size. * @return int[] */ private function order_ids( int $cutoff_ts, int $page, int $per_page ): array { $orders = wc_get_orders( array( 'status' => $this->wc_statuses(), 'date_created' => '>=' . $cutoff_ts, 'limit' => $per_page, 'page' => $page, 'orderby' => 'date', 'order' => 'ASC', 'return' => 'ids', ) ); return array_map( 'intval', (array) $orders ); } /** * Counted statuses in the wc- prefixed form wc_get_orders expects. * * @return string[] */ private function wc_statuses(): array { return array_map( static fn( string $s ): string => 'wc-' . ltrim( $s, 'wc-' ), $this->snapshotter->counted_statuses() ); } }