config->get('module_reverb_webhook_secret'); if ($secret) { $signature = $_SERVER['HTTP_REVERB_SIGNATURE'] ?? ''; if (!hash_equals(hash_hmac('sha256', $raw, $secret), $signature)) { http_response_code(401); exit('Unauthorized'); } } $payload = json_decode($raw, true); if (!$payload || empty($payload['event'])) { http_response_code(400); exit('Bad Request'); } $this->load->model('extension/module/reverb'); try { switch ($payload['event']) { case 'listing/update': $this->handleListingUpdate($payload); break; case 'order/create': $this->handleOrderCreate($payload); break; default: // Unknown event — acknowledge silently break; } } catch (Exception $e) { $this->model_extension_module_reverb->log(0, 'pull', 'error', 'Webhook error: ' . $e->getMessage()); http_response_code(500); exit('Internal Error'); } http_response_code(200); exit('OK'); } // ------------------------------------------------------------------------- // Cron endpoint — polls Reverb for updates (fallback when webhooks not used) // URL: index.php?route=extension/module/reverb/cron&cron_token=SECRET // ------------------------------------------------------------------------- public function cron() { $cron_token = $this->config->get('module_reverb_cron_token'); if ($cron_token && ($this->request->get['cron_token'] ?? '') !== $cron_token) { http_response_code(403); exit('Forbidden'); } $this->load->model('extension/module/reverb'); $direction = $this->config->get('module_reverb_sync_direction') ?? 'push'; if ($direction === 'both') { $this->pollListingUpdates(); $this->pollOrders(); } // Push any products that haven't been synced yet $this->pushPendingProducts(); exit('Done'); } // ------------------------------------------------------------------------- // Internal: listing update (Reverb → OpenCart) // ------------------------------------------------------------------------- private function handleListingUpdate(array $payload) { $listing = $payload['listing'] ?? []; if (empty($listing['id'])) { return; } $listing_id = (string)$listing['id']; $query = $this->db->query(" SELECT product_id FROM `" . DB_PREFIX . "reverb_product_map` WHERE reverb_listing_id = '" . $this->db->escape($listing_id) . "' "); if (!$query->num_rows) { return; } $product_id = (int)$query->row['product_id']; require_once(DIR_SYSTEM . 'library/reverb/ProductMapper.php'); $updates = ProductMapper::fromReverb($listing); if (!empty($updates)) { $this->load->model('catalog/product'); $this->model_catalog_product->editProduct($product_id, $updates); $this->model_extension_module_reverb->log($product_id, 'pull', 'success', 'Updated from Reverb listing ' . $listing_id); } } // ------------------------------------------------------------------------- // Internal: new order (Reverb → OpenCart) // ------------------------------------------------------------------------- private function handleOrderCreate(array $payload) { $reverb_order = $payload['order'] ?? []; if (empty($reverb_order['order_number'])) { return; } $order_number = $reverb_order['order_number']; // Check for duplicate — avoid creating the same order twice $check = $this->db->query(" SELECT order_id FROM `" . DB_PREFIX . "order` WHERE comment LIKE '%" . $this->db->escape('Reverb Order #' . $order_number) . "%' LIMIT 1 "); if ($check->num_rows) { return; } // Find the matching OC product by SKU or listing_id $listing_id = (string)($reverb_order['listing']['id'] ?? ''); $oc_product = $this->findProductByListingId($listing_id); if (!$oc_product) { $sku = $reverb_order['listing']['sku'] ?? ''; $oc_product = $sku ? $this->findProductBySku($sku) : null; } if (!$oc_product) { // Create a placeholder product entry so the order can still be recorded $oc_product = [ 'product_id' => 0, 'name' => $reverb_order['listing']['title'] ?? 'Reverb Product', 'model' => $reverb_order['listing']['sku'] ?? '', 'price' => $reverb_order['total']['amount'] ?? 0, ]; } require_once(DIR_SYSTEM . 'library/reverb/OrderMapper.php'); $store_info = [ 'store_id' => $this->config->get('config_store_id') ?? 0, 'store_name' => $this->config->get('config_name') ?? '', 'store_url' => $this->config->get('config_url') ?? '', 'language_id' => $this->config->get('config_language_id') ?? 1, 'currency_id' => 1, 'currency_code' => $this->config->get('config_currency') ?? 'AUD', 'currency_value' => 1, ]; $order_data = OrderMapper::toOpenCart($reverb_order, $oc_product, $store_info); $this->load->model('checkout/order'); $order_id = $this->model_checkout_order->addOrder($order_data); // Decrease OC stock for the matched product if ($oc_product['product_id']) { $this->db->query(" UPDATE `" . DB_PREFIX . "product` SET quantity = GREATEST(0, quantity - " . (int)($reverb_order['quantity'] ?? 1) . ") WHERE product_id = '" . (int)$oc_product['product_id'] . "' "); } $this->model_extension_module_reverb->log( $oc_product['product_id'], 'pull', 'success', 'Created OC order #' . $order_id . ' from Reverb order #' . $order_number ); } // ------------------------------------------------------------------------- // Internal: cron helpers // ------------------------------------------------------------------------- private function pollListingUpdates() { try { require_once(DIR_SYSTEM . 'library/reverb/ReverbApi.php'); $token = $this->config->get('module_reverb_api_token'); if (!$token) return; $api = new ReverbApi($token); $response = $api->getListings(); $listings = $response['listings'] ?? []; foreach ($listings as $listing) { $this->handleListingUpdate(['listing' => $listing]); } } catch (Exception $e) { $this->model_extension_module_reverb->log(0, 'pull', 'error', 'Poll listings failed: ' . $e->getMessage()); } } private function pollOrders() { try { require_once(DIR_SYSTEM . 'library/reverb/ReverbApi.php'); $token = $this->config->get('module_reverb_api_token'); if (!$token) return; $api = new ReverbApi($token); $response = $api->getOrders(); $orders = $response['orders'] ?? []; foreach ($orders as $order) { $this->handleOrderCreate(['order' => $order]); } } catch (Exception $e) { $this->model_extension_module_reverb->log(0, 'pull', 'error', 'Poll orders failed: ' . $e->getMessage()); } } private function pushPendingProducts() { $this->load->model('setting/setting'); $allowed_categories = $this->config->get('module_reverb_sync_categories') ?? []; if (empty($allowed_categories)) return; $settings = [ 'api_token' => $this->config->get('module_reverb_api_token'), 'shipping_domestic' => $this->config->get('module_reverb_shipping_domestic') ?? '0', 'shipping_international' => $this->config->get('module_reverb_shipping_international') ?? '0', 'currency' => $this->config->get('config_currency') ?? 'AUD', 'store_url' => $this->config->get('config_url') ?? '', ]; $products = $this->model_extension_module_reverb->getSyncEnabledProducts((array)$allowed_categories); foreach ($products as $product) { try { $this->model_extension_module_reverb->syncProductToReverb($product, $product, $settings); $this->model_extension_module_reverb->log($product['product_id'], 'push', 'success', 'Cron sync: ' . $product['name']); } catch (Exception $e) { $this->model_extension_module_reverb->log($product['product_id'], 'push', 'error', $e->getMessage()); } } } // ------------------------------------------------------------------------- // DB lookups // ------------------------------------------------------------------------- private function findProductByListingId($listing_id) { if (!$listing_id) return null; $query = $this->db->query(" SELECT p.product_id, pd.name, p.model, p.price FROM `" . DB_PREFIX . "reverb_product_map` r JOIN `" . DB_PREFIX . "product` p ON p.product_id = r.product_id JOIN `" . DB_PREFIX . "product_description` pd ON pd.product_id = p.product_id AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "' WHERE r.reverb_listing_id = '" . $this->db->escape($listing_id) . "' LIMIT 1 "); return $query->num_rows ? $query->row : null; } private function findProductBySku($sku) { if (!$sku) return null; $query = $this->db->query(" SELECT p.product_id, pd.name, p.model, p.price FROM `" . DB_PREFIX . "product` p JOIN `" . DB_PREFIX . "product_description` pd ON pd.product_id = p.product_id AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "' WHERE p.model = '" . $this->db->escape($sku) . "' LIMIT 1 "); return $query->num_rows ? $query->row : null; } }