syncWebhooksOnOptionChange('aikanen_auto_stock_sync', ['stock.updated']); } public function register(): void { add_action('action_scheduler_init', [$this, 'scheduleFullSync']); add_action('rest_api_init', [$this, 'registerWebhookEndpoint']); add_action('woocommerce_product_set_stock', [$this, 'onWooStockChange']); add_action('woocommerce_variation_set_stock', [$this, 'onWooStockChange']); add_action('aikanen_stock_full_sync', [$this, 'fullSync']); add_action('aikanen_push_stock', [$this, 'handlePushStock'], 10, 2); } // Add recurring stock sync to Action Scheduler public function scheduleFullSync(): void { $interval = get_option('aikanen_stock_full_sync_interval', 'disabled'); if ($interval === 'disabled') { // Remove all existing sync schedules if full stock sync is disabled as_unschedule_all_actions('aikanen_stock_full_sync'); return; } if (!as_has_scheduled_action('aikanen_stock_full_sync')) { // Schedule recurring stock sync if selected $seconds = $interval === 'daily' ? DAY_IN_SECONDS : HOUR_IN_SECONDS; as_schedule_recurring_action(time(), $seconds, 'aikanen_stock_full_sync'); } } // Register webhook endpoint for receiving stock update events from Aikanen public function registerWebhookEndpoint(): void { register_rest_route('aikanen/v1', 'webhook/' . $this->key(), [ 'methods' => 'POST', 'callback' => [$this, 'handleWebhook'], 'permission_callback' => [$this, 'verifyWebhookSignature'], ]); } // Handle stock update webhook from Aikanen public function handleWebhook(\WP_REST_Request $request): \WP_REST_Response { $payload = $request->get_json_params(); $products = $payload['data']['products'] ?? []; foreach ($products as $item) { // Process each product from webhook $this->updateWooStock($item['id'], $item['stock']); } return new \WP_REST_Response(['ok' => true]); } // Push stock update to Aikanen when changed on Woo public function onWooStockChange(\WC_Product $product): void { if (SyncState::$suppressOutbound) { // Skip if update was initiated by Aikanen return; } $aikanenId = $product->get_meta('_aikanen_service_id'); if (!$aikanenId) { // Skip if product is not linked to Aikanen return; } if (!$product->managing_stock()) { // Don't push if stock management is disabled return; } // Cancel any pending job for this product and enqueue a fresh one with latest changes as_unschedule_action('aikanen_push_stock', ['aikanen_id' => $aikanenId]); as_enqueue_async_action('aikanen_push_stock', [ 'aikanen_id' => $aikanenId, 'stock' => (int) $product->get_stock_quantity(), ]); } // Send stock update to Aikanen public function handlePushStock(string $aikanenId, int $stock): void { $this->api->patch('/stock', [ 'products' => [ ['id' => $aikanenId, 'stock' => $stock], ], ]); } // Update product stock on Woo private function updateWooStock(string $aikanenId, int $quantity): void { $products = wc_get_products([ 'meta_key' => '_aikanen_service_id', 'meta_value' => $aikanenId, 'limit' => 1, ]); $product = $products[0] ?? null; if (!$product || (int) $product->get_stock_quantity() === $quantity) { // Product is not linked to Aikanen, or doesn't have changed stock quantity return; } $product->set_stock_quantity($quantity); $product->update_meta_data('_aikanen_last_synced', time()); // Update stock on Woo but prevent updating back to Aikanen SyncState::$suppressOutbound = true; try { $product->save(); } finally { SyncState::$suppressOutbound = false; } } // Execute full stock sync (from Aikanen to Woo, or Woo to Aikanen) public function fullSync(): void { $direction = get_option('aikanen_stock_source_of_truth', 'aikanen'); $direction === 'woocommerce' ? $this->pushAllToAikanen() : $this->pullAllFromAikanen(); } // Pull all stock from Aikanen and update WooCommerce private function pullAllFromAikanen(): void { $page = 1; do { // Process paginated results from Aikanen API $result = $this->api->get('/stock', ['page' => $page]); $items = $result['data'] ?? []; foreach ($items as $item) { // Process each item in result $stock = $item['attributes']['stock'] ?? null; if ($stock !== null) { // Update to Woo if has stock $this->updateWooStock($item['id'], $stock); } } $hasNext = !empty($result['links']['next']); // While has next paginated page $page++; } while ($hasNext); } // Push all WooCommerce stock to Aikanen private function pushAllToAikanen(): void { $page = 1; do { // Send stocks of linked WooCommerce products in batches of 50 $products = wc_get_products([ 'meta_key' => '_aikanen_service_id', 'limit' => 50, 'page' => $page, ]); if (empty($products)) { // No linked products break; } // Send products that have stock management $payload = array_map(fn (\WC_Product $p) => [ 'id' => $p->get_meta('_aikanen_service_id'), 'stock' => (int) $p->get_stock_quantity(), ], array_filter($products, fn (\WC_Product $p) => $p->managing_stock())); if (!empty($payload)) { // Send if any products match $this->api->patch('/stock', ['products' => $payload]); } $page++; } while (count($products) === 50); // In batches of 50 products } }