reverb.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. <?php
  2. class ModelExtensionModuleReverb extends Model {
  3. // -------------------------------------------------------------------------
  4. // Install / Uninstall
  5. // -------------------------------------------------------------------------
  6. public function install() {
  7. $this->db->query("
  8. CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "reverb_product_map` (
  9. `product_id` INT(11) NOT NULL,
  10. `reverb_listing_id` VARCHAR(64) NOT NULL DEFAULT '',
  11. `sync_enabled` TINYINT(1) NOT NULL DEFAULT 0,
  12. `condition_uuid` VARCHAR(64) NOT NULL DEFAULT '',
  13. `reverb_category_uuid` VARCHAR(64) NOT NULL DEFAULT '',
  14. `handmade` TINYINT(1) NOT NULL DEFAULT 0,
  15. `upc_does_not_apply` TINYINT(1) NOT NULL DEFAULT 1,
  16. `origin_country_code` VARCHAR(2) NOT NULL DEFAULT '',
  17. `last_synced_at` DATETIME NULL DEFAULT NULL,
  18. PRIMARY KEY (`product_id`)
  19. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
  20. ");
  21. $this->migrate();
  22. $this->db->query("
  23. CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "reverb_order_map` (
  24. `reverb_order_number` VARCHAR(64) NOT NULL,
  25. `order_id` INT(11) NOT NULL DEFAULT 0,
  26. `reverb_status` VARCHAR(32) NOT NULL DEFAULT '',
  27. `created_at` DATETIME NOT NULL,
  28. PRIMARY KEY (`reverb_order_number`)
  29. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
  30. ");
  31. $this->db->query("
  32. CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "reverb_sync_log` (
  33. `log_id` INT(11) NOT NULL AUTO_INCREMENT,
  34. `product_id` INT(11) NOT NULL DEFAULT 0,
  35. `direction` ENUM('push','pull') NOT NULL DEFAULT 'push',
  36. `status` ENUM('success','error') NOT NULL DEFAULT 'success',
  37. `message` TEXT NOT NULL,
  38. `created_at` DATETIME NOT NULL,
  39. PRIMARY KEY (`log_id`),
  40. KEY `product_id` (`product_id`),
  41. KEY `created_at` (`created_at`)
  42. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
  43. ");
  44. }
  45. public function uninstall() {
  46. $this->db->query("DROP TABLE IF EXISTS `" . DB_PREFIX . "reverb_product_map`");
  47. $this->db->query("DROP TABLE IF EXISTS `" . DB_PREFIX . "reverb_sync_log`");
  48. $this->db->query("DROP TABLE IF EXISTS `" . DB_PREFIX . "reverb_order_map`");
  49. }
  50. public function migrate() {
  51. static $done = false;
  52. if ($done) return;
  53. $done = true;
  54. $this->addColumnIfMissing(DB_PREFIX . 'reverb_product_map', 'handmade', 'TINYINT(1) NOT NULL DEFAULT 0');
  55. $this->addColumnIfMissing(DB_PREFIX . 'reverb_product_map', 'upc_does_not_apply', 'TINYINT(1) NOT NULL DEFAULT 1');
  56. $this->addColumnIfMissing(DB_PREFIX . 'reverb_product_map', 'origin_country_code', "VARCHAR(2) NOT NULL DEFAULT ''");
  57. // Create order map table for upgrades from earlier versions
  58. $this->db->query("
  59. CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "reverb_order_map` (
  60. `reverb_order_number` VARCHAR(64) NOT NULL,
  61. `order_id` INT(11) NOT NULL DEFAULT 0,
  62. `reverb_status` VARCHAR(32) NOT NULL DEFAULT '',
  63. `created_at` DATETIME NOT NULL,
  64. PRIMARY KEY (`reverb_order_number`)
  65. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci
  66. ");
  67. }
  68. private function addColumnIfMissing($table, $column, $definition) {
  69. $r = $this->db->query("
  70. SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS
  71. WHERE TABLE_SCHEMA = DATABASE()
  72. AND TABLE_NAME = '" . $this->db->escape($table) . "'
  73. AND COLUMN_NAME = '" . $this->db->escape($column) . "'
  74. ");
  75. if (empty($r->row['cnt'])) {
  76. $this->db->query("ALTER TABLE `" . $table . "` ADD COLUMN `" . $column . "` " . $definition);
  77. }
  78. }
  79. // -------------------------------------------------------------------------
  80. // Product map CRUD
  81. // -------------------------------------------------------------------------
  82. public function getProductMap($product_id) {
  83. $this->migrate();
  84. $query = $this->db->query("
  85. SELECT * FROM `" . DB_PREFIX . "reverb_product_map`
  86. WHERE `product_id` = '" . (int)$product_id . "'
  87. ");
  88. return $query->num_rows ? $query->row : null;
  89. }
  90. public function saveProductMap($product_id, array $data) {
  91. $this->migrate();
  92. $existing = $this->getProductMap($product_id);
  93. $sync_enabled = isset($data['sync_enabled']) ? (int)(bool)$data['sync_enabled'] : 0;
  94. $condition_uuid = isset($data['condition_uuid']) ? $this->db->escape($data['condition_uuid']) : '';
  95. $reverb_category_uuid = isset($data['reverb_category_uuid']) ? $this->db->escape($data['reverb_category_uuid']) : '';
  96. $reverb_listing_id = isset($data['reverb_listing_id']) ? $this->db->escape($data['reverb_listing_id']) : '';
  97. $last_synced_at = isset($data['last_synced_at']) ? "'" . $this->db->escape($data['last_synced_at']) . "'" : 'NULL';
  98. $handmade = isset($data['handmade']) ? (int)(bool)$data['handmade'] : 0;
  99. $upc_does_not_apply = isset($data['upc_does_not_apply']) ? (int)(bool)$data['upc_does_not_apply'] : 1;
  100. $origin_country_code = isset($data['origin_country_code']) ? strtoupper(substr($this->db->escape($data['origin_country_code']), 0, 2)) : '';
  101. if ($existing) {
  102. $this->db->query("
  103. UPDATE `" . DB_PREFIX . "reverb_product_map`
  104. SET `sync_enabled` = $sync_enabled,
  105. `condition_uuid` = '$condition_uuid',
  106. `reverb_category_uuid` = '$reverb_category_uuid',
  107. `handmade` = $handmade,
  108. `upc_does_not_apply` = $upc_does_not_apply,
  109. `origin_country_code` = '$origin_country_code'"
  110. . (!empty($reverb_listing_id) ? ", `reverb_listing_id` = '$reverb_listing_id'" : '')
  111. . (isset($data['last_synced_at']) ? ", `last_synced_at` = $last_synced_at" : '')
  112. . " WHERE `product_id` = '" . (int)$product_id . "'"
  113. );
  114. } else {
  115. $this->db->query("
  116. INSERT INTO `" . DB_PREFIX . "reverb_product_map`
  117. (`product_id`, `sync_enabled`, `condition_uuid`, `reverb_category_uuid`,
  118. `reverb_listing_id`, `handmade`, `upc_does_not_apply`, `origin_country_code`, `last_synced_at`)
  119. VALUES (
  120. '" . (int)$product_id . "',
  121. $sync_enabled,
  122. '$condition_uuid',
  123. '$reverb_category_uuid',
  124. '$reverb_listing_id',
  125. $handmade,
  126. $upc_does_not_apply,
  127. '$origin_country_code',
  128. $last_synced_at
  129. )
  130. ");
  131. }
  132. }
  133. public function updateListingId($product_id, $listing_id) {
  134. $this->db->query("
  135. UPDATE `" . DB_PREFIX . "reverb_product_map`
  136. SET `reverb_listing_id` = '" . $this->db->escape($listing_id) . "',
  137. `last_synced_at` = NOW()
  138. WHERE `product_id` = '" . (int)$product_id . "'
  139. ");
  140. }
  141. /**
  142. * Return all products eligible for sync (sync_enabled = 1, in allowed categories).
  143. *
  144. * @param array $allowed_category_ids List of OC category IDs.
  145. * @return array
  146. */
  147. public function getSyncEnabledProducts(array $allowed_category_ids) {
  148. if (empty($allowed_category_ids)) {
  149. return [];
  150. }
  151. $ids = implode(',', array_map('intval', $allowed_category_ids));
  152. $query = $this->db->query("
  153. SELECT p.product_id, pd.name, pd.description, p.model, p.price, p.quantity, p.image,
  154. r.reverb_listing_id, r.condition_uuid, r.reverb_category_uuid,
  155. r.handmade, r.upc_does_not_apply,
  156. m.name AS manufacturer
  157. FROM `" . DB_PREFIX . "product` p
  158. INNER JOIN `" . DB_PREFIX . "product_description` pd
  159. ON pd.product_id = p.product_id AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "'
  160. INNER JOIN `" . DB_PREFIX . "product_to_category` ptc
  161. ON ptc.product_id = p.product_id AND ptc.category_id IN ($ids)
  162. INNER JOIN `" . DB_PREFIX . "reverb_product_map` r
  163. ON r.product_id = p.product_id AND r.sync_enabled = 1
  164. LEFT JOIN `" . DB_PREFIX . "manufacturer` m
  165. ON m.manufacturer_id = p.manufacturer_id
  166. WHERE p.status = 1
  167. GROUP BY p.product_id
  168. ");
  169. return $query->rows;
  170. }
  171. // -------------------------------------------------------------------------
  172. // Product images
  173. // -------------------------------------------------------------------------
  174. public function getProductImages($product_id) {
  175. $images = [];
  176. $product_query = $this->db->query("
  177. SELECT image FROM `" . DB_PREFIX . "product`
  178. WHERE product_id = '" . (int)$product_id . "'
  179. ");
  180. if ($product_query->num_rows && !empty($product_query->row['image'])) {
  181. $images[] = $product_query->row['image'];
  182. }
  183. $gallery_query = $this->db->query("
  184. SELECT image FROM `" . DB_PREFIX . "product_image`
  185. WHERE product_id = '" . (int)$product_id . "'
  186. ORDER BY sort_order ASC
  187. ");
  188. foreach ($gallery_query->rows as $row) {
  189. $images[] = $row['image'];
  190. }
  191. return $images;
  192. }
  193. // -------------------------------------------------------------------------
  194. // Category mappings (OC category → Reverb category UUID)
  195. // -------------------------------------------------------------------------
  196. public function getCategoryMappings() {
  197. $raw = $this->config->get('module_reverb_category_mappings');
  198. return $raw ? json_decode($raw, true) : [];
  199. }
  200. public function saveCategoryMappings(array $mappings) {
  201. $this->load->model('setting/setting');
  202. $this->model_setting_setting->editSettingValue('module_reverb', 'module_reverb_category_mappings', json_encode($mappings));
  203. }
  204. // -------------------------------------------------------------------------
  205. // Reverb metadata cache (conditions + categories)
  206. // -------------------------------------------------------------------------
  207. public function getListingConditions() {
  208. $cached = $this->config->get('module_reverb_conditions_cache');
  209. $cached_at = (int)$this->config->get('module_reverb_conditions_cached_at');
  210. if ($cached && (time() - $cached_at) < 86400) {
  211. return json_decode($cached, true);
  212. }
  213. try {
  214. $api = $this->getApi();
  215. $resp = $api->getListingConditions();
  216. $conditions = isset($resp['conditions']) ? $resp['conditions'] : [];
  217. $this->load->model('setting/setting');
  218. $this->model_setting_setting->editSettingValue('module_reverb', 'module_reverb_conditions_cache', json_encode($conditions));
  219. $this->model_setting_setting->editSettingValue('module_reverb', 'module_reverb_conditions_cached_at', time());
  220. return $conditions;
  221. } catch (Exception $e) {
  222. return $cached ? json_decode($cached, true) : [];
  223. }
  224. }
  225. public function getReverbCategories() {
  226. $cached = $this->config->get('module_reverb_categories_cache');
  227. $cached_at = (int)$this->config->get('module_reverb_categories_cached_at');
  228. if ($cached && (time() - $cached_at) < 86400) {
  229. return json_decode($cached, true);
  230. }
  231. return $this->refreshReverbCategories();
  232. }
  233. public function refreshReverbCategories() {
  234. $cached = $this->config->get('module_reverb_categories_cache');
  235. try {
  236. $api = $this->getApi();
  237. $resp = $api->getCategories();
  238. $categories = isset($resp['categories']) ? $resp['categories'] : [];
  239. $this->load->model('setting/setting');
  240. $this->model_setting_setting->editSettingValue('module_reverb', 'module_reverb_categories_cache', json_encode($categories));
  241. $this->model_setting_setting->editSettingValue('module_reverb', 'module_reverb_categories_cached_at', time());
  242. return $categories;
  243. } catch (Exception $e) {
  244. return $cached ? json_decode($cached, true) : [];
  245. }
  246. }
  247. /**
  248. * Return Reverb categories grouped by their root (top-level) category name.
  249. * Parses the full_name field (e.g. "Guitars > Electric Guitars > Solidbody").
  250. *
  251. * @return array ['Root Name' => [['uuid'=>..., 'full_name'=>..., 'name'=>...], ...], ...]
  252. */
  253. public function getReverbCategoriesGrouped() {
  254. $flat = $this->getReverbCategories();
  255. $grouped = [];
  256. foreach ($flat as $cat) {
  257. $full = $cat['full_name'] ?? $cat['name'] ?? '';
  258. $parts = array_map('trim', explode('>', $full));
  259. $root = $parts[0] ?: 'Other';
  260. $grouped[$root][] = $cat;
  261. }
  262. ksort($grouped);
  263. return $grouped;
  264. }
  265. // -------------------------------------------------------------------------
  266. // Sync log
  267. // -------------------------------------------------------------------------
  268. public function log($product_id, $direction, $status, $message) {
  269. $this->db->query("
  270. INSERT INTO `" . DB_PREFIX . "reverb_sync_log`
  271. (`product_id`, `direction`, `status`, `message`, `created_at`)
  272. VALUES (
  273. '" . (int)$product_id . "',
  274. '" . $this->db->escape($direction) . "',
  275. '" . $this->db->escape($status) . "',
  276. '" . $this->db->escape(substr($message, 0, 65535)) . "',
  277. NOW()
  278. )
  279. ");
  280. }
  281. public function clearListingId($product_id) {
  282. $this->db->query("
  283. UPDATE `" . DB_PREFIX . "reverb_product_map`
  284. SET `reverb_listing_id` = '', `last_synced_at` = NULL
  285. WHERE `product_id` = '" . (int)$product_id . "'
  286. ");
  287. }
  288. public function clearSyncLog() {
  289. $this->db->query("DELETE FROM `" . DB_PREFIX . "reverb_sync_log`");
  290. }
  291. public function getSyncLog($limit = 100) {
  292. $query = $this->db->query("
  293. SELECT l.*, pd.name AS product_name
  294. FROM `" . DB_PREFIX . "reverb_sync_log` l
  295. LEFT JOIN `" . DB_PREFIX . "product_description` pd
  296. ON pd.product_id = l.product_id
  297. AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "'
  298. ORDER BY l.created_at DESC
  299. LIMIT " . (int)$limit
  300. );
  301. return $query->rows;
  302. }
  303. // -------------------------------------------------------------------------
  304. // Sync helpers
  305. // -------------------------------------------------------------------------
  306. /**
  307. * Push a single product to Reverb. Creates or updates the listing and uploads images.
  308. *
  309. * @param array $product Product row (product_id, name, model, price, quantity, image, ...).
  310. * @param array $reverb_data Row from reverb_product_map.
  311. * @param array $settings Global Reverb settings array.
  312. * @return string The Reverb listing ID.
  313. */
  314. public function syncProductToReverb(array $product, array $reverb_data, array $settings) {
  315. require_once(DIR_SYSTEM . 'library/reverb/ReverbApi.php');
  316. require_once(DIR_SYSTEM . 'library/reverb/ProductMapper.php');
  317. $api = $this->getApi();
  318. $payload = ProductMapper::toReverb($product, $reverb_data, $settings);
  319. // Photos are plain URL strings sent inside the listing payload (Reverb API v3)
  320. $store_url = $settings['store_url'] ?? '';
  321. $images = $this->getProductImages($product['product_id']);
  322. if (!empty($store_url) && !empty($images)) {
  323. $base = rtrim($store_url, '/') . '/image/';
  324. $photos = [];
  325. foreach ($images as $path) {
  326. if (!empty($path)) {
  327. $photos[] = $base . ltrim($path, '/');
  328. }
  329. }
  330. if (!empty($photos)) {
  331. $payload['photos'] = $photos;
  332. $payload['publish'] = true;
  333. $this->log($product['product_id'], 'push', 'success', 'Including ' . count($photos) . ' photo(s): ' . implode(', ', $photos));
  334. }
  335. } elseif (empty($store_url)) {
  336. $this->log($product['product_id'], 'push', 'error', 'Photo upload skipped: store URL not configured (check System > Settings > Store URL)');
  337. } else {
  338. $this->log($product['product_id'], 'push', 'error', 'No images found for this product in OpenCart.');
  339. }
  340. $listing_id = !empty($reverb_data['reverb_listing_id']) ? $reverb_data['reverb_listing_id'] : null;
  341. if ($listing_id) {
  342. $api->updateListing($listing_id, $payload);
  343. } else {
  344. $response = $api->createListing($payload);
  345. $listing_id = $response['id'] ?? ($response['listing']['id'] ?? null);
  346. if (!$listing_id) {
  347. throw new RuntimeException('Reverb did not return a listing ID after create.');
  348. }
  349. $this->updateListingId($product['product_id'], $listing_id);
  350. }
  351. return $listing_id;
  352. }
  353. // -------------------------------------------------------------------------
  354. // Order import (Reverb → OpenCart)
  355. // -------------------------------------------------------------------------
  356. public function importOrdersFromReverb(array $settings) {
  357. require_once(DIR_SYSTEM . 'library/reverb/ReverbApi.php');
  358. $this->migrate();
  359. $api = $this->getApi();
  360. $last_sync = $this->config->get('module_reverb_order_last_sync');
  361. $imported = 0;
  362. $skipped = 0;
  363. $page = 1;
  364. do {
  365. $response = $api->getOrders($page, 50, $last_sync ?: null);
  366. $orders = $response['orders'] ?? [];
  367. $total_pages = (int)($response['total_pages'] ?? 1);
  368. foreach ($orders as $reverb_order) {
  369. $order_number = $reverb_order['order_number'] ?? null;
  370. if (!$order_number) { $skipped++; continue; }
  371. // Skip already-imported orders
  372. $existing = $this->db->query("
  373. SELECT order_id FROM `" . DB_PREFIX . "reverb_order_map`
  374. WHERE `reverb_order_number` = '" . $this->db->escape($order_number) . "'
  375. ");
  376. if ($existing->num_rows) { $skipped++; continue; }
  377. // Skip cancelled/unpaid
  378. $status = $reverb_order['status'] ?? '';
  379. if (in_array($status, ['cancelled', 'blocked', 'unpaid', 'payment_pending', 'pending_review'])) {
  380. $skipped++; continue;
  381. }
  382. $order_id = $this->createOcOrderFromReverb($reverb_order, $settings);
  383. if ($order_id) {
  384. $this->db->query("
  385. INSERT INTO `" . DB_PREFIX . "reverb_order_map`
  386. (`reverb_order_number`, `order_id`, `reverb_status`, `created_at`)
  387. VALUES (
  388. '" . $this->db->escape($order_number) . "',
  389. '" . (int)$order_id . "',
  390. '" . $this->db->escape($status) . "',
  391. NOW()
  392. )
  393. ");
  394. $this->log(0, 'pull', 'success', 'Imported Reverb order #' . $order_number . ' → OC order #' . $order_id);
  395. $imported++;
  396. }
  397. }
  398. $page++;
  399. } while ($page <= $total_pages);
  400. // Save timestamp so next import only fetches newer orders
  401. $this->load->model('setting/setting');
  402. $this->model_setting_setting->editSettingValue('module_reverb', 'module_reverb_order_last_sync', date('c'));
  403. return ['imported' => $imported, 'skipped' => $skipped];
  404. }
  405. private function createOcOrderFromReverb(array $o, array $settings) {
  406. // Buyer name
  407. $buyer_name = trim($o['buyer_name'] ?? 'Reverb Buyer');
  408. $np = explode(' ', $buyer_name, 2);
  409. $firstname = $np[0];
  410. $lastname = $np[1] ?? '';
  411. // Shipping address
  412. $addr = $o['shipping_address'] ?? [];
  413. $anp = explode(' ', trim($addr['name'] ?? $buyer_name), 2);
  414. $ship_first = $anp[0];
  415. $ship_last = $anp[1] ?? '';
  416. // Amounts
  417. $product_amt = (float)($o['amount_product']['amount'] ?? $o['amount_product'] ?? 0);
  418. $shipping_amt = (float)($o['shipping']['amount'] ?? $o['shipping'] ?? 0);
  419. $total_amt = (float)($o['total']['amount'] ?? $o['total'] ?? ($product_amt + $shipping_amt));
  420. $currency = $o['total']['currency'] ?? $settings['currency'] ?? 'AUD';
  421. // Store and quantity from settings
  422. $order_stores = $settings['order_stores'] ?? [0];
  423. $store_id = (int)($order_stores[0] ?? 0);
  424. $default_qty = max(1, (int)($settings['default_qty'] ?? 1));
  425. $qty = (int)($o['quantity'] ?? 0) ?: $default_qty;
  426. // OC order status mapping
  427. $status_map = ['paid' => 2, 'shipped' => 3, 'received' => 5, 'picked_up' => 5];
  428. $status_id = $status_map[$o['status'] ?? ''] ?? 1;
  429. // Match OC product by Reverb listing ID or SKU
  430. $listing_id = (string)($o['listing_id'] ?? $o['product_id'] ?? '');
  431. $sku = $o['sku'] ?? '';
  432. $title = $o['title'] ?? 'Reverb Item';
  433. $oc_product = ($listing_id ? $this->findOcProductByListingId($listing_id) : null)
  434. ?: ($sku ? $this->findOcProductBySku($sku) : null);
  435. $product_id = $oc_product ? (int)$oc_product['product_id'] : 0;
  436. $product_name = $oc_product ? $oc_product['name'] : $title;
  437. $product_model = $oc_product ? $oc_product['model'] : $sku;
  438. // Currency ID from OC DB
  439. $cq = $this->db->query("SELECT currency_id FROM `" . DB_PREFIX . "currency` WHERE code = '" . $this->db->escape($currency) . "' LIMIT 1");
  440. $currency_id = $cq->num_rows ? (int)$cq->row['currency_id'] : 1;
  441. $date_added = date('Y-m-d H:i:s', strtotime($o['paid_at'] ?? $o['created_at'] ?? 'now'));
  442. $store_name = $this->db->escape($this->config->get('config_name') ?? '');
  443. $store_url = $this->db->escape($settings['store_url'] ?? '');
  444. $lang_id = (int)$this->config->get('config_language_id');
  445. $comment = $this->db->escape('Reverb order #' . ($o['order_number'] ?? ''));
  446. $this->db->query("
  447. INSERT INTO `" . DB_PREFIX . "order` SET
  448. `invoice_prefix` = 'REV-',
  449. `invoice_no` = 0,
  450. `store_id` = $store_id,
  451. `store_name` = '$store_name',
  452. `store_url` = '$store_url',
  453. `customer_id` = 0,
  454. `customer_group_id` = 1,
  455. `firstname` = '" . $this->db->escape($firstname) . "',
  456. `lastname` = '" . $this->db->escape($lastname) . "',
  457. `email` = '" . $this->db->escape($o['buyer_id'] ?? '') . "',
  458. `telephone` = '" . $this->db->escape($addr['phone'] ?? '') . "',
  459. `fax` = '',
  460. `custom_field` = '{}',
  461. `payment_firstname` = '" . $this->db->escape($ship_first) . "',
  462. `payment_lastname` = '" . $this->db->escape($ship_last) . "',
  463. `payment_company` = '',
  464. `payment_address_1` = '" . $this->db->escape($addr['street_address'] ?? '') . "',
  465. `payment_address_2` = '" . $this->db->escape($addr['extended_address'] ?? '') . "',
  466. `payment_city` = '" . $this->db->escape($addr['locality'] ?? '') . "',
  467. `payment_postcode` = '" . $this->db->escape($addr['postal_code'] ?? '') . "',
  468. `payment_country` = '" . $this->db->escape($addr['country_code'] ?? '') . "',
  469. `payment_country_id` = 0,
  470. `payment_zone` = '" . $this->db->escape($addr['region'] ?? '') . "',
  471. `payment_zone_id` = 0,
  472. `payment_address_format` = '',
  473. `payment_custom_field` = '{}',
  474. `payment_method` = 'Reverb',
  475. `payment_code` = 'reverb',
  476. `shipping_firstname` = '" . $this->db->escape($ship_first) . "',
  477. `shipping_lastname` = '" . $this->db->escape($ship_last) . "',
  478. `shipping_company` = '',
  479. `shipping_address_1` = '" . $this->db->escape($addr['street_address'] ?? '') . "',
  480. `shipping_address_2` = '" . $this->db->escape($addr['extended_address'] ?? '') . "',
  481. `shipping_city` = '" . $this->db->escape($addr['locality'] ?? '') . "',
  482. `shipping_postcode` = '" . $this->db->escape($addr['postal_code'] ?? '') . "',
  483. `shipping_country` = '" . $this->db->escape($addr['country_code'] ?? '') . "',
  484. `shipping_country_id` = 0,
  485. `shipping_zone` = '" . $this->db->escape($addr['region'] ?? '') . "',
  486. `shipping_zone_id` = 0,
  487. `shipping_address_format` = '',
  488. `shipping_custom_field` = '{}',
  489. `shipping_method` = 'Reverb',
  490. `shipping_code` = 'reverb.reverb',
  491. `comment` = '$comment',
  492. `total` = '" . number_format($total_amt, 4, '.', '') . "',
  493. `order_status_id` = $status_id,
  494. `affiliate_id` = 0,
  495. `commission` = '0.0000',
  496. `marketing_id` = 0,
  497. `tracking` = '',
  498. `language_id` = $lang_id,
  499. `currency_id` = $currency_id,
  500. `currency_code` = '" . $this->db->escape($currency) . "',
  501. `currency_value` = '1.00000000',
  502. `ip` = '',
  503. `forwarded_ip` = '',
  504. `user_agent` = 'Reverb Import',
  505. `accept_language` = '',
  506. `date_added` = '" . $this->db->escape($date_added) . "',
  507. `date_modified` = NOW()
  508. ");
  509. $order_id = (int)$this->db->getLastId();
  510. if (!$order_id) return null;
  511. // Order product
  512. $unit_price = $qty > 0 ? $product_amt / $qty : $product_amt;
  513. $this->db->query("
  514. INSERT INTO `" . DB_PREFIX . "order_product` SET
  515. `order_id` = $order_id,
  516. `product_id` = $product_id,
  517. `master_id` = 0,
  518. `name` = '" . $this->db->escape($product_name) . "',
  519. `model` = '" . $this->db->escape($product_model) . "',
  520. `quantity` = $qty,
  521. `price` = '" . number_format($unit_price, 4, '.', '') . "',
  522. `total` = '" . number_format($product_amt, 4, '.', '') . "',
  523. `tax` = '0.0000',
  524. `reward` = 0
  525. ");
  526. // Totals
  527. $this->db->query("INSERT INTO `" . DB_PREFIX . "order_total` SET `order_id`=$order_id, `code`='sub_total', `title`='Sub-Total', `value`='" . number_format($product_amt, 4, '.', '') . "', `sort_order`=1");
  528. if ($shipping_amt > 0) {
  529. $this->db->query("INSERT INTO `" . DB_PREFIX . "order_total` SET `order_id`=$order_id, `code`='shipping', `title`='Shipping', `value`='" . number_format($shipping_amt, 4, '.', '') . "', `sort_order`=3");
  530. }
  531. $this->db->query("INSERT INTO `" . DB_PREFIX . "order_total` SET `order_id`=$order_id, `code`='total', `title`='Total', `value`='" . number_format($total_amt, 4, '.', '') . "', `sort_order`=9");
  532. // History
  533. $this->db->query("INSERT INTO `" . DB_PREFIX . "order_history` SET `order_id`=$order_id, `order_status_id`=$status_id, `notify`=0, `comment`='Imported from Reverb', `date_added`=NOW()");
  534. // Decrement OC stock
  535. if ($product_id) {
  536. $this->db->query("UPDATE `" . DB_PREFIX . "product` SET `quantity`=GREATEST(0,`quantity`-$qty) WHERE `product_id`=$product_id");
  537. }
  538. return $order_id;
  539. }
  540. private function findOcProductByListingId($listing_id) {
  541. if (!$listing_id) return null;
  542. $q = $this->db->query("
  543. SELECT p.product_id, pd.name, p.model, p.price
  544. FROM `" . DB_PREFIX . "reverb_product_map` r
  545. JOIN `" . DB_PREFIX . "product` p ON p.product_id = r.product_id
  546. JOIN `" . DB_PREFIX . "product_description` pd ON pd.product_id = p.product_id
  547. AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "'
  548. WHERE r.reverb_listing_id = '" . $this->db->escape($listing_id) . "'
  549. LIMIT 1
  550. ");
  551. return $q->num_rows ? $q->row : null;
  552. }
  553. private function findOcProductBySku($sku) {
  554. if (!$sku) return null;
  555. $q = $this->db->query("
  556. SELECT p.product_id, pd.name, p.model, p.price
  557. FROM `" . DB_PREFIX . "product` p
  558. JOIN `" . DB_PREFIX . "product_description` pd ON pd.product_id = p.product_id
  559. AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "'
  560. WHERE p.model = '" . $this->db->escape($sku) . "'
  561. LIMIT 1
  562. ");
  563. return $q->num_rows ? $q->row : null;
  564. }
  565. // -------------------------------------------------------------------------
  566. // Utility
  567. // -------------------------------------------------------------------------
  568. private function getApi() {
  569. $token = $this->config->get('module_reverb_api_token');
  570. if (empty($token)) {
  571. throw new RuntimeException('Reverb API token is not configured.');
  572. }
  573. require_once(DIR_SYSTEM . 'library/reverb/ReverbApi.php');
  574. return new ReverbApi($token);
  575. }
  576. }