true, 'started' => false, 'message' => __( 'A sync is already running.', 'woo-business-central' ), 'run' => $run, ); } if ( get_option( 'wbc_enable_stock_sync', 'yes' ) !== 'yes' && get_option( 'wbc_enable_price_sync', 'yes' ) !== 'yes' ) { WBC_Logger::info( 'BatchSync', 'Sync skipped - both stock and price sync are disabled' ); return array( 'success' => true, 'message' => __( 'Sync skipped - both stock and price sync are disabled.', 'woo-business-central' ), ); } if ( ! WBC_OAuth::is_configured() ) { WBC_Logger::error( 'BatchSync', 'Sync failed - API credentials not configured' ); return array( 'success' => false, 'message' => __( 'Sync failed - API credentials not configured.', 'woo-business-central' ), ); } if ( ! function_exists( 'as_enqueue_async_action' ) ) { WBC_Logger::error( 'BatchSync', 'Action Scheduler not available (ships with WooCommerce)' ); return array( 'success' => false, 'message' => __( 'Background task runner is not available.', 'woo-business-central' ), ); } // ID-only query with a meta filter, so 60k+ catalogs don't get loaded // as full WC_Product objects just to check for a SKU. $product_ids = wc_get_products( array( 'limit' => -1, 'status' => 'publish', 'return' => 'ids', 'meta_query' => array( // phpcs:ignore WooCommerce.Commenting -- deliberate, avoids full-object load at scale. array( 'key' => '_sku', 'value' => '', 'compare' => '!=', ), ), ) ); if ( empty( $product_ids ) ) { WBC_Logger::info( 'BatchSync', 'No WooCommerce products with SKU found' ); return array( 'success' => true, 'message' => __( 'No WooCommerce products with SKU found.', 'woo-business-central' ), ); } $run_id = uniqid( 'wbc_', true ); $batches = array_chunk( $product_ids, self::BATCH_SIZE ); update_option( self::RUN_OPTION, array( 'run_id' => $run_id, 'status' => 'running', 'total' => count( $product_ids ), 'total_batches' => count( $batches ), 'batches_done' => 0, 'success' => 0, 'failed' => 0, 'skipped' => 0, 'started_at' => current_time( 'mysql' ), 'finished_at' => '', ), false ); // Safety-net expiry in case a batch throws and never reports back; // prevents the lock from wedging the sync button forever. set_transient( self::LOCK_TRANSIENT, $run_id, 4 * HOUR_IN_SECONDS ); foreach ( $batches as $batch ) { as_enqueue_async_action( self::ACTION_HOOK, array( 'run_id' => $run_id, 'product_ids' => $batch, ), self::ACTION_GROUP ); } WBC_Logger::info( 'BatchSync', 'Queued background sync', array( 'run_id' => $run_id, 'total_products' => count( $product_ids ), 'total_batches' => count( $batches ), ) ); return array( 'success' => true, 'started' => true, 'message' => sprintf( /* translators: %d: number of products */ __( 'Sync started in the background for %d product(s).', 'woo-business-central' ), count( $product_ids ) ), ); } /** * Process a single batch (called by Action Scheduler) * * @param array $args Action args: run_id, product_ids. */ public function process_batch( $args ) { $run_id = $args['run_id'] ?? ''; $product_ids = $args['product_ids'] ?? array(); $run = get_option( self::RUN_OPTION, array() ); // A newer sync superseded this run (e.g. re-triggered manually) - drop it. if ( empty( $run ) || ( $run['run_id'] ?? '' ) !== $run_id ) { return; } $sync = new WBC_Product_Sync(); $sync->run_sync_for_ids( $product_ids ); $batch_results = $sync->get_results(); // Progress counters are best-effort: Action Scheduler can run batches // concurrently, and update_option() read-modify-write isn't atomic. // Good enough for a progress indicator; not used for anything // correctness-critical. $run = get_option( self::RUN_OPTION, $run ); if ( ( $run['run_id'] ?? '' ) !== $run_id ) { return; } $run['batches_done'] = (int) ( $run['batches_done'] ?? 0 ) + 1; $run['success'] = (int) ( $run['success'] ?? 0 ) + $batch_results['success']; $run['failed'] = (int) ( $run['failed'] ?? 0 ) + $batch_results['failed']; $run['skipped'] = (int) ( $run['skipped'] ?? 0 ) + $batch_results['skipped']; if ( $run['batches_done'] >= $run['total_batches'] ) { $run['status'] = 'completed'; $run['finished_at'] = current_time( 'mysql' ); WBC_Cron::update_last_sync_time(); delete_transient( self::LOCK_TRANSIENT ); WBC_Logger::info( 'BatchSync', 'Background sync completed', $run ); } update_option( self::RUN_OPTION, $run, false ); } /** * Get the current/last sync run status * * @return array Run status, or array('status' => 'none') if never run. */ public static function get_status() { $run = get_option( self::RUN_OPTION, array() ); if ( empty( $run ) ) { return array( 'status' => 'none' ); } return $run; } }