fix: run product sync in background batches to avoid 502 on large catalogs
Manual and scheduled sync previously processed every WooCommerce product inline in a single PHP request/AJAX call, with several sequential BC API calls per product. At real catalog sizes this exceeds PHP's max_execution_time and the proxy's read timeout, surfacing as a 502 Bad Gateway on "Sync Now". Adds WBC_Batch_Sync, which queues the catalog in chunks of 25 via Action Scheduler (bundled with WooCommerce) and processes them asynchronously. The manual sync AJAX call now returns immediately after queuing, and the admin UI polls for progress instead of waiting on one long request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -262,6 +262,11 @@ class WBC_Admin {
|
||||
|
||||
/**
|
||||
* AJAX: Manual sync
|
||||
*
|
||||
* Queues background batches and returns immediately - the full sync no
|
||||
* longer runs inline in this request (that approach hits PHP/proxy
|
||||
* timeouts at real catalog sizes and surfaces as a 502 Bad Gateway).
|
||||
* Poll ajax_sync_status() for progress.
|
||||
*/
|
||||
public function ajax_manual_sync() {
|
||||
check_ajax_referer( 'wbc_admin_nonce', 'nonce' );
|
||||
@@ -270,7 +275,7 @@ class WBC_Admin {
|
||||
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'woo-business-central' ) ) );
|
||||
}
|
||||
|
||||
$result = WBC_Cron::run_sync_now();
|
||||
$result = WBC_Batch_Sync::queue_full_sync();
|
||||
|
||||
if ( isset( $result['success'] ) && $result['success'] ) {
|
||||
wp_send_json_success( $result );
|
||||
@@ -279,6 +284,19 @@ class WBC_Admin {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get background sync progress
|
||||
*/
|
||||
public function ajax_sync_status() {
|
||||
check_ajax_referer( 'wbc_admin_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'woo-business-central' ) ) );
|
||||
}
|
||||
|
||||
wp_send_json_success( WBC_Batch_Sync::get_status() );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Clear logs
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,36 @@
|
||||
*/
|
||||
init: function() {
|
||||
this.bindEvents();
|
||||
this.checkInitialSyncStatus();
|
||||
},
|
||||
|
||||
/**
|
||||
* If a background sync is already running (e.g. triggered by cron,
|
||||
* or the admin reloaded the page mid-sync), resume polling for it.
|
||||
*/
|
||||
checkInitialSyncStatus: function() {
|
||||
var $btn = $('#wbc-manual-sync');
|
||||
var $status = $('#wbc-sync-status');
|
||||
|
||||
if (!$btn.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: wbc_admin.ajax_url,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'wbc_sync_status',
|
||||
nonce: wbc_admin.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.data.status === 'running') {
|
||||
$btn.prop('disabled', true);
|
||||
$status.removeClass('success error').addClass('loading');
|
||||
WBC_Admin.pollSyncStatus($btn, $status);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -74,6 +104,10 @@
|
||||
|
||||
/**
|
||||
* Run manual sync
|
||||
*
|
||||
* The sync itself runs in the background (Action Scheduler batches),
|
||||
* so this just kicks it off and then polls for progress instead of
|
||||
* waiting on one long-lived request.
|
||||
*/
|
||||
manualSync: function(e) {
|
||||
e.preventDefault();
|
||||
@@ -92,13 +126,51 @@
|
||||
nonce: wbc_admin.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
$btn.prop('disabled', false);
|
||||
$status.removeClass('loading');
|
||||
|
||||
if (response.success) {
|
||||
$status.addClass('success').text(response.data.message);
|
||||
WBC_Admin.pollSyncStatus($btn, $status);
|
||||
} else {
|
||||
$status.addClass('error').text(response.data.message || 'Sync failed');
|
||||
$btn.prop('disabled', false);
|
||||
$status.removeClass('loading').addClass('error').text((response.data && response.data.message) || 'Sync failed');
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$btn.prop('disabled', false);
|
||||
$status.removeClass('loading').addClass('error').text('Request failed: ' + error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Poll background sync progress until it completes
|
||||
*/
|
||||
pollSyncStatus: function($btn, $status) {
|
||||
$.ajax({
|
||||
url: wbc_admin.ajax_url,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'wbc_sync_status',
|
||||
nonce: wbc_admin.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
if (!response.success) {
|
||||
$btn.prop('disabled', false);
|
||||
$status.removeClass('loading').addClass('error').text((response.data && response.data.message) || 'Failed to get sync status');
|
||||
return;
|
||||
}
|
||||
|
||||
var run = response.data;
|
||||
|
||||
if (run.status === 'running') {
|
||||
$status.text('Syncing... ' + (run.batches_done || 0) + ' / ' + (run.total_batches || 0) + ' batches (success: ' + (run.success || 0) + ', failed: ' + (run.failed || 0) + ', skipped: ' + (run.skipped || 0) + ')');
|
||||
setTimeout(function() {
|
||||
WBC_Admin.pollSyncStatus($btn, $status);
|
||||
}, 3000);
|
||||
} else if (run.status === 'completed') {
|
||||
$btn.prop('disabled', false);
|
||||
$status.removeClass('loading').addClass('success').text('Sync completed. Success: ' + (run.success || 0) + ', Failed: ' + (run.failed || 0) + ', Skipped: ' + (run.skipped || 0));
|
||||
} else {
|
||||
$btn.prop('disabled', false);
|
||||
$status.removeClass('loading');
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
|
||||
229
woo-business-central/includes/class-wbc-batch-sync.php
Normal file
229
woo-business-central/includes/class-wbc-batch-sync.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
/**
|
||||
* Background batch orchestration for product sync
|
||||
*
|
||||
* Splits the WooCommerce-first product sync into small chunks processed via
|
||||
* Action Scheduler (bundled with WooCommerce), so neither the "Sync Now"
|
||||
* AJAX request nor the WP-Cron event has to process the entire catalog in a
|
||||
* single PHP execution. At real catalog sizes (tens of thousands of SKUs)
|
||||
* that single-request approach exceeds PHP's max_execution_time and the
|
||||
* webserver/proxy's read timeout, surfacing as a 502 Bad Gateway.
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WBC_Batch_Sync
|
||||
*/
|
||||
class WBC_Batch_Sync {
|
||||
|
||||
/**
|
||||
* Number of products processed per background action
|
||||
*/
|
||||
const BATCH_SIZE = 25;
|
||||
|
||||
/**
|
||||
* Option name storing the current/last run's progress
|
||||
*/
|
||||
const RUN_OPTION = 'wbc_sync_run';
|
||||
|
||||
/**
|
||||
* Transient used as a simple "a sync is already running" guard
|
||||
*/
|
||||
const LOCK_TRANSIENT = 'wbc_sync_lock';
|
||||
|
||||
/**
|
||||
* Action Scheduler hook processed per batch
|
||||
*/
|
||||
const ACTION_HOOK = 'wbc_process_sync_batch';
|
||||
|
||||
/**
|
||||
* Action Scheduler group name
|
||||
*/
|
||||
const ACTION_GROUP = 'wbc_product_sync';
|
||||
|
||||
/**
|
||||
* Register the Action Scheduler batch handler
|
||||
*/
|
||||
public function register_hooks() {
|
||||
add_action( self::ACTION_HOOK, array( $this, 'process_batch' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off a full catalog sync in the background
|
||||
*
|
||||
* Gathers WooCommerce product IDs with a SKU (ID-only query - never
|
||||
* loads full product objects for the whole catalog into memory), splits
|
||||
* them into batches, and enqueues one background action per batch.
|
||||
* Returns immediately; actual work happens asynchronously.
|
||||
*
|
||||
* @return array Result with 'success' and 'message'.
|
||||
*/
|
||||
public static function queue_full_sync() {
|
||||
if ( get_transient( self::LOCK_TRANSIENT ) ) {
|
||||
$run = get_option( self::RUN_OPTION, array() );
|
||||
return array(
|
||||
'success' => 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;
|
||||
}
|
||||
}
|
||||
@@ -42,17 +42,20 @@ class WBC_Cron {
|
||||
|
||||
/**
|
||||
* Run scheduled product sync
|
||||
*
|
||||
* Queues background batches via WBC_Batch_Sync rather than running the
|
||||
* whole catalog inline - WP-Cron events run under the same PHP time/memory
|
||||
* limits as any other request and would otherwise time out at scale.
|
||||
*/
|
||||
public function run_scheduled_sync() {
|
||||
WBC_Logger::info( 'Cron', 'Starting scheduled product sync' );
|
||||
|
||||
$product_sync = new WBC_Product_Sync();
|
||||
$result = $product_sync->run_sync();
|
||||
$result = WBC_Batch_Sync::queue_full_sync();
|
||||
|
||||
if ( isset( $result['success'] ) && $result['success'] ) {
|
||||
WBC_Logger::info( 'Cron', 'Scheduled product sync completed', $result );
|
||||
WBC_Logger::info( 'Cron', 'Scheduled product sync queued', $result );
|
||||
} else {
|
||||
WBC_Logger::error( 'Cron', 'Scheduled product sync failed', $result );
|
||||
WBC_Logger::error( 'Cron', 'Scheduled product sync failed to queue', $result );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,23 +140,6 @@ class WBC_Cron {
|
||||
update_option( 'wbc_last_sync_time', current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run sync now (manual trigger)
|
||||
*
|
||||
* @return array Sync result.
|
||||
*/
|
||||
public static function run_sync_now() {
|
||||
WBC_Logger::info( 'Cron', 'Manual sync triggered' );
|
||||
|
||||
$product_sync = new WBC_Product_Sync();
|
||||
$result = $product_sync->run_sync();
|
||||
|
||||
// Update last sync time
|
||||
self::update_last_sync_time();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all scheduled events
|
||||
*/
|
||||
|
||||
@@ -96,6 +96,10 @@ class WBC_Loader {
|
||||
$cron = new WBC_Cron();
|
||||
$this->add_action( 'wbc_product_sync_event', $cron, 'run_scheduled_sync' );
|
||||
|
||||
// Register background batch sync handler (Action Scheduler)
|
||||
$batch_sync = new WBC_Batch_Sync();
|
||||
$batch_sync->register_hooks();
|
||||
|
||||
// Register order sync handler
|
||||
$order_sync = new WBC_Order_Sync();
|
||||
$this->add_action( 'woocommerce_order_status_processing', $order_sync, 'sync_order', 10, 1 );
|
||||
@@ -109,6 +113,7 @@ class WBC_Loader {
|
||||
$this->add_action( 'admin_enqueue_scripts', $admin, 'enqueue_scripts' );
|
||||
$this->add_action( 'wp_ajax_wbc_test_connection', $admin, 'ajax_test_connection' );
|
||||
$this->add_action( 'wp_ajax_wbc_manual_sync', $admin, 'ajax_manual_sync' );
|
||||
$this->add_action( 'wp_ajax_wbc_sync_status', $admin, 'ajax_sync_status' );
|
||||
$this->add_action( 'wp_ajax_wbc_clear_logs', $admin, 'ajax_clear_logs' );
|
||||
$this->add_action( 'wp_ajax_wbc_get_companies', $admin, 'ajax_get_companies' );
|
||||
$this->add_action( 'admin_init', $admin, 'handle_csv_export' );
|
||||
|
||||
@@ -46,102 +46,37 @@ class WBC_Product_Sync {
|
||||
);
|
||||
|
||||
/**
|
||||
* Run the product sync (WooCommerce-first approach)
|
||||
* Process a batch of WooCommerce products by ID (WooCommerce-first approach)
|
||||
*
|
||||
* Instead of pulling all 60k+ items from BC, we iterate WooCommerce
|
||||
* products and query BC for each one by SKU/GTIN. This means ~100
|
||||
* targeted API calls instead of 600+ paginated calls.
|
||||
* products and query BC for each one by reference/GTIN/number. This is
|
||||
* called per-batch by WBC_Batch_Sync so a single request never has to
|
||||
* process the whole catalog (which would exceed PHP/proxy timeouts at
|
||||
* real-world catalog sizes).
|
||||
*
|
||||
* @return array Sync results.
|
||||
* @param int[] $product_ids WooCommerce product IDs to process.
|
||||
* @return array Sync results for this batch.
|
||||
*/
|
||||
public function run_sync() {
|
||||
// Check if sync is enabled
|
||||
if ( get_option( 'wbc_enable_stock_sync', 'yes' ) !== 'yes' && get_option( 'wbc_enable_price_sync', 'yes' ) !== 'yes' ) {
|
||||
WBC_Logger::info( 'ProductSync', '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' ),
|
||||
);
|
||||
}
|
||||
public function run_sync_for_ids( array $product_ids ) {
|
||||
foreach ( $product_ids as $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
// Check if credentials are configured
|
||||
if ( ! WBC_OAuth::is_configured() ) {
|
||||
WBC_Logger::error( 'ProductSync', 'Sync failed - API credentials not configured' );
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Sync failed - API credentials not configured.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
if ( ! $product || empty( $product->get_sku() ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
WBC_Logger::info( 'ProductSync', 'Starting product sync (WooCommerce-first)' );
|
||||
|
||||
$start_time = microtime( true );
|
||||
|
||||
// Get all WooCommerce products that have a SKU
|
||||
$products = $this->get_wc_products_with_sku();
|
||||
|
||||
if ( empty( $products ) ) {
|
||||
WBC_Logger::info( 'ProductSync', 'No WooCommerce products with SKU found' );
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( 'No WooCommerce products with SKU found.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
|
||||
WBC_Logger::info( 'ProductSync', 'Found WooCommerce products to sync', array(
|
||||
'count' => count( $products ),
|
||||
) );
|
||||
|
||||
// Process each WooCommerce product
|
||||
foreach ( $products as $product ) {
|
||||
$this->process_wc_product( $product );
|
||||
|
||||
// Small delay between API calls to avoid rate limiting
|
||||
usleep( 50000 ); // 50ms
|
||||
}
|
||||
|
||||
$duration = round( microtime( true ) - $start_time, 2 );
|
||||
|
||||
WBC_Logger::info( 'ProductSync', 'Product sync completed', array(
|
||||
'duration_seconds' => $duration,
|
||||
'total' => $this->results['total'],
|
||||
'success' => $this->results['success'],
|
||||
'failed' => $this->results['failed'],
|
||||
'skipped' => $this->results['skipped'],
|
||||
) );
|
||||
|
||||
return array(
|
||||
'success' => empty( $this->results['errors'] ),
|
||||
'message' => sprintf(
|
||||
/* translators: 1: duration, 2: success count, 3: failed count, 4: skipped count */
|
||||
__( 'Sync completed in %1$s seconds. Success: %2$d, Failed: %3$d, Skipped: %4$d', 'woo-business-central' ),
|
||||
$duration,
|
||||
$this->results['success'],
|
||||
$this->results['failed'],
|
||||
$this->results['skipped']
|
||||
),
|
||||
'results' => $this->results,
|
||||
'success' => empty( $this->results['errors'] ),
|
||||
'results' => $this->results,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all WooCommerce products that have a SKU set
|
||||
*
|
||||
* @return WC_Product[] Array of WooCommerce products.
|
||||
*/
|
||||
private function get_wc_products_with_sku() {
|
||||
$products = wc_get_products( array(
|
||||
'limit' => -1,
|
||||
'status' => 'publish',
|
||||
'return' => 'objects',
|
||||
) );
|
||||
|
||||
// Filter to only products with SKUs
|
||||
return array_filter( $products, function ( $product ) {
|
||||
return ! empty( $product->get_sku() );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single WooCommerce product - find it in BC and sync
|
||||
*
|
||||
|
||||
@@ -131,6 +131,7 @@ function wbc_init() {
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-oauth.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-api-client.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-product-sync.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-batch-sync.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-customer-sync.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-order-sync.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-cron.php';
|
||||
|
||||
Reference in New Issue
Block a user