reverb.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. <?php
  2. class ControllerExtensionModuleReverb extends Controller {
  3. private $error = [];
  4. // -------------------------------------------------------------------------
  5. // Main settings page
  6. // -------------------------------------------------------------------------
  7. public function index() {
  8. $this->load->language('extension/module/reverb');
  9. $this->load->model('extension/module/reverb');
  10. $this->load->model('setting/setting');
  11. $this->load->model('catalog/category');
  12. $this->document->setTitle($this->language->get('heading_title'));
  13. if ($this->request->server['REQUEST_METHOD'] === 'POST' && $this->validate()) {
  14. $this->model_setting_setting->editSetting('module_reverb', $this->request->post);
  15. // Save category mappings separately (they come as a sub-array)
  16. if (isset($this->request->post['module_reverb_category_mappings'])) {
  17. $this->model_extension_module_reverb->saveCategoryMappings(
  18. $this->request->post['module_reverb_category_mappings']
  19. );
  20. }
  21. $this->session->data['success'] = $this->language->get('text_success');
  22. $this->response->redirect($this->url->link('extension/module/reverb', 'user_token=' . $this->session->data['user_token'], true));
  23. }
  24. $data = $this->buildBreadcrumbs();
  25. // Language strings required by the view
  26. $lang_keys = [
  27. 'tab_settings', 'tab_categories', 'tab_reverb_cats', 'tab_log',
  28. 'text_api_settings', 'text_shipping_settings', 'text_sync_settings', 'text_manual_sync',
  29. 'entry_api_token', 'help_api_token',
  30. 'entry_status', 'entry_sync_direction',
  31. 'text_sync_push', 'text_sync_both',
  32. 'entry_shipping_domestic', 'help_shipping_domestic',
  33. 'entry_shipping_international', 'help_shipping_international',
  34. 'help_sync_categories', 'button_sync_now',
  35. 'text_order_import', 'button_import_orders',
  36. 'entry_order_stores', 'help_order_stores', 'text_select_all', 'text_unselect_all',
  37. 'entry_default_qty', 'help_default_qty',
  38. 'text_category_mapping_help', 'text_no_categories',
  39. 'column_oc_category', 'column_reverb_category',
  40. 'text_reverb_cats_help', 'button_refresh_cats', 'text_filter_cats',
  41. 'column_cat_name', 'column_cat_uuid',
  42. 'column_date', 'column_product', 'column_direction', 'column_status', 'column_message',
  43. 'text_push', 'text_pull', 'text_error', 'text_no_log', 'button_clear_log',
  44. 'text_success', 'text_log_success',
  45. 'error_warning', 'error_api_token',
  46. ];
  47. foreach ($lang_keys as $key) {
  48. $data[$key] = $this->language->get($key);
  49. }
  50. // Global OC strings
  51. $data['text_enabled'] = $this->language->get('text_enabled');
  52. $data['text_disabled'] = $this->language->get('text_disabled');
  53. $data['button_save'] = $this->language->get('button_save');
  54. $data['button_cancel'] = $this->language->get('button_cancel');
  55. // Pull saved settings into $data
  56. $fields = [
  57. 'module_reverb_api_token',
  58. 'module_reverb_status',
  59. 'module_reverb_sync_direction',
  60. 'module_reverb_sync_categories',
  61. 'module_reverb_shipping_domestic',
  62. 'module_reverb_shipping_international',
  63. 'module_reverb_order_stores',
  64. 'module_reverb_default_qty',
  65. ];
  66. foreach ($fields as $key) {
  67. $data[$key] = $this->request->post[$key] ?? $this->config->get($key);
  68. }
  69. // Defaults
  70. $data['module_reverb_sync_direction'] = $data['module_reverb_sync_direction'] ?? 'push';
  71. $data['module_reverb_sync_categories'] = $data['module_reverb_sync_categories'] ?? [];
  72. $data['module_reverb_shipping_domestic'] = $data['module_reverb_shipping_domestic'] ?? '0.00';
  73. $data['module_reverb_shipping_international'] = $data['module_reverb_shipping_international'] ?? '0.00';
  74. $data['module_reverb_order_stores'] = $data['module_reverb_order_stores'] ?? [0];
  75. $data['module_reverb_default_qty'] = $data['module_reverb_default_qty'] ?? 1;
  76. // Stores list for the checkbox UI
  77. $this->load->model('setting/store');
  78. $stores = $this->model_setting_store->getStores();
  79. array_unshift($stores, ['store_id' => 0, 'name' => 'Default']);
  80. $data['stores'] = $stores;
  81. // All OC categories for the multi-select
  82. $data['categories'] = $this->getCategoryTree();
  83. // Category mappings for the mapping tab
  84. $data['category_mappings'] = $this->model_extension_module_reverb->getCategoryMappings();
  85. $data['reverb_categories'] = $this->model_extension_module_reverb->getReverbCategories();
  86. $data['reverb_categories_grouped'] = $this->model_extension_module_reverb->getReverbCategoriesGrouped();
  87. $data['module_reverb_category_mappings'] = $data['category_mappings'];
  88. // Sync log
  89. $data['sync_log'] = $this->model_extension_module_reverb->getSyncLog(200);
  90. // Alerts
  91. $data['error_warning'] = $this->error['warning'] ?? '';
  92. $data['success'] = $this->session->data['success'] ?? '';
  93. unset($this->session->data['success']);
  94. $data['action'] = $this->url->link('extension/module/reverb', 'user_token=' . $this->session->data['user_token'], true);
  95. $data['cancel'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=module', true);
  96. $data['sync_url'] = $this->url->link('extension/module/reverb/sync', 'user_token=' . $this->session->data['user_token'], true);
  97. $data['import_url'] = $this->url->link('extension/module/reverb/importOrders', 'user_token=' . $this->session->data['user_token'], true);
  98. $data['clear_log_url'] = $this->url->link('extension/module/reverb/clearLog', 'user_token=' . $this->session->data['user_token'], true);
  99. $data['refresh_cats_url'] = $this->url->link('extension/module/reverb/refreshCategories', 'user_token=' . $this->session->data['user_token'], true);
  100. $data['reverb_categories_flat'] = $this->model_extension_module_reverb->getReverbCategories();
  101. $data['categories_url'] = $this->url->link('extension/module/reverb/reverbCategories', 'user_token=' . $this->session->data['user_token'], true);
  102. $data['header'] = $this->load->controller('common/header');
  103. $data['column_left'] = $this->load->controller('common/column_left');
  104. $data['footer'] = $this->load->controller('common/footer');
  105. $this->response->setOutput($this->load->view('extension/module/reverb', $data));
  106. }
  107. // -------------------------------------------------------------------------
  108. // Install / Uninstall
  109. // -------------------------------------------------------------------------
  110. public function install() {
  111. $this->load->model('extension/module/reverb');
  112. $this->model_extension_module_reverb->install();
  113. // Register events for order pulling (admin side)
  114. $this->load->model('setting/event');
  115. $this->model_setting_event->addEvent(
  116. 'reverb',
  117. 'admin/model/catalog/product/editProduct/after',
  118. 'extension/module/reverb/eventProductSave'
  119. );
  120. $this->model_setting_event->addEvent(
  121. 'reverb',
  122. 'admin/model/catalog/product/addProduct/after',
  123. 'extension/module/reverb/eventProductAddSave'
  124. );
  125. }
  126. public function uninstall() {
  127. $this->load->model('extension/module/reverb');
  128. $this->model_extension_module_reverb->uninstall();
  129. $this->load->model('setting/event');
  130. $this->model_setting_event->deleteEventByCode('reverb');
  131. }
  132. // -------------------------------------------------------------------------
  133. // Event handlers (called by OC event system on product save)
  134. // -------------------------------------------------------------------------
  135. public function eventProductSave(&$route, &$args, &$output) {
  136. $product_id = (int)$args[0];
  137. $this->saveProductReverb($product_id);
  138. }
  139. public function eventProductAddSave(&$route, &$args, &$output) {
  140. // $output holds the new product_id for addProduct
  141. $product_id = (int)$output;
  142. if ($product_id) {
  143. $this->saveProductReverb($product_id);
  144. }
  145. }
  146. private function saveProductReverb($product_id) {
  147. if (!isset($this->request->post['reverb_sync_enabled'])) {
  148. return;
  149. }
  150. $this->load->model('extension/module/reverb');
  151. $this->model_extension_module_reverb->saveProductMap($product_id, [
  152. 'sync_enabled' => (int)(bool)$this->request->post['reverb_sync_enabled'],
  153. 'condition_uuid' => $this->request->post['reverb_condition_uuid'] ?? '',
  154. 'reverb_category_uuid' => $this->request->post['reverb_category_uuid'] ?? '',
  155. 'handmade' => (int)(bool)($this->request->post['reverb_handmade'] ?? 0),
  156. 'upc_does_not_apply' => (int)(bool)($this->request->post['reverb_upc_does_not_apply'] ?? 1),
  157. 'origin_country_code' => strtoupper(substr($this->request->post['reverb_origin_country_code'] ?? '', 0, 2)),
  158. ]);
  159. }
  160. // -------------------------------------------------------------------------
  161. // Manual sync (AJAX)
  162. // -------------------------------------------------------------------------
  163. public function sync() {
  164. $this->load->language('extension/module/reverb');
  165. $this->load->model('extension/module/reverb');
  166. // Set JSON header first so any uncaught error still returns parseable JSON.
  167. $this->response->addHeader('Content-Type: application/json');
  168. // Discard any PHP notices/warnings buffered before this point so they
  169. // cannot corrupt the JSON response body.
  170. if (ob_get_level()) {
  171. ob_clean();
  172. }
  173. try {
  174. if (!$this->user->hasPermission('modify', 'extension/module/reverb')) {
  175. $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_permission')]));
  176. return;
  177. }
  178. $settings = $this->buildSettings();
  179. if (empty($settings['api_token'])) {
  180. $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_api_token')]));
  181. return;
  182. }
  183. $allowed_categories = $this->config->get('module_reverb_sync_categories');
  184. if (empty($allowed_categories)) {
  185. $this->response->setOutput(json_encode(['success' => false, 'error' => 'No sync categories configured. Select at least one category in the Settings tab.']));
  186. return;
  187. }
  188. $products = $this->model_extension_module_reverb->getSyncEnabledProducts((array)$allowed_categories);
  189. $pushed = 0;
  190. $errors = 0;
  191. foreach ($products as $product) {
  192. try {
  193. $this->model_extension_module_reverb->syncProductToReverb($product, $product, $settings);
  194. $pushed++;
  195. $this->safeLog($product['product_id'], 'push', 'success', 'Synced: ' . $product['name']);
  196. } catch (Exception $e) {
  197. $errors++;
  198. $this->safeLog($product['product_id'], 'push', 'error', $e->getMessage());
  199. }
  200. }
  201. $this->response->setOutput(json_encode([
  202. 'success' => true,
  203. 'message' => sprintf($this->language->get('text_sync_complete'), $pushed, $errors),
  204. ]));
  205. } catch (Exception $e) {
  206. $this->response->setOutput(json_encode(['success' => false, 'error' => $e->getMessage()]));
  207. }
  208. }
  209. // -------------------------------------------------------------------------
  210. // Order import (AJAX)
  211. // -------------------------------------------------------------------------
  212. public function importOrders() {
  213. $this->load->language('extension/module/reverb');
  214. $this->load->model('extension/module/reverb');
  215. $this->response->addHeader('Content-Type: application/json');
  216. if (ob_get_level()) {
  217. ob_clean();
  218. }
  219. try {
  220. if (!$this->user->hasPermission('modify', 'extension/module/reverb')) {
  221. $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_permission')]));
  222. return;
  223. }
  224. $settings = $this->buildSettings();
  225. if (empty($settings['api_token'])) {
  226. $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_api_token')]));
  227. return;
  228. }
  229. $result = $this->model_extension_module_reverb->importOrdersFromReverb($settings);
  230. $this->response->setOutput(json_encode([
  231. 'success' => true,
  232. 'message' => sprintf($this->language->get('text_orders_imported'), $result['imported'], $result['skipped']),
  233. ]));
  234. } catch (Exception $e) {
  235. $this->response->setOutput(json_encode(['success' => false, 'error' => $e->getMessage()]));
  236. }
  237. }
  238. private function safeLog($product_id, $direction, $status, $message) {
  239. try {
  240. $this->model_extension_module_reverb->log($product_id, $direction, $status, $message);
  241. } catch (Exception $e) {
  242. // Log table missing or unavailable — ignore silently.
  243. }
  244. }
  245. // -------------------------------------------------------------------------
  246. // Per-product Reverb tab (AJAX — loaded into product edit page)
  247. // URL: extension/module/reverb/productTab?product_id=N
  248. // -------------------------------------------------------------------------
  249. public function productTab() {
  250. if (!$this->user->isLogged()) {
  251. $this->response->setOutput('');
  252. return;
  253. }
  254. $this->load->language('extension/module/reverb');
  255. $this->load->model('extension/module/reverb');
  256. $product_id = isset($this->request->get['product_id']) ? (int)$this->request->get['product_id'] : 0;
  257. $reverb_row = $this->model_extension_module_reverb->getProductMap($product_id);
  258. // Countries list for the origin dropdown
  259. $this->load->model('localisation/country');
  260. $countries = $this->model_localisation_country->getCountries();
  261. // Store default country ISO code
  262. $store_country_id = (int)$this->config->get('config_country_id');
  263. $store_country_iso = '';
  264. foreach ($countries as $c) {
  265. if ((int)$c['country_id'] === $store_country_id) {
  266. $store_country_iso = $c['iso_code_2'];
  267. break;
  268. }
  269. }
  270. // Saved per-product value, falling back to store default
  271. $saved_country = $reverb_row ? $reverb_row['origin_country_code'] : '';
  272. if ($saved_country === '') {
  273. $saved_country = $store_country_iso;
  274. }
  275. $data = [
  276. 'product_id' => $product_id,
  277. 'reverb_sync_enabled' => $reverb_row ? (int)$reverb_row['sync_enabled'] : 0,
  278. 'reverb_condition_uuid' => $reverb_row ? $reverb_row['condition_uuid'] : '',
  279. 'reverb_category_uuid' => $reverb_row ? $reverb_row['reverb_category_uuid'] : '',
  280. 'reverb_listing_id' => $reverb_row ? $reverb_row['reverb_listing_id'] : '',
  281. 'reverb_handmade' => $reverb_row ? (int)$reverb_row['handmade'] : 0,
  282. 'reverb_upc_does_not_apply' => $reverb_row ? (int)$reverb_row['upc_does_not_apply'] : 1,
  283. 'origin_country_code' => $saved_country,
  284. 'countries' => $countries,
  285. 'reverb_conditions' => $this->model_extension_module_reverb->getListingConditions(),
  286. 'reverb_categories_grouped' => $this->model_extension_module_reverb->getReverbCategoriesGrouped(),
  287. 'clear_listing_url' => $this->url->link('extension/module/reverb/clearListingId', 'user_token=' . $this->session->data['user_token'], true),
  288. ];
  289. $this->response->setOutput($this->load->view('extension/module/reverb_product', $data));
  290. }
  291. // -------------------------------------------------------------------------
  292. // Clear Reverb listing ID from a product (AJAX)
  293. // -------------------------------------------------------------------------
  294. public function clearListingId() {
  295. $this->load->language('extension/module/reverb');
  296. $this->load->model('extension/module/reverb');
  297. $this->response->addHeader('Content-Type: application/json');
  298. if (!$this->user->hasPermission('modify', 'extension/module/reverb')) {
  299. $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_permission')]));
  300. return;
  301. }
  302. $product_id = (int)($this->request->get['product_id'] ?? 0);
  303. if (!$product_id) {
  304. $this->response->setOutput(json_encode(['success' => false, 'error' => 'Invalid product ID.']));
  305. return;
  306. }
  307. $this->model_extension_module_reverb->clearListingId($product_id);
  308. $this->response->setOutput(json_encode(['success' => true]));
  309. }
  310. // -------------------------------------------------------------------------
  311. // Clear sync log (AJAX)
  312. // -------------------------------------------------------------------------
  313. public function clearLog() {
  314. $this->load->language('extension/module/reverb');
  315. $this->load->model('extension/module/reverb');
  316. $this->response->addHeader('Content-Type: application/json');
  317. if (!$this->user->hasPermission('modify', 'extension/module/reverb')) {
  318. $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_permission')]));
  319. return;
  320. }
  321. $this->model_extension_module_reverb->clearSyncLog();
  322. $this->response->setOutput(json_encode(['success' => true, 'message' => $this->language->get('text_log_cleared')]));
  323. }
  324. // -------------------------------------------------------------------------
  325. // Refresh Reverb category list (AJAX — bypasses 24 h cache)
  326. // -------------------------------------------------------------------------
  327. public function refreshCategories() {
  328. $this->load->language('extension/module/reverb');
  329. $this->load->model('extension/module/reverb');
  330. $this->response->addHeader('Content-Type: application/json');
  331. if (!$this->user->hasPermission('modify', 'extension/module/reverb')) {
  332. $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_permission')]));
  333. return;
  334. }
  335. $categories = $this->model_extension_module_reverb->refreshReverbCategories();
  336. $this->response->setOutput(json_encode([
  337. 'success' => true,
  338. 'count' => count($categories),
  339. 'message' => sprintf($this->language->get('text_cats_refreshed'), count($categories)),
  340. 'categories' => $categories,
  341. ]));
  342. }
  343. // -------------------------------------------------------------------------
  344. // Reverb categories (AJAX — for category mapping dropdowns)
  345. // -------------------------------------------------------------------------
  346. public function reverbCategories() {
  347. $this->load->model('extension/module/reverb');
  348. $categories = $this->model_extension_module_reverb->getReverbCategories();
  349. $this->response->addHeader('Content-Type: application/json');
  350. $this->response->setOutput(json_encode(['categories' => $categories]));
  351. }
  352. // -------------------------------------------------------------------------
  353. // Per-product tab (loaded inline via OCMOD template include)
  354. // The data is passed through the OCMOD PHP patch, not via a separate request.
  355. // -------------------------------------------------------------------------
  356. // -------------------------------------------------------------------------
  357. // Helpers
  358. // -------------------------------------------------------------------------
  359. private function validate() {
  360. if (!$this->user->hasPermission('modify', 'extension/module/reverb')) {
  361. $this->error['warning'] = $this->language->get('error_permission');
  362. }
  363. $token = $this->request->post['module_reverb_api_token'] ?? '';
  364. if (empty(trim($token))) {
  365. $this->error['warning'] = $this->language->get('error_api_token');
  366. }
  367. return empty($this->error);
  368. }
  369. private function buildBreadcrumbs() {
  370. return [
  371. 'heading_title' => $this->language->get('heading_title'),
  372. 'breadcrumbs' => [
  373. [
  374. 'text' => $this->language->get('text_home'),
  375. 'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true),
  376. ],
  377. [
  378. 'text' => $this->language->get('text_extension'),
  379. 'href' => $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=module', true),
  380. ],
  381. [
  382. 'text' => $this->language->get('heading_title'),
  383. 'href' => $this->url->link('extension/module/reverb', 'user_token=' . $this->session->data['user_token'], true),
  384. ],
  385. ],
  386. ];
  387. }
  388. private function buildSettings() {
  389. // config_url is often empty in OC3; fall back to the catalog constants from admin/config.php
  390. $store_url = $this->config->get('config_url');
  391. if (empty($store_url)) {
  392. if (defined('HTTPS_CATALOG')) {
  393. $store_url = HTTPS_CATALOG;
  394. } elseif (defined('HTTP_CATALOG')) {
  395. $store_url = HTTP_CATALOG;
  396. }
  397. }
  398. $order_stores = $this->config->get('module_reverb_order_stores');
  399. if (!is_array($order_stores) || empty($order_stores)) {
  400. $order_stores = [0];
  401. }
  402. return [
  403. 'api_token' => $this->config->get('module_reverb_api_token'),
  404. 'sync_direction' => $this->config->get('module_reverb_sync_direction') ?? 'push',
  405. 'shipping_domestic' => $this->config->get('module_reverb_shipping_domestic') ?? '0',
  406. 'shipping_international' => $this->config->get('module_reverb_shipping_international') ?? '0',
  407. 'currency' => $this->config->get('config_currency') ?? 'AUD',
  408. 'store_url' => $store_url ?? '',
  409. 'order_stores' => $order_stores,
  410. 'default_qty' => max(1, (int)($this->config->get('module_reverb_default_qty') ?? 1)),
  411. ];
  412. }
  413. private function getCategoryTree() {
  414. $language_id = (int)$this->config->get('config_language_id');
  415. $query = $this->db->query("
  416. SELECT c.category_id, c.parent_id, cd.name
  417. FROM `" . DB_PREFIX . "category` c
  418. LEFT JOIN `" . DB_PREFIX . "category_description` cd
  419. ON cd.category_id = c.category_id AND cd.language_id = '" . $language_id . "'
  420. WHERE c.status = 1
  421. ORDER BY c.parent_id ASC, cd.name ASC
  422. ");
  423. $all = $query->rows;
  424. $byPid = [];
  425. foreach ($all as $row) {
  426. $byPid[(int)$row['parent_id']][] = $row;
  427. }
  428. $result = [];
  429. $stack = [[0, '']];
  430. while ($stack) {
  431. [$pid, $indent] = array_pop($stack);
  432. if (empty($byPid[$pid])) {
  433. continue;
  434. }
  435. foreach (array_reverse($byPid[$pid]) as $cat) {
  436. $result[] = [
  437. 'category_id' => $cat['category_id'],
  438. 'name' => $indent . $cat['name'],
  439. ];
  440. array_push($stack, [(int)$cat['category_id'], $indent . '&nbsp;&nbsp;&nbsp;']);
  441. }
  442. }
  443. return $result;
  444. }
  445. }