scheduler = $scheduler; } /** * Estimate eligible orders before enqueue. * * @return array{count:int,order_ids:int[],window_days:int,cap:int} */ public function estimate() { $ids = $this->find_eligible_order_ids(); return array( 'count' => count( $ids ), 'order_ids' => $ids, 'window_days' => self::MAX_DAYS, 'cap' => self::MAX_ORDERS, ); } /** * Enqueue preview evaluations (opt-in only). * * @return array{success:bool,enqueued:int,skipped:int,message?:string} */ public function start() { $pressure = ( new QueuePressureGuard( $this->scheduler ) )->assess(); if ( ! empty( $pressure['blocked'] ) ) { return array( 'success' => false, 'enqueued' => 0, 'skipped' => 0, 'message' => isset( $pressure['reason'] ) ? (string) $pressure['reason'] : __( 'Queue is under pressure. Try again later.', 'marginbeacon' ), ); } $estimate = $this->estimate(); $ids = $estimate['order_ids']; if ( empty( $ids ) ) { return array( 'success' => true, 'enqueued' => 0, 'skipped' => 0, 'message' => __( 'No eligible promoted orders in the last 30 days (max 100).', 'marginbeacon' ), ); } $detector = new PromotionSignalDetector(); $enqueued = 0; $skipped = 0; foreach ( $ids as $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { ++$skipped; continue; } $signal = $detector->detect( $order ); if ( 'none' === $signal['signal'] ) { ++$skipped; continue; } // Prefer async when scheduler healthy; otherwise evaluate inline for local/dev reliability. $queued = false; if ( $this->scheduler ) { $revision = OrderMaterialHasher::hash_order( $order ); $id = $this->scheduler->enqueue( Constants::ACTION_EVALUATE_ORDER, array( 'order_id' => (int) $order_id, 'revision' => $revision, 'origin' => 'recent_preview', ), Constants::SCHEDULER_GROUP, null, 'preview_' . $order_id . '_' . substr( $revision, 0, 12 ) ); $queued = SchedulerResult::is_enqueued( $id ); } if ( $queued ) { ++$enqueued; } else { ++$skipped; } } $state = array( 'last_run_gmt' => gmdate( 'Y-m-d H:i:s' ), 'last_enqueued' => $enqueued, 'last_estimate' => $estimate['count'], 'ever_run' => true, ); update_option( Constants::OPTION_RECENT_PREVIEW_STATE, $state, false ); if ( 0 === $enqueued && $skipped > 0 && ! $this->scheduler ) { return array( 'success' => false, 'enqueued' => 0, 'skipped' => $skipped, 'message' => __( 'Scheduler not ready. Recent Orders Preview requires Action Scheduler.', 'marginbeacon' ), ); } return array( 'success' => $enqueued > 0 || 0 === $skipped, 'enqueued' => $enqueued, 'skipped' => $skipped, 'message' => sprintf( /* translators: %d: enqueued count */ __( 'Recent Orders Preview started for %d order(s). Badges update as jobs complete.', 'marginbeacon' ), $enqueued ), ); } /** * Preview run state. * * @return array */ public function state() { $state = get_option( Constants::OPTION_RECENT_PREVIEW_STATE, array() ); return is_array( $state ) ? $state : array(); } /** * @return int[] */ private function find_eligible_order_ids() { if ( ! function_exists( 'wc_get_orders' ) ) { return array(); } $after = gmdate( 'Y-m-d H:i:s', time() - ( self::MAX_DAYS * DAY_IN_SECONDS ) ); $orders = wc_get_orders( array( 'limit' => self::MAX_ORDERS, 'orderby' => 'date', 'order' => 'DESC', 'status' => array( 'processing', 'completed', 'on-hold' ), 'date_created' => '>=' . $after, 'return' => 'ids', ) ); if ( ! is_array( $orders ) ) { return array(); } $detector = new PromotionSignalDetector(); $eligible = array(); foreach ( $orders as $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { continue; } $signal = $detector->detect( $order ); if ( 'none' === $signal['signal'] ) { continue; } $eligible[] = (int) $order_id; if ( count( $eligible ) >= self::MAX_ORDERS ) { break; } } return $eligible; } }