| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- <?php
- class ControllerExtensionModuleReverb extends Controller {
- // -------------------------------------------------------------------------
- // Webhook receiver
- // URL: index.php?route=extension/module/reverb/webhook
- // -------------------------------------------------------------------------
- public function webhook() {
- $raw = file_get_contents('php://input');
- // Validate Reverb signature if a secret is configured
- $secret = $this->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 via order map table
- $check = $this->db->query("
- SELECT order_id FROM `" . DB_PREFIX . "reverb_order_map`
- WHERE `reverb_order_number` = '" . $this->db->escape($order_number) . "'
- LIMIT 1
- ");
- if ($check->num_rows) {
- return;
- }
- // Skip non-actionable statuses
- $status = $reverb_order['status'] ?? '';
- if (in_array($status, ['cancelled', 'blocked', 'unpaid', 'payment_pending', 'pending_review'])) {
- return;
- }
- // Find the matching OC product by listing_id or SKU
- $listing_id = (string)($reverb_order['listing']['id'] ?? $reverb_order['listing_id'] ?? '');
- $oc_product = $this->findProductByListingId($listing_id);
- if (!$oc_product) {
- $sku = $reverb_order['listing']['sku'] ?? $reverb_order['sku'] ?? '';
- $oc_product = $sku ? $this->findProductBySku($sku) : null;
- }
- if (!$oc_product) {
- $oc_product = [
- 'product_id' => 0,
- 'name' => $reverb_order['listing']['title'] ?? $reverb_order['title'] ?? 'Reverb Product',
- 'model' => $reverb_order['listing']['sku'] ?? $reverb_order['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 (!empty($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'] . "'
- ");
- }
- // Record in order map to prevent future duplicates
- $this->db->query("
- INSERT IGNORE INTO `" . DB_PREFIX . "reverb_order_map`
- (`reverb_order_number`, `order_id`, `reverb_status`, `created_at`)
- VALUES (
- '" . $this->db->escape($order_number) . "',
- '" . (int)$order_id . "',
- '" . $this->db->escape($status) . "',
- NOW()
- )
- ");
- $this->model_extension_module_reverb->log(
- (int)$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);
- $last_sync = $this->config->get('module_reverb_order_last_sync') ?: null;
- $page = 1;
- do {
- $response = $api->getOrders($page, 50, $last_sync);
- $orders = $response['orders'] ?? [];
- $total_pages = (int)($response['total_pages'] ?? 1);
- foreach ($orders as $order) {
- $this->handleOrderCreate(['order' => $order]);
- }
- $page++;
- } while ($page <= $total_pages);
- } 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;
- }
- }
|