load->language('extension/module/reverb'); $this->load->model('extension/module/reverb'); $this->load->model('setting/setting'); $this->load->model('catalog/category'); $this->document->setTitle($this->language->get('heading_title')); if ($this->request->server['REQUEST_METHOD'] === 'POST' && $this->validate()) { $this->model_setting_setting->editSetting('module_reverb', $this->request->post); // Save category mappings separately (they come as a sub-array) if (isset($this->request->post['module_reverb_category_mappings'])) { $this->model_extension_module_reverb->saveCategoryMappings( $this->request->post['module_reverb_category_mappings'] ); } $this->session->data['success'] = $this->language->get('text_success'); $this->response->redirect($this->url->link('extension/module/reverb', 'user_token=' . $this->session->data['user_token'], true)); } $data = $this->buildBreadcrumbs(); // Language strings required by the view $lang_keys = [ 'tab_settings', 'tab_categories', 'tab_log', 'text_api_settings', 'text_shipping_settings', 'text_sync_settings', 'text_manual_sync', 'entry_api_token', 'help_api_token', 'entry_status', 'entry_sync_direction', 'text_sync_push', 'text_sync_both', 'entry_shipping_domestic', 'help_shipping_domestic', 'entry_shipping_international', 'help_shipping_international', 'help_sync_categories', 'button_sync_now', 'text_order_import', 'button_import_orders', 'entry_order_stores', 'help_order_stores', 'text_select_all', 'text_unselect_all', 'entry_default_qty', 'help_default_qty', 'text_category_mapping_help', 'text_no_categories', 'column_oc_category', 'column_reverb_category', 'column_date', 'column_product', 'column_direction', 'column_status', 'column_message', 'text_push', 'text_pull', 'text_error', 'text_no_log', 'button_clear_log', 'text_success', 'text_log_success', 'error_warning', 'error_api_token', ]; foreach ($lang_keys as $key) { $data[$key] = $this->language->get($key); } // Global OC strings $data['text_enabled'] = $this->language->get('text_enabled'); $data['text_disabled'] = $this->language->get('text_disabled'); $data['button_save'] = $this->language->get('button_save'); $data['button_cancel'] = $this->language->get('button_cancel'); // Pull saved settings into $data $fields = [ 'module_reverb_api_token', 'module_reverb_status', 'module_reverb_sync_direction', 'module_reverb_sync_categories', 'module_reverb_shipping_domestic', 'module_reverb_shipping_international', 'module_reverb_order_stores', 'module_reverb_default_qty', ]; foreach ($fields as $key) { $data[$key] = $this->request->post[$key] ?? $this->config->get($key); } // Defaults $data['module_reverb_sync_direction'] = $data['module_reverb_sync_direction'] ?? 'push'; $data['module_reverb_sync_categories'] = $data['module_reverb_sync_categories'] ?? []; $data['module_reverb_shipping_domestic'] = $data['module_reverb_shipping_domestic'] ?? '0.00'; $data['module_reverb_shipping_international'] = $data['module_reverb_shipping_international'] ?? '0.00'; $data['module_reverb_order_stores'] = $data['module_reverb_order_stores'] ?? [0]; $data['module_reverb_default_qty'] = $data['module_reverb_default_qty'] ?? 1; // Stores list for the checkbox UI $this->load->model('setting/store'); $stores = $this->model_setting_store->getStores(); array_unshift($stores, ['store_id' => 0, 'name' => 'Default']); $data['stores'] = $stores; // All OC categories for the multi-select $data['categories'] = $this->getCategoryTree(); // Category mappings for the mapping tab $data['category_mappings'] = $this->model_extension_module_reverb->getCategoryMappings(); $data['reverb_categories'] = $this->model_extension_module_reverb->getReverbCategories(); $data['reverb_categories_grouped'] = $this->model_extension_module_reverb->getReverbCategoriesGrouped(); $data['module_reverb_category_mappings'] = $data['category_mappings']; // Sync log $data['sync_log'] = $this->model_extension_module_reverb->getSyncLog(200); // Alerts $data['error_warning'] = $this->error['warning'] ?? ''; $data['success'] = $this->session->data['success'] ?? ''; unset($this->session->data['success']); $data['action'] = $this->url->link('extension/module/reverb', 'user_token=' . $this->session->data['user_token'], true); $data['cancel'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=module', true); $data['sync_url'] = $this->url->link('extension/module/reverb/sync', 'user_token=' . $this->session->data['user_token'], true); $data['import_url'] = $this->url->link('extension/module/reverb/importOrders', 'user_token=' . $this->session->data['user_token'], true); $data['clear_log_url'] = $this->url->link('extension/module/reverb/clearLog', 'user_token=' . $this->session->data['user_token'], true); $data['categories_url'] = $this->url->link('extension/module/reverb/reverbCategories', 'user_token=' . $this->session->data['user_token'], true); $data['header'] = $this->load->controller('common/header'); $data['column_left'] = $this->load->controller('common/column_left'); $data['footer'] = $this->load->controller('common/footer'); $this->response->setOutput($this->load->view('extension/module/reverb', $data)); } // ------------------------------------------------------------------------- // Install / Uninstall // ------------------------------------------------------------------------- public function install() { $this->load->model('extension/module/reverb'); $this->model_extension_module_reverb->install(); // Register events for order pulling (admin side) $this->load->model('setting/event'); $this->model_setting_event->addEvent( 'reverb', 'admin/model/catalog/product/editProduct/after', 'extension/module/reverb/eventProductSave' ); $this->model_setting_event->addEvent( 'reverb', 'admin/model/catalog/product/addProduct/after', 'extension/module/reverb/eventProductAddSave' ); } public function uninstall() { $this->load->model('extension/module/reverb'); $this->model_extension_module_reverb->uninstall(); $this->load->model('setting/event'); $this->model_setting_event->deleteEventByCode('reverb'); } // ------------------------------------------------------------------------- // Event handlers (called by OC event system on product save) // ------------------------------------------------------------------------- public function eventProductSave(&$route, &$args, &$output) { $product_id = (int)$args[0]; $this->saveProductReverb($product_id); } public function eventProductAddSave(&$route, &$args, &$output) { // $output holds the new product_id for addProduct $product_id = (int)$output; if ($product_id) { $this->saveProductReverb($product_id); } } private function saveProductReverb($product_id) { if (!isset($this->request->post['reverb_sync_enabled'])) { return; } $this->load->model('extension/module/reverb'); $this->model_extension_module_reverb->saveProductMap($product_id, [ 'sync_enabled' => (int)(bool)$this->request->post['reverb_sync_enabled'], 'condition_uuid' => $this->request->post['reverb_condition_uuid'] ?? '', 'reverb_category_uuid' => $this->request->post['reverb_category_uuid'] ?? '', 'handmade' => (int)(bool)($this->request->post['reverb_handmade'] ?? 0), 'upc_does_not_apply' => (int)(bool)($this->request->post['reverb_upc_does_not_apply'] ?? 1), ]); } // ------------------------------------------------------------------------- // Manual sync (AJAX) // ------------------------------------------------------------------------- public function sync() { $this->load->language('extension/module/reverb'); $this->load->model('extension/module/reverb'); // Set JSON header first so any uncaught error still returns parseable JSON. $this->response->addHeader('Content-Type: application/json'); // Discard any PHP notices/warnings buffered before this point so they // cannot corrupt the JSON response body. if (ob_get_level()) { ob_clean(); } try { if (!$this->user->hasPermission('modify', 'extension/module/reverb')) { $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_permission')])); return; } $settings = $this->buildSettings(); if (empty($settings['api_token'])) { $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_api_token')])); return; } $allowed_categories = $this->config->get('module_reverb_sync_categories'); if (empty($allowed_categories)) { $this->response->setOutput(json_encode(['success' => false, 'error' => 'No sync categories configured. Select at least one category in the Settings tab.'])); return; } $products = $this->model_extension_module_reverb->getSyncEnabledProducts((array)$allowed_categories); $pushed = 0; $errors = 0; foreach ($products as $product) { try { $this->model_extension_module_reverb->syncProductToReverb($product, $product, $settings); $pushed++; $this->safeLog($product['product_id'], 'push', 'success', 'Synced: ' . $product['name']); } catch (Exception $e) { $errors++; $this->safeLog($product['product_id'], 'push', 'error', $e->getMessage()); } } $this->response->setOutput(json_encode([ 'success' => true, 'message' => sprintf($this->language->get('text_sync_complete'), $pushed, $errors), ])); } catch (Exception $e) { $this->response->setOutput(json_encode(['success' => false, 'error' => $e->getMessage()])); } } // ------------------------------------------------------------------------- // Order import (AJAX) // ------------------------------------------------------------------------- public function importOrders() { $this->load->language('extension/module/reverb'); $this->load->model('extension/module/reverb'); $this->response->addHeader('Content-Type: application/json'); if (ob_get_level()) { ob_clean(); } try { if (!$this->user->hasPermission('modify', 'extension/module/reverb')) { $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_permission')])); return; } $settings = $this->buildSettings(); if (empty($settings['api_token'])) { $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_api_token')])); return; } $result = $this->model_extension_module_reverb->importOrdersFromReverb($settings); $this->response->setOutput(json_encode([ 'success' => true, 'message' => sprintf($this->language->get('text_orders_imported'), $result['imported'], $result['skipped']), ])); } catch (Exception $e) { $this->response->setOutput(json_encode(['success' => false, 'error' => $e->getMessage()])); } } private function safeLog($product_id, $direction, $status, $message) { try { $this->model_extension_module_reverb->log($product_id, $direction, $status, $message); } catch (Exception $e) { // Log table missing or unavailable — ignore silently. } } // ------------------------------------------------------------------------- // Per-product Reverb tab (AJAX — loaded into product edit page) // URL: extension/module/reverb/productTab?product_id=N // ------------------------------------------------------------------------- public function productTab() { if (!$this->user->isLogged()) { $this->response->setOutput(''); return; } $this->load->language('extension/module/reverb'); $this->load->model('extension/module/reverb'); $product_id = isset($this->request->get['product_id']) ? (int)$this->request->get['product_id'] : 0; $reverb_row = $this->model_extension_module_reverb->getProductMap($product_id); $data = [ 'product_id' => $product_id, 'reverb_sync_enabled' => $reverb_row ? (int)$reverb_row['sync_enabled'] : 0, 'reverb_condition_uuid' => $reverb_row ? $reverb_row['condition_uuid'] : '', 'reverb_category_uuid' => $reverb_row ? $reverb_row['reverb_category_uuid'] : '', 'reverb_listing_id' => $reverb_row ? $reverb_row['reverb_listing_id'] : '', 'reverb_handmade' => $reverb_row ? (int)$reverb_row['handmade'] : 0, 'reverb_upc_does_not_apply' => $reverb_row ? (int)$reverb_row['upc_does_not_apply'] : 1, 'reverb_conditions' => $this->model_extension_module_reverb->getListingConditions(), 'reverb_categories_grouped' => $this->model_extension_module_reverb->getReverbCategoriesGrouped(), 'clear_listing_url' => $this->url->link('extension/module/reverb/clearListingId', 'user_token=' . $this->session->data['user_token'], true), ]; $this->response->setOutput($this->load->view('extension/module/reverb_product', $data)); } // ------------------------------------------------------------------------- // Clear Reverb listing ID from a product (AJAX) // ------------------------------------------------------------------------- public function clearListingId() { $this->load->language('extension/module/reverb'); $this->load->model('extension/module/reverb'); $this->response->addHeader('Content-Type: application/json'); if (!$this->user->hasPermission('modify', 'extension/module/reverb')) { $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_permission')])); return; } $product_id = (int)($this->request->get['product_id'] ?? 0); if (!$product_id) { $this->response->setOutput(json_encode(['success' => false, 'error' => 'Invalid product ID.'])); return; } $this->model_extension_module_reverb->clearListingId($product_id); $this->response->setOutput(json_encode(['success' => true])); } // ------------------------------------------------------------------------- // Clear sync log (AJAX) // ------------------------------------------------------------------------- public function clearLog() { $this->load->language('extension/module/reverb'); $this->load->model('extension/module/reverb'); $this->response->addHeader('Content-Type: application/json'); if (!$this->user->hasPermission('modify', 'extension/module/reverb')) { $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_permission')])); return; } $this->model_extension_module_reverb->clearSyncLog(); $this->response->setOutput(json_encode(['success' => true, 'message' => $this->language->get('text_log_cleared')])); } // ------------------------------------------------------------------------- // Reverb categories (AJAX — for category mapping dropdowns) // ------------------------------------------------------------------------- public function reverbCategories() { $this->load->model('extension/module/reverb'); $categories = $this->model_extension_module_reverb->getReverbCategories(); $this->response->addHeader('Content-Type: application/json'); $this->response->setOutput(json_encode(['categories' => $categories])); } // ------------------------------------------------------------------------- // Per-product tab (loaded inline via OCMOD template include) // The data is passed through the OCMOD PHP patch, not via a separate request. // ------------------------------------------------------------------------- // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private function validate() { if (!$this->user->hasPermission('modify', 'extension/module/reverb')) { $this->error['warning'] = $this->language->get('error_permission'); } $token = $this->request->post['module_reverb_api_token'] ?? ''; if (empty(trim($token))) { $this->error['warning'] = $this->language->get('error_api_token'); } return empty($this->error); } private function buildBreadcrumbs() { return [ 'heading_title' => $this->language->get('heading_title'), 'breadcrumbs' => [ [ 'text' => $this->language->get('text_home'), 'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true), ], [ 'text' => $this->language->get('text_extension'), 'href' => $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=module', true), ], [ 'text' => $this->language->get('heading_title'), 'href' => $this->url->link('extension/module/reverb', 'user_token=' . $this->session->data['user_token'], true), ], ], ]; } private function buildSettings() { // config_url is often empty in OC3; fall back to the catalog constants from admin/config.php $store_url = $this->config->get('config_url'); if (empty($store_url)) { if (defined('HTTPS_CATALOG')) { $store_url = HTTPS_CATALOG; } elseif (defined('HTTP_CATALOG')) { $store_url = HTTP_CATALOG; } } $order_stores = $this->config->get('module_reverb_order_stores'); if (!is_array($order_stores) || empty($order_stores)) { $order_stores = [0]; } return [ 'api_token' => $this->config->get('module_reverb_api_token'), 'sync_direction' => $this->config->get('module_reverb_sync_direction') ?? 'push', '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' => $store_url ?? '', 'order_stores' => $order_stores, 'default_qty' => max(1, (int)($this->config->get('module_reverb_default_qty') ?? 1)), ]; } private function getCategoryTree() { $language_id = (int)$this->config->get('config_language_id'); $query = $this->db->query(" SELECT c.category_id, c.parent_id, cd.name FROM `" . DB_PREFIX . "category` c LEFT JOIN `" . DB_PREFIX . "category_description` cd ON cd.category_id = c.category_id AND cd.language_id = '" . $language_id . "' WHERE c.status = 1 ORDER BY c.parent_id ASC, cd.name ASC "); $all = $query->rows; $byPid = []; foreach ($all as $row) { $byPid[(int)$row['parent_id']][] = $row; } $result = []; $stack = [[0, '']]; while ($stack) { [$pid, $indent] = array_pop($stack); if (empty($byPid[$pid])) { continue; } foreach (array_reverse($byPid[$pid]) as $cat) { $result[] = [ 'category_id' => $cat['category_id'], 'name' => $indent . $cat['name'], ]; array_push($stack, [(int)$cat['category_id'], $indent . '   ']); } } return $result; } }