diff --git a/data.json b/data.json new file mode 100644 index 0000000..68874c5 --- /dev/null +++ b/data.json @@ -0,0 +1,33 @@ + + { + "status": "ok", + "message": "", + "data": { + "sku": "2246-911", + "product_name": "KitchPro", + "product_price": "12.480", + "currency": "EUR", + "recommended_retail_price": "32.990", + "image": "https:\/\/zoho-site.com\/cdn\/img\/649036", + "images": "[\"https:\\\/\\\/ourshopcdn.com\\\/01\\\/56\\\/db\\\/40\\\/fb\\\/5d\\\/5e\\\/9f\\\/1c\\\/40\\\/8 + "gif_or_video": "https:\/\/zoho-site.com\/cdn\/img\/1290557", + "description": "Kitchen food processor", + "long_description": "Multi-function kitchen tool grinds, slices, chops and mashes foods to help you quickly", + "categories": "[\"Kitchen\",\"0Promo\",\"0Warranty\",\"0Cross-sell\",\"0Clearance\"]", + "google_categories": "[\"Home & Garden\",\"Kitchen & Dining\",\"Kitchen Appliances\",\"Food Mixers & Blende", + "specifications": "{\"description\":\"KitchPro is a multi-purpose food processor. It is designed to chop, m}", + "dimensions": "{\"weight\":0,\"height\":0,\"width\":0,\"length\":0}", + "stock": "269", + "updated_at": "2025-02-13 14:01:03", + "products_id": [ + 17842 + ], + "ean": "3831125001095", + "has_variations": false + } + } + + + +php cron.php token=2bff5bb42beb2069ee5dc2fdb6f68933 + diff --git a/ecomzone-v10.zip b/ecomzone-v10.zip new file mode 100644 index 0000000..3f1135d Binary files /dev/null and b/ecomzone-v10.zip differ diff --git a/ecomzone-v11.zip b/ecomzone-v11.zip new file mode 100644 index 0000000..92ec522 Binary files /dev/null and b/ecomzone-v11.zip differ diff --git a/ecomzone-v12.zip b/ecomzone-v12.zip new file mode 100644 index 0000000..1e57f6c Binary files /dev/null and b/ecomzone-v12.zip differ diff --git a/ecomzone-v13.zip b/ecomzone-v13.zip new file mode 100644 index 0000000..038fad8 Binary files /dev/null and b/ecomzone-v13.zip differ diff --git a/ecomzone-v14.zip b/ecomzone-v14.zip new file mode 100644 index 0000000..6eb6635 Binary files /dev/null and b/ecomzone-v14.zip differ diff --git a/ecomzone-v15.zip b/ecomzone-v15.zip new file mode 100644 index 0000000..bac8c13 Binary files /dev/null and b/ecomzone-v15.zip differ diff --git a/ecomzone-v16.zip b/ecomzone-v16.zip new file mode 100644 index 0000000..9c3b775 Binary files /dev/null and b/ecomzone-v16.zip differ diff --git a/ecomzone-v17.zip b/ecomzone-v17.zip new file mode 100644 index 0000000..9c3b775 Binary files /dev/null and b/ecomzone-v17.zip differ diff --git a/ecomzone-v18.zip b/ecomzone-v18.zip new file mode 100644 index 0000000..02a676c Binary files /dev/null and b/ecomzone-v18.zip differ diff --git a/ecomzone-v19.zip b/ecomzone-v19.zip new file mode 100644 index 0000000..7e3ca13 Binary files /dev/null and b/ecomzone-v19.zip differ diff --git a/ecomzone-v20.zip b/ecomzone-v20.zip new file mode 100644 index 0000000..d9f663d Binary files /dev/null and b/ecomzone-v20.zip differ diff --git a/ecomzone-v5.2.zip b/ecomzone-v5.2.zip new file mode 100644 index 0000000..45f16f0 Binary files /dev/null and b/ecomzone-v5.2.zip differ diff --git a/ecomzone-v5.3.zip b/ecomzone-v5.3.zip new file mode 100644 index 0000000..28d74e4 Binary files /dev/null and b/ecomzone-v5.3.zip differ diff --git a/ecomzone-v6.1.zip b/ecomzone-v6.1.zip new file mode 100644 index 0000000..ecc81b4 Binary files /dev/null and b/ecomzone-v6.1.zip differ diff --git a/ecomzone-v6.2.zip b/ecomzone-v6.2.zip new file mode 100644 index 0000000..489d794 Binary files /dev/null and b/ecomzone-v6.2.zip differ diff --git a/ecomzone-v6.zip b/ecomzone-v6.zip new file mode 100644 index 0000000..1e365ab Binary files /dev/null and b/ecomzone-v6.zip differ diff --git a/ecomzone-v7.zip b/ecomzone-v7.zip new file mode 100644 index 0000000..037b8cc Binary files /dev/null and b/ecomzone-v7.zip differ diff --git a/ecomzone-v8.zip b/ecomzone-v8.zip new file mode 100644 index 0000000..635ffe6 Binary files /dev/null and b/ecomzone-v8.zip differ diff --git a/ecomzone-v9.zip b/ecomzone-v9.zip new file mode 100644 index 0000000..e6a9047 Binary files /dev/null and b/ecomzone-v9.zip differ diff --git a/ecomzone/classes/EcomZoneApiTest.php b/ecomzone/classes/EcomZoneApiTest.php new file mode 100644 index 0000000..e11f6f2 --- /dev/null +++ b/ecomzone/classes/EcomZoneApiTest.php @@ -0,0 +1,53 @@ + +client = new EcomZoneClient(); + } + + public function testProductFetch() + { + try { + // Get first page with one product + $result = $this->client->getCatalog(1, 1); + + EcomZoneLogger::log("API Test Result", "INFO", [ + "result" => $result, + ]); + + return $result; + } catch (Exception $e) { + EcomZoneLogger::log("API Test Failed", "ERROR", [ + "error" => $e->getMessage(), + ]); + throw $e; + } + } + public function getProducts($page = 1, $perPage = 10) + { + try { + $result = $this->client->getCatalog($page, $perPage); + + return [ + "products" => $result["data"] ?? [], + "total" => $result["total"] ?? 0, + "current_page" => $page, + "per_page" => $perPage, + ]; + } catch (Exception $e) { + EcomZoneLogger::log("Failed to get products", "ERROR", [ + "error" => $e->getMessage(), + ]); + throw $e; + } + } +} + diff --git a/ecomzone/classes/EcomZoneCategoryHandler.php b/ecomzone/classes/EcomZoneCategoryHandler.php new file mode 100644 index 0000000..377df4c --- /dev/null +++ b/ecomzone/classes/EcomZoneCategoryHandler.php @@ -0,0 +1,215 @@ +defaultLangId = (int) Configuration::get("PS_LANG_DEFAULT"); + $this->defaultShopId = (int) Context::getContext()->shop->id; + + EcomZoneLogger::log("Category Handler initialized", "INFO", [ + "default_lang" => $this->defaultLangId, + "shop_id" => $this->defaultShopId, + ]); + } + + public function createCategory(string $name, ?int $parentId = null): int + { + try { + if (isset($this->categoryCache[$name])) { + return $this->categoryCache[$name]; + } + + $parentId = + $parentId ?: (int) Configuration::get("PS_HOME_CATEGORY"); + + $category = new Category(); + $category->name = [$this->defaultLangId => trim($name)]; + $category->link_rewrite = [ + $this->defaultLangId => $this->generateLinkRewrite($name), + ]; + $category->id_parent = $parentId; + $category->active = true; + $category->id_shop_default = $this->defaultShopId; + + // Set shop association for multistore + if (Shop::isFeatureActive()) { + $category->id_shop_list = [$this->defaultShopId]; + } + + if (!$category->add()) { + throw new EcomZoneException( + "Failed to create category: $name", + EcomZoneException::CATEGORY_CREATE_ERROR + ); + } + + $this->categoryCache[$name] = (int) $category->id; + + EcomZoneLogger::log("Category created", "INFO", [ + "name" => $name, + "id" => $category->id, + "parent_id" => $parentId, + ]); + + return $this->categoryCache[$name]; + } catch (Exception $e) { + EcomZoneLogger::log("Category creation failed", "ERROR", [ + "name" => $name, + "error" => $e->getMessage(), + ]); + throw new EcomZoneException( + "Category creation failed: " . $e->getMessage(), + EcomZoneException::CATEGORY_CREATE_ERROR, + $e + ); + } + } + + public function getCategoryIdByName(string $name, ?int $parentId = null): ?int + { + try { + if (isset($this->categoryCache[$name])) { + return $this->categoryCache[$name]; + } + + $name = trim($name); + $sql = 'SELECT c.id_category + FROM `' . + _DB_PREFIX_ . + 'category_lang` cl + JOIN `' . + _DB_PREFIX_ . + 'category` c ON c.id_category = cl.id_category + WHERE cl.name = "' . + pSQL($name) . + '" + AND cl.id_lang = ' . + (int) $this->defaultLangId . + ' + AND c.active = 1'; + + if ($parentId !== null) { + $sql .= ' AND c.id_parent = ' . (int) $parentId; + } + + $categoryId = (int) Db::getInstance()->getValue($sql); + + if ($categoryId) { + $this->categoryCache[$name] = $categoryId; + return $categoryId; + } + + return null; + } catch (Exception $e) { + EcomZoneLogger::log("Category lookup failed", "ERROR", [ + "name" => $name, + "error" => $e->getMessage(), + ]); + return null; + } + } + + public function processCategoriesFromString(string $categories): array + { + $categoryNames = array_map('trim', explode('>', $categories)); + $categoryIds = []; + $parentId = null; + + foreach ($categoryNames as $name) { + $categoryId = $this->getCategoryIdByName($name, $parentId); + if (!$categoryId) { + $categoryId = $this->createCategory($name, $parentId); + } + if ($categoryId) { + $categoryIds[] = $categoryId; + $parentId = $categoryId; + } + } + + return $categoryIds; + } + + private function generateLinkRewrite(string $name): string + { + if (isset($this->categoryLinkRewriteCache[$name])) { + return $this->categoryLinkRewriteCache[$name]; + } + + $linkRewrite = Tools::str2url($name); + + // Ensure uniqueness + $suffix = 1; + $originalLinkRewrite = $linkRewrite; + while ($this->linkRewriteExists($linkRewrite)) { + $linkRewrite = $originalLinkRewrite . "-" . $suffix++; + } + + $this->categoryLinkRewriteCache[$name] = $linkRewrite; + return $linkRewrite; + } + + private function linkRewriteExists(string $linkRewrite): bool + { + return (bool) Db::getInstance()->getValue( + 'SELECT COUNT(*) + FROM `' . + _DB_PREFIX_ . + 'category_lang` cl + JOIN `' . + _DB_PREFIX_ . + 'category_shop` cs + ON cl.id_category = cs.id_category + WHERE cl.link_rewrite = "' . + pSQL($linkRewrite) . + '" + AND cl.id_lang = ' . + (int) $this->defaultLangId . + ' + AND cs.id_shop = ' . + (int) $this->defaultShopId + ); + } + + public function createCategoryPath(array $categoryNames): int + { + $parentId = (int) Configuration::get("PS_HOME_CATEGORY"); + $lastCategoryId = $parentId; + + foreach ($categoryNames as $categoryName) { + $categoryName = trim($categoryName); + if (empty($categoryName)) { + continue; + } + + try { + $categoryId = $this->getCategoryIdByName($categoryName); + if (!$categoryId) { + $categoryId = $this->createCategory( + $categoryName, + $parentId + ); + } + $lastCategoryId = $categoryId; + $parentId = $categoryId; + } catch (Exception $e) { + EcomZoneLogger::log("Category path creation failed", "ERROR", [ + "category" => $categoryName, + "error" => $e->getMessage(), + ]); + break; + } + } + + return $lastCategoryId; + } +} + diff --git a/ecomzone/classes/EcomZoneClient.php b/ecomzone/classes/EcomZoneClient.php index fe3d14f..f282a9c 100644 --- a/ecomzone/classes/EcomZoneClient.php +++ b/ecomzone/classes/EcomZoneClient.php @@ -1,107 +1,152 @@ apiToken = 'klRyAdrXaxL0s6PEUp7LDlH6T8aPSCtBY8NiEHsHiWpc6646K2TZPi5KMxUg'; - $this->apiUrl = Configuration::get('ECOMZONE_API_URL'); - - if (empty($this->apiUrl)) { - throw new Exception('API URL not configured'); + $this->apiToken = Configuration::get("ECOMZONE_API_TOKEN"); + $this->apiUrl = Configuration::get("ECOMZONE_API_URL"); + + if (empty($this->apiToken)) { + throw new EcomZoneException( + "API token not configured", + EcomZoneException::API_ERROR + ); } + + if (empty($this->apiUrl)) { + throw new EcomZoneException( + "API URL not configured", + EcomZoneException::API_ERROR + ); + } + + EcomZoneLogger::log("API Client initialized", "INFO", [ + "url" => $this->apiUrl, + ]); } public function getCatalog($page = 1, $perPage = 100) { - $url = $this->apiUrl . '/catalog?page=' . $page . '&per_page=' . $perPage; - - EcomZoneLogger::log("Making API request", 'INFO', ['url' => $url]); - return $this->makeRequest('GET', $url); + $url = $this->apiUrl . "/catalog"; + $params = [ + "page" => $page, + "per_page" => $perPage, + ]; + + return $this->makeRequest("GET", $url, $params); } public function getProduct($sku) { - return $this->makeRequest('GET', $this->apiUrl . '/product/' . $sku); + $url = $this->apiUrl . "/product/" . urlencode($sku); + return $this->makeRequest("GET", $url); } - public function createOrder($orderData) - { - return $this->makeRequest('POST', $this->apiUrl . '/ordering', $orderData); - } - - public function getOrder($orderId) - { - return $this->makeRequest('GET', $this->apiUrl . '/order/' . $orderId); - } - - private function makeRequest($method, $url, $data = null) + private function makeRequest($method, $url, $params = [], $data = null) { $retryCount = 0; - $maxRetries = 5; - $backoff = 1; // initial backoff time in seconds - do { - $curl = curl_init(); - $headers = [ - 'Authorization: Bearer ' . $this->apiToken, - 'Accept: application/json', - 'Content-Type: application/json', - ]; - - $options = [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_ENCODING => '', - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 30, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_CUSTOMREQUEST => $method, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_SSL_VERIFYPEER => false, // Temporarily disable SSL verification for testing - ]; - - if ($data !== null) { - $options[CURLOPT_POSTFIELDS] = json_encode($data); - } - - curl_setopt_array($curl, $options); - - $response = curl_exec($curl); - $err = curl_error($curl); - $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); - - EcomZoneLogger::log("API Response", 'INFO', [ - 'url' => $url, - 'method' => $method, - 'http_code' => $httpCode, - 'curl_error' => $err, - 'response' => $response - ]); - - curl_close($curl); - - if ($err) { - if (strpos($err, 'Connection timed out') !== false && $retryCount < $maxRetries) { - EcomZoneLogger::log("Connection timed out. Retrying in $backoff seconds...", 'WARNING'); - sleep($backoff); - $backoff *= 2; // exponential backoff - $retryCount++; - } else { - throw new Exception('cURL Error: ' . $err); - } - } else if ($httpCode >= 400) { - throw new Exception('API Error: HTTP ' . $httpCode . ' - ' . $response); - } else { - $decodedResponse = json_decode($response, true); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new Exception('Invalid JSON response: ' . json_last_error_msg()); - } - return $decodedResponse; - } - } while ($retryCount < $maxRetries); - } + $lastError = null; + + do { + try { + EcomZoneLogger::log("Making API request", "INFO", [ + "method" => $method, + "url" => $url, + "params" => $params, + "retry" => $retryCount, + ]); + + // Add query parameters + if (!empty($params)) { + $url .= + (strpos($url, "?") === false ? "?" : "&") . + http_build_query($params); + } + + $curl = curl_init(); + $options = [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => [ + "Authorization: Bearer " . $this->apiToken, + "Accept: application/json", + "Content-Type: application/json", + ], + ]; + + if ($data !== null) { + $options[CURLOPT_POSTFIELDS] = json_encode($data); + } + + curl_setopt_array($curl, $options); + $response = curl_exec($curl); + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $error = curl_error($curl); + curl_close($curl); + + if ($error) { + throw new Exception("cURL Error: " . $error); + } + + if ($httpCode >= 400) { + throw new Exception( + "HTTP Error: " . $httpCode . " Response: " . $response + ); + } + + $decodedResponse = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception("Invalid JSON response"); + } + + EcomZoneLogger::log("API request successful", "INFO", [ + "http_code" => $httpCode, + ]); + + return $decodedResponse; + } catch (Exception $e) { + $lastError = $e; + $retryCount++; + + EcomZoneLogger::log("API request failed", "ERROR", [ + "error" => $e->getMessage(), + "retry" => $retryCount, + ]); + + if ($retryCount < $this->maxRetries) { + sleep(pow(2, $retryCount)); + continue; + } + + throw new EcomZoneException( + "API request failed after {$this->maxRetries} retries: " . + $e->getMessage(), + EcomZoneException::API_ERROR, + $e + ); + } + } while ($retryCount < $this->maxRetries); + + throw new EcomZoneException( + "Max retries reached: " . $lastError->getMessage(), + EcomZoneException::API_ERROR, + $lastError + ); + } } + diff --git a/ecomzone/classes/EcomZoneCronTask.php b/ecomzone/classes/EcomZoneCronTask.php deleted file mode 100644 index 212c893..0000000 --- a/ecomzone/classes/EcomZoneCronTask.php +++ /dev/null @@ -1,40 +0,0 @@ -name = 'ecomzone_cron'; - $this->title = $this->l('EcomZone Product Sync'); - $this->cron_frequency = 3600 * 2; // Run every hour, change this to 2 hours!!! - } - - public function install() - { - if (!parent::install()) { - return false; - } - - Configuration::updateValue('ECOMZONE_LAST_CRON_RUN', ''); - return true; - } - - public function run() - { - try { - EcomZoneLogger::log('Starting scheduled product sync'); - - $productSync = new EcomZoneProductSync(); - $result = $produgtSync->importProducts(); - - Configuration::updateValue('ECOMZONE_LAST_CRON_RUN', date('Y-m-d H:i:s')); - EcomZoneLogger::log('Scheduled product sync completed', 'INFO', $result); - - return true; - } catch (Exception $e) { - EcomZoneLogger::log('Scheduled product sync failed', 'ERROR', ['error' => $e->getMessage()]); - return false; - } - } -} diff --git a/ecomzone/classes/EcomZoneException.php b/ecomzone/classes/EcomZoneException.php new file mode 100644 index 0000000..ca8a374 --- /dev/null +++ b/ecomzone/classes/EcomZoneException.php @@ -0,0 +1,13 @@ + self::$maxLogSize) { + $backup = self::$logFile . "." . date("Y-m-d-H-i-s") . ".bak"; + rename(self::$logFile, $backup); + touch(self::$logFile); + chmod(self::$logFile, 0666); + } + } +} + diff --git a/ecomzone/classes/EcomZoneOrderSync.php b/ecomzone/classes/EcomZoneOrderSync.php deleted file mode 100644 index 6e9bc4f..0000000 --- a/ecomzone/classes/EcomZoneOrderSync.php +++ /dev/null @@ -1,81 +0,0 @@ -client = new EcomZoneClient(); - } - - public function syncOrder($orderId) - { - try { - EcomZoneLogger::log("Starting order sync", 'INFO', ['order_id' => $orderId]); - - $order = new Order($orderId); - $customer = new Customer($order->id_customer); - $address = new Address($order->id_address_delivery); - - $orderData = $this->prepareOrderData($order, $customer, $address); - $result = $this->client->createOrder($orderData); - - EcomZoneLogger::log("Order sync completed", 'INFO', [ - 'order_id' => $orderId, - 'result' => $result - ]); - - return $result; - } catch (Exception $e) { - EcomZoneLogger::log("Order sync failed", 'ERROR', [ - 'order_id' => $orderId, - 'error' => $e->getMessage() - ]); - throw $e; - } - } - - private function prepareOrderData($order, $customer, $address) - { - return [ - 'order_index' => $order->id, - 'ext_id' => $order->reference, - 'payment' => [ - 'method' => $this->getPaymentMethod($order), - 'customer_price' => $order->total_paid - ], - 'customer_data' => [ - 'full_name' => $customer->firstname . ' ' . $customer->lastname, - 'email' => $customer->email, - 'phone_number' => $address->phone, - 'country' => Country::getIsoById($address->id_country), - 'address' => $address->address1 . ' ' . $address->address2, - 'city' => $address->city, - 'post_code' => $address->postcode - ], - 'items' => $this->getOrderItems($order) - ]; - } - - private function getPaymentMethod($order) - { - // Map PrestaShop payment modules to eComZone payment methods - return $order->module === 'cashondelivery' ? 'cod' : 'pp'; - } - - private function getOrderItems($order) - { - $items = []; - $products = $order->getProducts(); - - foreach ($products as $product) { - $items[] = [ - 'full_sku' => $product['reference'], - 'quantity' => $product['product_quantity'] - ]; - } - - return $items; - } -} \ No newline at end of file diff --git a/ecomzone/classes/EcomZoneProductImport.php b/ecomzone/classes/EcomZoneProductImport.php new file mode 100644 index 0000000..c5029d2 --- /dev/null +++ b/ecomzone/classes/EcomZoneProductImport.php @@ -0,0 +1,345 @@ +module = Module::getInstanceByName("ecomzone"); + $this->defaultLangId = (int) Configuration::get("PS_LANG_DEFAULT"); + $this->defaultShopId = (int) Context::getContext()->shop->id; + } + + public function importProduct($productData) + { + try { + $data = $productData["data"]; + + // Start transaction + Db::getInstance()->execute("START TRANSACTION"); + + // Get or create product + $product = $this->getOrCreateProduct($data["sku"]); + + // Update basic information + $this->updateBasicInfo($product, $data); + + // Update categories + $this->updateCategories($product, $data); + + // Update images + $this->updateImages($product, $data); + + // Update specifications + $this->updateSpecifications($product, $data); + + // Update stock + $this->updateStock($product, $data); + + // Commit transaction + Db::getInstance()->execute("COMMIT"); + + EcomZoneLogger::log("Product imported successfully", "INFO", [ + "sku" => $data["sku"], + ]); + + return true; + } catch (Exception $e) { + Db::getInstance()->execute("ROLLBACK"); + EcomZoneLogger::log("Product import failed", "ERROR", [ + "sku" => $data["sku"] ?? "unknown", + "error" => $e->getMessage(), + ]); + throw $e; + } + } + + private function getOrCreateProduct($sku) + { + $productId = Product::getIdByReference($sku); + + if ($productId) { + $product = new Product($productId); + } else { + $product = new Product(); + $product->reference = $sku; + } + + return $product; + } + + private function updateBasicInfo($product, $data) + { + $product->name[$this->defaultLangId] = $data["product_name"]; + $product->description[$this->defaultLangId] = $data["long_description"]; + $product->description_short[$this->defaultLangId] = + $data["description"]; + $product->price = (float) $data["product_price"]; + $product->wholesale_price = + (float) ($data["recommended_retail_price"] ?? 0); + $product->ean13 = $data["ean"] ?? ""; + + // Product settings + $product->active = true; + $product->visibility = "both"; + $product->available_for_order = true; + $product->show_price = true; + $product->indexed = 1; + + // Dimensions + if (!empty($data["dimensions"])) { + $dimensions = json_decode($data["dimensions"], true); + $product->weight = (float) ($dimensions["weight"] ?? 0); + $product->height = (float) ($dimensions["height"] ?? 0); + $product->width = (float) ($dimensions["width"] ?? 0); + $product->depth = (float) ($dimensions["length"] ?? 0); + } + + if (!$product->id) { + $product->add(); + } else { + $product->update(); + } + } + + private function updateCategories($product, $data) + { + $categoryIds = []; + + // Process main categories + if (!empty($data["categories"])) { + $categories = json_decode($data["categories"], true); + foreach ($categories as $categoryName) { + if (strpos($categoryName, "0") === 0) { + continue; + } // Skip special categories + $categoryId = $this->getOrCreateCategory($categoryName); + if ($categoryId) { + $categoryIds[] = $categoryId; + } + } + } + + // Process Google categories + if (!empty($data["google_categories"])) { + $googleCategories = json_decode($data["google_categories"], true); + foreach ($googleCategories as $categoryName) { + $categoryId = $this->getOrCreateCategory($categoryName); + if ($categoryId) { + $categoryIds[] = $categoryId; + } + } + } + + // Ensure we have at least default category + if (empty($categoryIds)) { + $categoryIds[] = (int) Configuration::get("PS_HOME_CATEGORY"); + } + + $categoryIds = array_unique($categoryIds); + $product->id_category_default = reset($categoryIds); + $product->updateCategories($categoryIds); + } + + private function getOrCreateCategory($name) + { + $name = trim($name); + if (empty($name)) { + return null; + } + + // Try to find existing category + $categoryId = (int) Db::getInstance()->getValue( + 'SELECT c.id_category + FROM `' . + _DB_PREFIX_ . + 'category_lang` cl + JOIN `' . + _DB_PREFIX_ . + 'category` c ON c.id_category = cl.id_category + WHERE cl.name = "' . + pSQL($name) . + '" + AND cl.id_lang = ' . + (int) $this->defaultLangId + ); + + if ($categoryId) { + return $categoryId; + } + + // Create new category + $category = new Category(); + $category->name = [$this->defaultLangId => $name]; + $category->link_rewrite = [ + $this->defaultLangId => Tools::str2url($name), + ]; + $category->id_parent = (int) Configuration::get("PS_HOME_CATEGORY"); + $category->active = true; + + if ($category->add()) { + return (int) $category->id; + } + + return null; + } + + private function updateImages($product, $data) + { + $imageUrls = []; + + // Main image + if (!empty($data["image"])) { + $imageUrls[] = $data["image"]; + } + + // Additional images + if (!empty($data["images"])) { + $additionalImages = json_decode($data["images"], true); + if (is_array($additionalImages)) { + $imageUrls = array_merge($imageUrls, $additionalImages); + } + } + + // GIF or video thumbnail + if (!empty($data["gif_or_video"])) { + $imageUrls[] = $data["gif_or_video"]; + } + + if (empty($imageUrls)) { + return; + } + + // Delete existing images + $product->deleteImages(); + + // Import new images + foreach ($imageUrls as $index => $imageUrl) { + $this->importImage($product, $imageUrl, $index === 0); + } + } + + private function importImage($product, $imageUrl, $cover = false) + { + $tmpFile = tempnam(_PS_TMP_IMG_DIR_, "ecomzone_"); + + try { + if (!copy($imageUrl, $tmpFile)) { + throw new Exception("Failed to download image"); + } + + $image = new Image(); + $image->id_product = $product->id; + $image->position = Image::getHighestPosition($product->id) + 1; + $image->cover = $cover; + + if (!$image->add()) { + throw new Exception("Failed to create image record"); + } + + $imagesTypes = ImageType::getImagesTypes("products"); + foreach ($imagesTypes as $type) { + ImageManager::resize( + $tmpFile, + _PS_PROD_IMG_DIR_ . + $image->getExistingImgPath() . + "." . + $type["name"] . + ".jpg", + $type["width"], + $type["height"] + ); + } + + // Save original image + ImageManager::resize( + $tmpFile, + _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . ".jpg" + ); + } catch (Exception $e) { + EcomZoneLogger::log("Image import failed", "ERROR", [ + "url" => $imageUrl, + "error" => $e->getMessage(), + ]); + } finally { + if (file_exists($tmpFile)) { + unlink($tmpFile); + } + } + } + + private function updateSpecifications($product, $data) + { + if (empty($data["specifications"])) { + return; + } + + try { + $specifications = json_decode($data["specifications"], true); + if (!is_array($specifications)) { + return; + } + + foreach ($specifications as $name => $value) { + if (empty($value)) { + continue; + } + + // Get or create feature + $featureId = Feature::getIdByName($name); + if (!$featureId) { + $feature = new Feature(); + $feature->name[$this->defaultLangId] = $name; + if (!$feature->add()) { + continue; + } + $featureId = $feature->id; + } + + // Get or create feature value + $featureValue = new FeatureValue(); + $featureValue->id_feature = $featureId; + $featureValue->value[$this->defaultLangId] = $value; + if (!$featureValue->add()) { + continue; + } + + // Associate with product + $product->addFeaturesToDB([$featureId => $featureValue->id]); + } + } catch (Exception $e) { + EcomZoneLogger::log("Failed to update specifications", "ERROR", [ + "sku" => $product->reference, + "error" => $e->getMessage(), + ]); + } + } + + private function updateStock($product, $data) + { + try { + $quantity = (int) ($data["stock_quantity"] ?? $data["stock"] ?? 0); + StockAvailable::setQuantity($product->id, 0, $quantity); + + // Update out of stock behavior + StockAvailable::setProductOutOfStock( + $product->id, + $quantity > 0 ? 2 : 0, // 2 = Allow orders, 0 = Deny orders + $this->defaultShopId + ); + } catch (Exception $e) { + throw new EcomZoneException( + "Failed to update stock: " . $e->getMessage(), + EcomZoneException::INVALID_PRODUCT_DATA, + $e + ); + } + } +} + diff --git a/ecomzone/classes/EcomZoneProductSync.php b/ecomzone/classes/EcomZoneProductSync.php index 73b637d..15a8591 100644 --- a/ecomzone/classes/EcomZoneProductSync.php +++ b/ecomzone/classes/EcomZoneProductSync.php @@ -1,29 +1,558 @@ client = new EcomZoneClient(); + $this->categoryHandler = new EcomZoneCategoryHandler(); + $this->defaultLangId = (int) Configuration::get("PS_LANG_DEFAULT"); + $this->defaultShopId = (int) Context::getContext()->shop->id; + + EcomZoneLogger::log("Product Sync initialized", "INFO"); } - - private function resizeImage($src, $dest, $width, $height) + + public function importProducts(int $perPage = 100): array { - list($srcWidth, $srcHeight, $type) = getimagesize($src); - $srcImage = $this->createImageFromType($src, $type); - - $destImage = imagecreatetruecolor($width, $height); - imagecopyresampled($destImage, $srcImage, 0, 0, 0, 0, $width, $height, $srcWidth, $srcHeight); - - $this->saveImageFromType($destImage, $dest, $type); - - imagedestroy($srcImage); - imagedestroy($destImage); + $page = 1; + $totalImported = 0; + $totalAvailable = 0; + $errors = []; + + try { + EcomZoneLogger::log("Starting product import", "INFO", [ + "per_page" => $perPage, + ]); + + do { + $catalog = $this->client->getCatalog($page, $perPage); + + if (!isset($catalog["data"]) || !is_array($catalog["data"])) { + throw new EcomZoneException( + "Invalid catalog response", + EcomZoneException::API_ERROR + ); + } + + foreach ($catalog["data"] as $product) { + try { + if ($this->importSingleProduct($product)) { + $totalImported++; + } + } catch (Exception $e) { + $errors[] = [ + "sku" => $product["sku"] ?? "unknown", + "error" => $e->getMessage(), + ]; + EcomZoneLogger::log("Product import failed", "ERROR", [ + "sku" => $product["sku"] ?? "unknown", + "error" => $e->getMessage(), + ]); + } + } + + $totalAvailable = $catalog["total"] ?? 0; + $page++; + + EcomZoneLogger::log("Page processed", "INFO", [ + "page" => $page - 1, + "imported" => $totalImported, + "total" => $totalAvailable, + ]); + } while ( + isset($catalog["next_page_url"]) && + $catalog["next_page_url"] !== null + ); + + $this->clearCache(); + + return [ + "success" => true, + "imported" => $totalImported, + "total" => $totalAvailable, + "errors" => $errors, + ]; + } catch (Exception $e) { + EcomZoneLogger::log("Product import process failed", "ERROR", [ + "error" => $e->getMessage(), + ]); + throw $e; + } } - - private function createImageFromType($filename, $type) + + public function importSingleProduct(array $productData): bool + { + try { + // Get SKU from catalog data + $sku = $productData["sku"] ?? $productData["data"]["sku"] ?? null; + + if (!$sku) { + throw new EcomZoneException("Missing SKU in product data"); + } + + // Fetch complete product data from product endpoint + $fullProductData = $this->client->getProduct($sku); + + if (!isset($fullProductData["data"])) { + throw new EcomZoneException("Invalid product response"); + } + + $data = $fullProductData["data"]; + + EcomZoneLogger::log("Processing product", "INFO", [ + "sku" => $sku, + "categories" => $data["categories"] ?? null, + "google_categories" => $data["google_categories"] ?? null + ]); + + $this->validateProductData($data); + + // Start transaction + Db::getInstance()->execute("START TRANSACTION"); + + $product = $this->getOrCreateProduct($sku); + + // Update product data + $this->updateProductData($product, $data); + $this->updateProductCategories($product, $data); + $this->updateProductStock($product, $data); + $this->processProductImages($product, $data); + + // Commit transaction + Db::getInstance()->execute("COMMIT"); + + return true; + } catch (Exception $e) { + Db::getInstance()->execute("ROLLBACK"); + EcomZoneLogger::log("Product import failed", "ERROR", [ + "sku" => $sku ?? "unknown", + "error" => $e->getMessage() + ]); + return false; + } + } + + private function validateProductData(array $data): void + { + $requiredFields = [ + 'sku', + 'product_name', + 'product_price', + 'description', + 'stock' + ]; + + foreach ($requiredFields as $field) { + if (empty($data[$field])) { + throw new EcomZoneException( + "Missing required field: {$field}", + EcomZoneException::VALIDATION_ERROR + ); + } + } + } + + private function getOrCreateProduct(string $sku): Product + { + try { + $productId = Product::getIdByReference($sku); + + if ($productId) { + $product = new Product($productId); + if (!Validate::isLoadedObject($product)) { + throw new Exception("Invalid product object"); + } + return $product; + } + + $product = new Product(); + $product->reference = $sku; + return $product; + } catch (Exception $e) { + throw new EcomZoneException( + "Failed to get/create product: " . $e->getMessage(), + EcomZoneException::INVALID_PRODUCT_DATA, + $e + ); + } + } + + private function updateProductData(Product $product, array $data): void + { + try { + // Basic information + $product->name[$this->defaultLangId] = $data['product_name']; + $product->description[$this->defaultLangId] = $data['long_description'] ?? $data['description']; + $product->description_short[$this->defaultLangId] = $data['description']; + $product->price = (float)$data['product_price']; + $product->wholesale_price = (float)($data['recommended_retail_price'] ?? 0); + $product->reference = $data['sku']; + $product->ean13 = $data['ean'] ?? ''; + + // Shop configuration + $product->id_shop_default = $this->defaultShopId; + $product->active = true; + $product->visibility = 'both'; + $product->available_for_order = true; + $product->show_price = true; + $product->indexed = 1; + + // Update dimensions if available + if (!empty($data['dimensions'])) { + $dimensions = json_decode($data['dimensions'], true); + if (is_array($dimensions)) { + $product->weight = (float)($dimensions['weight'] ?? 0); + $product->height = (float)($dimensions['height'] ?? 0); + $product->width = (float)($dimensions['width'] ?? 0); + $product->depth = (float)($dimensions['length'] ?? 0); + } + } + + // Set tax rules if configured + $taxRulesGroupId = (int)Configuration::get('PS_TAX_RULES_GROUP_DEFAULT'); + if ($taxRulesGroupId) { + $product->id_tax_rules_group = $taxRulesGroupId; + } + + // Save the product + if (!$product->id) { + if (!$product->add()) { + throw new EcomZoneException('Failed to create product'); + } + } else { + if (!$product->update()) { + throw new EcomZoneException('Failed to update product'); + } + } + + // Update shop association + if (Shop::isFeatureActive()) { + Shop::setContext(Shop::CONTEXT_SHOP, $this->defaultShopId); + $product->id_shop_list = [$this->defaultShopId]; + $product->id_shop_default = $this->defaultShopId; + + // Associate product with shop + Db::getInstance()->execute(' + INSERT IGNORE INTO '._DB_PREFIX_.'product_shop + (id_product, id_shop, price, wholesale_price) + VALUES + ('.(int)$product->id.', '.(int)$this->defaultShopId.', + '.(float)$product->price.', '.(float)$product->wholesale_price.') + '); + } + + // Process specifications if available + if (!empty($data['specifications'])) { + $this->updateProductFeatures($product, $data['specifications']); + } + + } catch (Exception $e) { + throw new EcomZoneException( + 'Failed to update product data: ' . $e->getMessage(), + EcomZoneException::INVALID_PRODUCT_DATA, + $e + ); + } + } + + private function updateProductCategories(Product $product, array $data): void + { + try { + $categoryIds = []; + + // Process main categories + if (!empty($data['categories'])) { + $categories = json_decode($data['categories'], true); + if (is_array($categories)) { + foreach ($categories as $categoryPath) { + // Skip special categories that start with '0' + if (strpos($categoryPath, '0') === 0) { + continue; + } + // Create each category as a single path + $categoryId = $this->categoryHandler->createCategoryPath([$categoryPath]); + if ($categoryId) { + $categoryIds[] = $categoryId; + } + } + } + } + + // Process Google categories + if (!empty($data['google_categories'])) { + $googleCategories = json_decode($data['google_categories'], true); + if (is_array($googleCategories)) { + $categoryId = $this->categoryHandler->createCategoryPath($googleCategories); + if ($categoryId) { + $categoryIds[] = $categoryId; + } + } + } + + // Ensure we have at least the default category + if (empty($categoryIds)) { + $categoryIds[] = (int)Configuration::get('PS_HOME_CATEGORY'); + } + + // Remove duplicates + $categoryIds = array_unique($categoryIds); + + // Set default category + $product->id_category_default = reset($categoryIds); + + // Save the product to ensure it has an ID + if (!$product->id) { + $product->add(); + } else { + $product->update(); + } + + // Clear existing category associations + Db::getInstance()->execute(' + DELETE FROM `'._DB_PREFIX_.'category_product` + WHERE id_product = '.(int)$product->id + ); + + // Insert new category associations + foreach ($categoryIds as $categoryId) { + Db::getInstance()->execute(' + INSERT INTO `'._DB_PREFIX_.'category_product` + (id_category, id_product, position) + VALUES + ('.(int)$categoryId.', '.(int)$product->id.', 0) + '); + } + + // Update shop associations if needed + if (Shop::isFeatureActive()) { + $product->id_shop_list = [$this->defaultShopId]; + $product->id_shop_default = $this->defaultShopId; + } + + EcomZoneLogger::log('Categories updated', 'DEBUG', [ + 'product' => $product->reference, + 'category_ids' => $categoryIds + ]); + + } catch (Exception $e) { + throw new EcomZoneException( + 'Failed to update product categories: ' . $e->getMessage(), + EcomZoneException::CATEGORY_CREATE_ERROR, + $e + ); + } + } + + private function updateProductStock(Product $product, array $data): void + { + try { + $quantity = (int) ($data['stock_quantity'] ?? 0); + StockAvailable::setQuantity($product->id, 0, $quantity); + + // Update out of stock behavior + StockAvailable::setProductOutOfStock( + $product->id, + $quantity > 0 ? 2 : 0, // 2 = Allow orders, 0 = Deny orders + $this->defaultShopId + ); + } catch (Exception $e) { + EcomZoneLogger::log("Failed to update stock", "WARNING", [ + "product" => $product->reference, + "error" => $e->getMessage() + ]); + } + } + + private function processProductImages(Product $product, array $data): void + { + try { + $imageUrls = []; + + // Process main image + if (!empty($data["image"])) { + $imageUrls[] = $data["image"]; + } + + // Process additional images + if (!empty($data["images"])) { + $additionalImages = json_decode($data["images"], true); + if (is_array($additionalImages)) { + $imageUrls = array_merge($imageUrls, $additionalImages); + } + } + + if (empty($imageUrls)) { + return; + } + + // Delete existing images if any + $product->deleteImages(); + + foreach ($imageUrls as $index => $imageUrl) { + $this->importProductImage($product, $imageUrl, $index === 0); + } + } catch (Exception $e) { + EcomZoneLogger::log("Failed to process product images", "WARNING", [ + "sku" => $product->reference, + "error" => $e->getMessage(), + ]); + } + } + + private function importProductImage(Product $product, string $imageUrl, bool $cover = false): void + { + try { + // Create temporary file + $tempFile = tempnam(_PS_TMP_IMG_DIR_, 'import_'); + + // Download image + if (!copy($imageUrl, $tempFile)) { + throw new Exception("Failed to download image: " . $imageUrl); + } + + // Create image object + $image = new Image(); + $image->id_product = $product->id; + $image->position = Image::getHighestPosition($product->id) + 1; + $image->cover = $cover; + + if (!$image->add()) { + throw new Exception("Failed to create image record"); + } + + // Create product image directory if it doesn't exist + $imagePath = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath(); + $imageDir = dirname($imagePath); + + if (!file_exists($imageDir)) { + mkdir($imageDir, 0755, true); + } + + // Generate image + if (!ImageManager::resize( + $tempFile, + $imagePath . '.jpg' + )) { + throw new Exception("Failed to process image"); + } + + // Generate thumbnails + $this->generateThumbnails($image); + + // Clean up + unlink($tempFile); + + } catch (Exception $e) { + EcomZoneLogger::log("Failed to import product image", "ERROR", [ + "product" => $product->reference, + "image_url" => $imageUrl, + "error" => $e->getMessage() + ]); + } + } + + private function generateThumbnails(Image $image): void + { + try { + $imageTypes = ImageType::getImagesTypes('products'); + foreach ($imageTypes as $imageType) { + $dir = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath(); + if (!file_exists($dir)) { + mkdir($dir, 0755, true); + } + ImageManager::resize( + _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg', + _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg', + (int)$imageType['width'], + (int)$imageType['height'] + ); + } + } catch (Exception $e) { + EcomZoneLogger::log("Failed to generate thumbnails", "WARNING", [ + "image_id" => $image->id, + "error" => $e->getMessage() + ]); + } + } + + private function generateImageType( + string $srcPath, + string $destPath, + ?int $width, + ?int $height + ): void { + try { + $this->createDirectoryIfNotExists(dirname($destPath)); + + if ($width === null || $height === null) { + copy($srcPath, $destPath); + return; + } + + $imageInfo = getimagesize($srcPath); + if (!$imageInfo) { + throw new Exception("Invalid image file"); + } + + $srcImage = $this->createImageFromType($srcPath, $imageInfo[2]); + $destImage = imagecreatetruecolor($width, $height); + + // Preserve transparency for PNG images + if ($imageInfo[2] === IMAGETYPE_PNG) { + imagealphablending($destImage, false); + imagesavealpha($destImage, true); + $transparent = imagecolorallocatealpha( + $destImage, + 255, + 255, + 255, + 127 + ); + imagefilledrectangle( + $destImage, + 0, + 0, + $width, + $height, + $transparent + ); + } + + imagecopyresampled( + $destImage, + $srcImage, + 0, + 0, + 0, + 0, + $width, + $height, + $imageInfo[0], + $imageInfo[1] + ); + + imagejpeg($destImage, $destPath, 95); + + imagedestroy($srcImage); + imagedestroy($destImage); + } catch (Exception $e) { + throw new EcomZoneException( + "Failed to generate image type: " . $e->getMessage(), + EcomZoneException::IMAGE_PROCESSING_ERROR, + $e + ); + } + } + + private function createImageFromType(string $filename, int $type) { switch ($type) { case IMAGETYPE_JPEG: @@ -36,236 +565,159 @@ class EcomZoneProductSync throw new Exception("Unsupported image type: " . $type); } } - - private function saveImageFromType($image, $filename, $type) + + private function createDirectoryIfNotExists(string $directory): void { - switch ($type) { - case IMAGETYPE_JPEG: - $this->createDirectoryIfNotExists(dirname($filename)); - imagejpeg($image, $filename); - break; - case IMAGETYPE_PNG: - $this->createDirectoryIfNotExists(dirname($filename)); - imagepng($image, $filename); - break; - case IMAGETYPE_GIF: - $this->createDirectoryIfNotExists(dirname($filename)); - imagegif($image, $filename); - break; - case IMAGETYPE_GIF: - imagegif($image, $filename); - break; - default: - throw new Exception("Unsupported image type: " . $type); - } - } - - - public function importProducts($perPage = 100) - { - $page = 1; - $totalImported = 0; - $totalAvailable = 0; - - EcomZoneLogger::log("Starting product import - perPage: $perPage"); - - EcomZoneLogger::log("Number of products to be imported per page: $perPage", 'INFO'); - - try { - do { - $catalog = $this->client->getCatalog($page, $perPage); -$importedCount = 0; -foreach ($catalog['data'] as $product) { - if ($this->importSingleProduct($product)) { - $importedCount++; - } -} - -$totalImported += $importedCount; -$totalAvailable = $catalog['total']; -$page++; - -EcomZoneLogger::log("Imported page $page", 'INFO', [ - 'total_imported' => $totalImported, - 'page' => $page, - 'total_available' => $totalAvailable -]); - - } while ($page <= ceil($totalAvailable / $perPage) && isset($catalog['next_page_url']) && $catalog['next_page_url'] !== null); - - EcomZoneLogger::log("Finished importing products", 'INFO', [ - 'total_imported' => $totalImported, - 'total_available' => $totalAvailable - ]); - - // Clear cache - Tools::clearSmartyCache(); - Tools::clearXMLCache(); - Media::clearCache(); - PrestaShopAutoload::getInstance()->generateIndex(); - - return [ - 'success' => true, - 'imported' => $totalImported, - 'total' => $totalAvailable - ]; - - } catch (Exception $e) { - EcomZoneLogger::log("Error importing products: " . $e->getMessage(), 'ERROR'); - throw $e; - } - } - - private function importSingleProduct($productData) - { - // Extract data from nested structure - $data = isset($productData['data']) ? $productData['data'] : $productData; - - if (!isset($data['sku']) || !isset($data['product_name']) || !isset($data['description']) || !isset($data['product_price'])) { - EcomZoneLogger::log("Invalid product data", 'ERROR', ['data' => $productData]); - return false; - } - - try { - // Check if product already exists by reference - $productId = Db::getInstance()->getValue(' - SELECT id_product - FROM ' . _DB_PREFIX_ . 'product - WHERE reference = "' . pSQL($data['sku']) . '" - '); - - $product = $productId ? new Product($productId) : new Product(); - - $defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT'); - - $product->reference = $data['sku']; - $product->name[$defaultLangId] = $data['product_name']; - $product->description[$defaultLangId] = $data['long_description'] ?? $data['description']; - $product->description_short[$defaultLangId] = $data['description']; - $product->price = $data['product_price']; - $product->active = true; - $product->quantity = (int)$data['stock']; - $homeCategoryId = (int)Configuration::get('PS_HOME_CATEGORY'); - $product->id_category_default = $homeCategoryId; - $product->addToCategories([$homeCategoryId]); - $product->visibility = 'both'; // Ensure product is visible in both catalog and search - $product->active = true; - $product->quantity = (int)$data['stock']; - $homeCategoryId = (int)Configuration::get('PS_HOME_CATEGORY'); - $product->id_category_default = $homeCategoryId; - $product->addToCategories([$homeCategoryId]); - - - // Save product first to get ID - if (!$product->id) { - $product->add(); - } else { - $product->update(); + if (!is_dir($directory)) { + if (!mkdir($directory, 0755, true)) { + throw new Exception( + "Failed to create directory: " . $directory + ); } -StockAvailable::setQuantity($product->id, 0, (int)$data['stock']); -$product->available_for_order = true; // Ensure product is available for order -$product->show_price = true; // Ensure price is shown + } + } - // Handle image import if URL is provided - if (isset($data['image']) && !empty($data['image'])) { - $this->importProductImage($product, $data['image']); + private function updateProductFeatures(Product $product, string $specificationsJson): void + { + try { + $specifications = json_decode($specificationsJson, true); + if (!is_array($specifications)) { + return; } - - StockAvailable::setQuantity($product->id, 0, (int)$data['stock']); - - EcomZoneLogger::log("Imported product", 'INFO', [ - 'sku' => $data['sku'], - 'id' => $product->id, - 'name' => $data['product_name'] + + foreach ($specifications as $featureName => $featureValue) { + if (empty($featureValue)) { + continue; + } + + // Convert array or object to string if necessary + if (is_array($featureValue) || is_object($featureValue)) { + $featureValue = json_encode($featureValue); + } + + // Ensure we have a string value + $featureValue = (string)$featureValue; + + // Get or create feature using DB query + $featureId = $this->getOrCreateFeature($featureName); + if (!$featureId) { + continue; + } + + // Get or create feature value + $featureValueId = $this->getOrCreateFeatureValue($featureId, $featureValue); + if (!$featureValueId) { + continue; + } + + // Associate feature with product + $product->addFeaturesToDB( + [$featureId => $featureValueId], + $this->defaultLangId + ); + } + + EcomZoneLogger::log("Features updated successfully", "DEBUG", [ + "product" => $product->reference, + "specifications" => $specifications ]); - - return true; - + } catch (Exception $e) { - EcomZoneLogger::log("Error importing product", 'ERROR', [ - 'sku' => $data['sku'], - 'error' => $e->getMessage() - ]); - return false; + EcomZoneLogger::log( + "Failed to update product features", + "WARNING", + [ + "product" => $product->reference, + "error" => $e->getMessage(), + ] + ); } } + private function getOrCreateFeature(string $name): ?int + { + // Try to find existing feature + $featureId = (int)Db::getInstance()->getValue( + 'SELECT f.id_feature + FROM `'._DB_PREFIX_.'feature_lang` fl + JOIN `'._DB_PREFIX_.'feature` f ON f.id_feature = fl.id_feature + WHERE fl.name = "'.pSQL($name).'" + AND fl.id_lang = '.(int)$this->defaultLangId + ); - -private function importProductImage($product, $imageUrl) -{ - try { - // Create temporary file - $tmpFile = tempnam(_PS_TMP_IMG_DIR_, 'ecomzone_'); - - // Download image - if (!copy($imageUrl, $tmpFile)) { - throw new Exception("Failed to download image from: " . $imageUrl); - } - - // Get image info - $imageInfo = getimagesize($tmpFile); - if (!$imageInfo) { - unlink($tmpFile); - throw new Exception("Invalid image file"); - } - - // Validate image dimensions and file size - if ($imageInfo[0] > 2000 || $imageInfo[1] > 2000 || filesize($tmpFile) > 5000000) { - unlink($tmpFile); - throw new Exception("Image dimensions or file size exceed limits"); - } - - // Generate unique name - $imageName = $product->reference . '-' . time() . '.' . pathinfo($imageUrl, PATHINFO_EXTENSION); - - // Delete existing images if any - $product->deleteImages(); - - // Add new image - $image = new Image(); - $image->id_product = $product->id; - $image->position = 1; - $image->cover = true; - - // Save the image to the correct directory - $imagePath = _PS_PROD_IMG_DIR_ . $image->getImgPath() . '.' . $image->image_format; - $this->createDirectoryIfNotExists(dirname($imagePath)); - if (!copy($tmpFile, $imagePath)) { - unlink($tmpFile); - throw new Exception("Failed to save image to: " . $imagePath); + if ($featureId) { + return $featureId; } - // Associate the image with the product - if (!$image->add()) { - unlink($tmpFile); - throw new Exception("Failed to add image to product"); + // Create new feature + $feature = new Feature(); + $feature->name = array_fill_keys(Language::getIDs(), $name); + + return $feature->add() ? (int)$feature->id : null; + } + + private function getOrCreateFeatureValue(int $featureId, string $value): ?int + { + // Try to find existing feature value + $valueId = (int)Db::getInstance()->getValue( + 'SELECT fv.id_feature_value + FROM `'._DB_PREFIX_.'feature_value_lang` fvl + JOIN `'._DB_PREFIX_.'feature_value` fv ON fv.id_feature_value = fvl.id_feature_value + WHERE fv.id_feature = '.(int)$featureId.' + AND fvl.value = "'.pSQL($value).'" + AND fvl.id_lang = '.(int)$this->defaultLangId + ); + + if ($valueId) { + return $valueId; } - // Manually resize the image and generate thumbnails - $this->resizeImage($imagePath, _PS_PROD_IMG_DIR_ . $image->getImgPath() . '-home_default.' . $image->image_format, 250, 250); - $this->resizeImage($imagePath, _PS_PROD_IMG_DIR_ . $image->getImgPath() . '-large_default.' . $image->image_format, 800, 800); - - // Cleanup - unlink($tmpFile); - EcomZoneLogger::log("Imported product image", 'INFO', [ - 'sku' => $product->reference, - 'image' => $imageUrl - ]); + // Create new feature value + $featureValue = new FeatureValue(); + $featureValue->id_feature = $featureId; + $featureValue->value = array_fill_keys(Language::getIDs(), $value); - } catch (Exception $e) { - EcomZoneLogger::log("Error importing product image", 'ERROR', [ - 'sku' => $product->reference, - 'image' => $imageUrl, - 'error' => $e->getMessage() - ]); + return $featureValue->add() ? (int)$featureValue->id : null; + } + + private function updateProductDimensions( + Product $product, + string $dimensionsJson + ): void { + try { + $dimensions = json_decode($dimensionsJson, true); + if (!is_array($dimensions)) { + return; + } + + $product->weight = (float) ($dimensions["weight"] ?? 0); + $product->height = (float) ($dimensions["height"] ?? 0); + $product->width = (float) ($dimensions["width"] ?? 0); + $product->depth = (float) ($dimensions["length"] ?? 0); + + $product->update(); + } catch (Exception $e) { + EcomZoneLogger::log( + "Failed to update product dimensions", + "WARNING", + [ + "product" => $product->reference, + "error" => $e->getMessage(), + ] + ); + } + } + + private function clearCache(): void + { + Tools::clearAllCache(); + Tools::clearSmartyCache(); + Tools::clearXMLCache(); + Media::clearCache(); + PrestaShopAutoload::getInstance()->generateIndex(); } } -private function createDirectoryIfNotExists($directory) -{ - if (!is_dir($directory)) { - mkdir($directory, 0755, true); - } -} -} +$token = Configuration::get('ECOMZONE_CRON_TOKEN'); +echo $token; + diff --git a/ecomzone/classes/interfaces/ICategory.php b/ecomzone/classes/interfaces/ICategory.php new file mode 100644 index 0000000..f1d0d27 --- /dev/null +++ b/ecomzone/classes/interfaces/ICategory.php @@ -0,0 +1,11 @@ + + + ecomzone + + + + + + 1 + 0 + + diff --git a/ecomzone/cron.php b/ecomzone/cron.php index fc8a4fa..0285624 100644 --- a/ecomzone/cron.php +++ b/ecomzone/cron.php @@ -1,24 +1,50 @@ 'classes/EcomZoneException.php', + 'EcomZoneClient' => 'classes/EcomZoneClient.php', + 'EcomZoneLogger' => 'classes/EcomZoneLogger.php', + 'EcomZoneProductSync' => 'classes/EcomZoneProductSync.php', + 'EcomZoneProductImport' => 'classes/EcomZoneProductImport.php', + 'EcomZoneCategoryHandler' => 'classes/EcomZoneCategoryHandler.php', + 'IProductSync' => 'classes/interfaces/IProductSync.php', + 'ICategory' => 'classes/interfaces/ICategory.php' + ]; + + if (isset($classMap[$className])) { + require_once dirname(__FILE__) . '/' . $classMap[$className]; + } +}); + +// Function to send JSON response +function sendJsonResponse($data, $statusCode = 200) +{ + if (php_sapi_name() === 'cli') { + echo json_encode($data, JSON_PRETTY_PRINT) . "\n"; + } else { + if (!headers_sent()) { + http_response_code($statusCode); + header("Content-Type: application/json"); + } + echo json_encode($data); + } + exit($statusCode === 200 ? 0 : 1); } -// Security token check -$token = null; - +// Get token from either GET parameter or command line argument +$token = ''; if (php_sapi_name() === 'cli') { global $argv; foreach ($argv as $arg) { - if (preg_match('/^--token=(.+)$/', $arg, $matches)) { - $token = $matches[1]; + if (strpos($arg, 'token=') === 0) { + $token = substr($arg, 6); break; } } @@ -26,18 +52,72 @@ if (php_sapi_name() === 'cli') { $token = Tools::getValue('token'); } -$configToken = Configuration::get('ECOMZONE_CRON_TOKEN'); - -echo "Config token: " . $configToken . "\n"; -echo "Provided token: " . $token . "\n"; +// Security check +$configToken = Configuration::get("ECOMZONE_CRON_TOKEN"); if (empty($token) || $token !== $configToken) { - die('Invalid token. Provided: ' . $token . ', Expected: ' . $configToken); + sendJsonResponse([ + 'success' => false, + 'error' => 'Invalid token', + 'provided_token' => $token, + 'expected_token' => $configToken + ], 403); } try { - $result = $module->runCronTasks(); - echo json_encode(['success' => true, 'result' => $result]); + // Initialize logger + EcomZoneLogger::log("Starting cron execution", "INFO", [ + "time" => date("Y-m-d H:i:s"), + ]); + + // Check last run time to prevent too frequent execution + $lastRun = Configuration::get("ECOMZONE_LAST_CRON_RUN"); + $minInterval = 360; + + if (!empty($lastRun) && strtotime($lastRun) + $minInterval > time()) { + EcomZoneLogger::log("Skipping cron - too soon since last run", "INFO", [ + "last_run" => $lastRun, + "next_run" => date("Y-m-d H:i:s", strtotime($lastRun) + $minInterval), + ]); + + sendJsonResponse([ + "success" => false, + "message" => "Too soon since last run", + "last_run" => $lastRun, + "next_run" => date("Y-m-d H:i:s", strtotime($lastRun) + $minInterval), + ]); + } + + // Initialize Product Sync + $productSync = new EcomZoneProductSync(); + + // Start the import process + $result = $productSync->importProducts(100); // Import 100 products per page + + // Update last run time + Configuration::updateValue("ECOMZONE_LAST_CRON_RUN", date("Y-m-d H:i:s")); + + // Log completion and send response + EcomZoneLogger::log("Cron execution completed", "INFO", [ + "imported" => $result["imported"], + "total" => $result["total"], + "errors" => count($result["errors"]), + ]); + + sendJsonResponse([ + "success" => true, + "result" => $result, + "timestamp" => date("Y-m-d H:i:s"), + ]); } catch (Exception $e) { - echo json_encode(['success' => false, 'error' => $e->getMessage()]); -} \ No newline at end of file + EcomZoneLogger::log("Cron execution failed", "ERROR", [ + "error" => $e->getMessage(), + "trace" => $e->getTraceAsString() + ]); + + sendJsonResponse([ + "success" => false, + "error" => $e->getMessage() + ], 500); +} + diff --git a/ecomzone/ecomzone.php b/ecomzone/ecomzone.php index e67cb82..80afe84 100644 --- a/ecomzone/ecomzone.php +++ b/ecomzone/ecomzone.php @@ -1,219 +1,345 @@ name = 'ecomzone'; - $this->tab = 'market_place'; - $this->version = '1.0.0'; - $this->author = 'Your Name'; + $this->name = "ecomzone"; + $this->tab = "shipping_logistics"; + $this->version = "1.0.0"; + $this->author = "EcomZone"; $this->need_instance = 0; $this->bootstrap = true; + $this->ps_versions_compliancy = [ + "min" => "8.0.0", + "max" => "8.99.99", + ]; parent::__construct(); - // Register autoloader - spl_autoload_register([$this, 'autoload']); - - $this->displayName = $this->l('EcomZone Dropshipping'); - $this->description = $this->l('Integration with EcomZone Dropshipping API'); - } - - /** - * Autoload classes - */ - public function autoload($className) - { - // Only handle our module's classes - if (strpos($className, 'EcomZone') !== 0) { - return; - } - - $classPath = dirname(__FILE__) . '/classes/' . $className . '.php'; - if (file_exists($classPath)) { - require_once $classPath; - } + $this->displayName = $this->trans( + "EcomZone Dropshipping", + [], + "Modules.Ecomzone.Admin" + ); + $this->description = $this->trans( + "Integration with EcomZone Dropshipping API", + [], + "Modules.Ecomzone.Admin" + ); + $this->confirmUninstall = $this->trans( + "Are you sure you want to uninstall?", + [], + "Modules.Ecomzone.Admin" + ); } public function install() { - return parent::install() && - $this->registerHook('actionOrderStatusUpdate') && - Configuration::updateValue('ECOMZONE_API_TOKEN', '') && - Configuration::updateValue('ECOMZONE_API_URL', 'https://dropship.ecomzone.eu/api') && - Configuration::updateValue('ECOMZONE_LAST_SYNC', '') && - Configuration::updateValue('ECOMZONE_CRON_TOKEN', Tools::encrypt(uniqid())); + if (!parent::install() + || !$this->registerHook('actionCronJob') + || !$this->registerHook('actionProductUpdate') + || !$this->registerHook('actionProductDelete') + ) { + return false; + } + + // Set default configuration values + Configuration::updateValue('ECOMZONE_API_URL', 'https://dropship.ecomzone.eu/api'); + Configuration::updateValue('ECOMZONE_CRON_TOKEN', Tools::encrypt(uniqid())); + Configuration::updateValue('ECOMZONE_LAST_SYNC', ''); + + // Create required directories + $this->createRequiredDirectories(); + + return true; } - private function createLogFile() + private function createRequiredDirectories() { - if (!file_exists(dirname(EcomZoneLogger::LOG_FILE))) { - mkdir(dirname(EcomZoneLogger::LOG_FILE), 0755, true); + $dirs = [ + _PS_MODULE_DIR_ . $this->name . '/log', + _PS_MODULE_DIR_ . $this->name . '/tmp' + ]; + + foreach ($dirs as $dir) { + if (!file_exists($dir)) { + mkdir($dir, 0755, true); + } } - - if (!file_exists(EcomZoneLogger::LOG_FILE)) { - touch(EcomZoneLogger::LOG_FILE); - chmod(EcomZoneLogger::LOG_FILE, 0666); - } - - return true; } public function uninstall() { return parent::uninstall() && - Configuration::deleteByName('ECOMZONE_API_TOKEN') && - Configuration::deleteByName('ECOMZONE_API_URL') && - Configuration::deleteByName('ECOMZONE_CRON_TOKEN') && - Configuration::deleteByName('ECOMZONE_LAST_CRON_RUN'); + Configuration::deleteByName("ECOMZONE_API_URL") && + Configuration::deleteByName("ECOMZONE_API_TOKEN") && + Configuration::deleteByName("ECOMZONE_CRON_TOKEN") && + Configuration::deleteByName("ECOMZONE_LAST_SYNC"); } public function getContent() { - $output = ''; - - if (Tools::isSubmit('submitEcomZoneModule')) { - Configuration::updateValue('ECOMZONE_API_TOKEN', Tools::getValue('ECOMZONE_API_TOKEN')); - $output .= $this->displayConfirmation($this->l('Settings updated')); - } - - // Handle manual product import - if (Tools::isSubmit('importPoducts')) { - try { - $productSync = new EcomZoneProductSync(); - $result = $productSync->importProducts(); + $output = ""; + + if (Tools::isSubmit("submitEcomZoneModule")) { + $apiToken = Tools::getValue("ECOMZONE_API_TOKEN"); + if (Configuration::updateValue("ECOMZONE_API_TOKEN", $apiToken)) { $output .= $this->displayConfirmation( - sprintf($this->l('Imported %d products'), $result['imported']) + $this->trans( + "Settings updated", + [], + "Modules.Ecomzone.Admin" + ) ); + } + } + + // Handle product fetch request + if (Tools::isSubmit("fetchProducts") || Tools::getValue("page")) { + try { + $page = (int) Tools::getValue("page", 1); + $perPage = 10; + + $result = $this->makeApiRequest( + "catalog?page={$page}&per_page={$perPage}" + ); + + if (!empty($result["data"])) { + $totalPages = ceil($result["total"] / $perPage); + + $this->context->smarty->assign([ + "API_PRODUCTS" => $result["data"], + "PAGINATION" => [ + "total_pages" => $totalPages, + "current_page" => $page, + "total_items" => $result["total"], + ], + ]); + + if (Tools::isSubmit("fetchProducts")) { + $output .= $this->displayConfirmation( + $this->trans( + "Products fetched successfully", + [], + "Modules.Ecomzone.Admin" + ) + ); + } + } } catch (Exception $e) { $output .= $this->displayError($e->getMessage()); } } - - // Add debug info - $debugInfo = $this->getDebugInfo(); - - $shopUrl = Tools::getShopDomainSsl(true); - $shopRoot = _PS_ROOT_DIR_; $this->context->smarty->assign([ - 'ECOMZONE_API_TOKEN' => Configuration::get('ECOMZONE_API_TOKEN'), - 'ECOMZONE_CRON_TOKEN' => Configuration::get('ECOMZONE_CRON_TOKEN'), - 'ECOMZONE_DEBUG_INFO' => $debugInfo, - 'ECOMZONE_LOGS' => $this->getRecentLogs(), - 'shop_url' => $shopUrl, - 'shop_root' => $shopRoot + "module_dir" => $this->_path, + "ECOMZONE_API_TOKEN" => Configuration::get("ECOMZONE_API_TOKEN"), + "ECOMZONE_CRON_TOKEN" => Configuration::get("ECOMZONE_CRON_TOKEN"), + "ECOMZONE_LAST_SYNC" => Configuration::get("ECOMZONE_LAST_SYNC"), + "current_url" => + $this->context->link->getAdminLink("AdminModules", false) . + "&configure=" . + $this->name . + "&tab_module=" . + $this->tab . + "&module_name=" . + $this->name, + "shop_url" => $this->context->link->getBaseLink(), ]); - - return $output . $this->display(__FILE__, 'views/templates/admin/configure.tpl'); + + return $output . + $this->display(__FILE__, "views/templates/admin/configure.tpl"); } - private function getDebugInfo() + private function makeApiRequest($endpoint, $method = "GET", $data = null) { - $lastCronRun = Configuration::get('ECOMZONE_LAST_CRON_RUN'); - $nextRun = !empty($lastCronRun) ? - date('Y-m-d H:i:s', strtotime($lastCronRun) + 3600) : - $this->l('Not scheduled yet'); + $apiUrl = Configuration::get("ECOMZONE_API_URL"); + $apiToken = Configuration::get("ECOMZONE_API_TOKEN"); - return [ - 'php_version' => PHP_VERSION, - 'prestashop_version' => _PS_VERSION_, - 'module_version' => $this->version, - 'curl_enabled' => function_exists('curl_version'), - 'api_url' => Configuration::get('ECOMZONE_API_URL'), - 'last_sync' => Configuration::get('ECOMZONE_LAST_SYNC'), - 'last_cron_run' => $lastCronRun ?: $this->l('Never'), - 'next_cron_run' => $nextRun, - 'log_file' => EcomZoneLogger::LOG_FILE, - 'log_file_exists' => file_exists(EcomZoneLogger::LOG_FILE), - 'log_file_writable' => is_writable(EcomZoneLogger::LOG_FILE) + if (empty($apiToken)) { + throw new Exception( + $this->trans( + "API token not configured", + [], + "Modules.Ecomzone.Admin" + ) + ); + } + + $curl = curl_init(); + $url = rtrim($apiUrl, "/") . "/" . ltrim($endpoint, "/"); + + $options = [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => [ + "Authorization: Bearer " . $apiToken, + "Accept: application/json", + "Content-Type: application/json", + ], + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, ]; - } - private function getRecentLogs($lines = 50) - { - if (!file_exists(EcomZoneLogger::LOG_FILE)) { - return []; + if ($data !== null) { + $options[CURLOPT_POSTFIELDS] = json_encode($data); } - - $logs = array_slice(file(EcomZoneLogger::LOG_FILE), -$lines); - return array_map('trim', $logs); - } - public function hookActionOrderStatusUpdate($params) - { - $order = $params['order']; - $newOrderStatus = $params['newOrderStatus']; + curl_setopt_array($curl, $options); - // Sync order when it's paid - if ($newOrderStatus->paid == 1) { - try { - $orderSync = new EcomZoneOrderSync(); - $result = $orderSync->syncOrder($order->id); - - // Log the result - PrestaShopLogger::addLog( - 'EcomZone order sync: ' . json_encode($result), - 1, - null, - 'Order', - $order->id, - true - ); - } catch (Exception $e) { - PrestaShopLogger::addLog( - 'EcomZone order sync error: ' . $e->getMessage(), - 3, - null, - 'Order', - $order->id, - true - ); - } + $response = curl_exec($curl); + $err = curl_error($curl); + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + curl_close($curl); + + if ($err) { + throw new Exception("cURL Error: " . $err); } + + if ($httpCode >= 400) { + $errorData = json_decode($response, true); + $errorMessage = isset($errorData['error']) + ? $errorData['error'] + : "HTTP Error: " . $httpCode; + throw new EcomZoneException( + $errorMessage, + EcomZoneException::API_ERROR + ); + } + + $decodedResponse = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception("Invalid JSON response"); + } + + return $decodedResponse; } public function hookActionCronJob($params) { - $cronTask = new EcomZoneCronTask(); - - // Check if it's time to run - $lastRun = Configuration::get('ECOMZONE_LAST_CRON_RUN'); - if (empty($lastRun) || (strtotime($lastRun) + $cronTask->cron_frequency) <= time()) { - return $cronTask->run(); + try { + $lastSync = Configuration::get("ECOMZONE_LAST_SYNC"); + $syncInterval = 24 * 3600; // 24 hours + + if ( + !empty($lastSync) && + strtotime($lastSync) + $syncInterval > time() + ) { + return; + } + + $page = 1; + $perPage = 100; + $totalImported = 0; + $errors = []; + + do { + $result = $this->makeApiRequest( + "catalog?page={$page}&per_page={$perPage}" + ); + + if (empty($result["data"])) { + break; + } + + foreach ($result["data"] as $product) { + try { + if ($this->importSingleProduct($product)) { + $totalImported++; + } + } catch (Exception $e) { + $errors[] = [ + "sku" => $product["sku"] ?? "unknown", + "error" => $e->getMessage() + ]; + } + // Clear memory after each product + gc_collect_cycles(); + } + + $page++; + } while (!empty($result["next_page_url"])); + + Configuration::updateValue( + "ECOMZONE_LAST_SYNC", + date("Y-m-d H:i:s") + ); + } catch (Exception $e) { + EcomZoneLogger::log("Cron job execution failed", "ERROR", [ + "error" => $e->getMessage(), + "trace" => $e->getTraceAsString() + ]); } - - return true; } - public function runCronTasks() + public function importSingleProduct(array $productData): bool { try { - EcomZoneLogger::log('Starting scheduled product sync'); - - $lastRun = Configuration::get('ECOMZONE_LAST_CRON_RUN'); - $frequency = 3600; // 1 hour in seconds - - if (!empty($lastRun) && (strtotime($lastRun) + $frequency) > time()) { - EcomZoneLogger::log('Skipping cron - too soon since last run'); - return false; - } - - $productSync = new EcomZoneProductSync(); - $result = $productSync->importProducts(); - - Configuration::updateValue('ECOMZONE_LAST_CRON_RUN', date('Y-m-d H:i:s')); - EcomZoneLogger::log('Scheduled product sync completed', 'INFO', $result); - - return $result; + $importer = new EcomZoneProductImport(); + return $importer->importProduct($productData); } catch (Exception $e) { - EcomZoneLogger::log('Scheduled product sync failed', 'ERROR', ['error' => $e->getMessage()]); - throw $e; + EcomZoneLogger::log("Product import failed", "ERROR", [ + "sku" => $productData['data']['sku'] ?? 'unknown', + "error" => $e->getMessage() + ]); + return false; } } -} + + private function importProductImage( + Product $product, + string $imageUrl, + bool $cover = false + ): void { + // Implementation of the method + } + + private function updateBasicInfo(Product $product, array $data): void + { + try { + // Basic information + $product->name[$this->defaultLangId] = $data["product_name"]; + $product->description[$this->defaultLangId] = $data["long_description"] ?? $data["description"]; + $product->description_short[$this->defaultLangId] = $data["description"]; + $product->price = (float) $data["product_price"]; + $product->reference = $data["sku"]; + $product->ean13 = $data["ean"] ?? ""; + + // Shop configuration + $product->id_shop_default = $this->defaultShopId; + $product->active = true; + $product->visibility = "both"; + $product->available_for_order = true; + $product->show_price = true; + $product->indexed = 1; + + // Critical: Save the product + if (!$product->save()) { + throw new EcomZoneException( + "Failed to save product", + EcomZoneException::INVALID_PRODUCT_DATA + ); + } + + // Add to default shop + $product->addToShop($this->defaultShopId); + } catch (Exception $e) { + throw new EcomZoneException( + "Failed to update product info: " . $e->getMessage(), + EcomZoneException::INVALID_PRODUCT_DATA, + $e + ); + } + } +} + diff --git a/ecomzone/index.php b/ecomzone/index.php new file mode 100644 index 0000000..c244971 --- /dev/null +++ b/ecomzone/index.php @@ -0,0 +1,8 @@ + -
-
- {l s='EcomZone Configuration' mod='ecomzone'} -
-
-
- -
- -
+ +{* views/templates/admin/configure.tpl *} + +
+

{l s='EcomZone Configuration' mod='ecomzone'}

+
+
+ +
+
+ +
+ +
+

+ {if $ECOMZONE_LAST_SYNC} + {$ECOMZONE_LAST_SYNC|escape:'html':'UTF-8'} + {else} + {l s='Never' mod='ecomzone'} + {/if} +

+
+
+ -
- + +
-
- {l s='Manual Actions' mod='ecomzone'} -
-
+

{l s='Product Import' mod='ecomzone'}

+
-
-
-
-
-
- {l s='Cron Setup Instructions' mod='ecomzone'} -
-
-

{l s='Add the following command to your server crontab to run every hour:' mod='ecomzone'}

-
0 * * * * curl -s "{$shop_url}modules/ecomzone/cron.php?token={$ECOMZONE_CRON_TOKEN}"
-

{l s='Or using PHP CLI:' mod='ecomzone'}

-
0 * * * * php {$shop_root}modules/ecomzone/cron.php --token={$ECOMZONE_CRON_TOKEN}
-
-
- -
-
- {l s='Debug Information' mod='ecomzone'} -
-
-
- - - {foreach from=$ECOMZONE_DEBUG_INFO key=key item=value} + {if isset($API_PRODUCTS) && $API_PRODUCTS} +
+
+ - - + + + + + - {/foreach} - - - - - - - - - -
{$key|escape:'html':'UTF-8'}{$value|escape:'html':'UTF-8'}{l s='SKU' mod='ecomzone'}{l s='Name' mod='ecomzone'}{l s='Price' mod='ecomzone'}{l s='Stock' mod='ecomzone'}{l s='Actions' mod='ecomzone'}
{l s='Last Cron Run' mod='ecomzone'}{$ECOMZONE_LAST_CRON_RUN|escape:'html':'UTF-8'}
{l s='Next Scheduled Run' mod='ecomzone'}{$ECOMZONE_NEXT_CRON_RUN|escape:'html':'UTF-8'}
-
+ + + {foreach from=$API_PRODUCTS item=product} + + {$product.sku|escape:'html':'UTF-8'} + {$product.product_name|escape:'html':'UTF-8'} + {$product.product_price|escape:'html':'UTF-8'} + {$product.stock|escape:'html':'UTF-8'} + +
+ + +
+ + + {/foreach} + + +
+ + {if isset($PAGINATION)} +
+
+
    + {if $PAGINATION.current_page > 1} +
  • + + + +
  • +
  • + + + +
  • + {/if} + + {assign var=p value=$PAGINATION.current_page-2} + {if $p < 1} + {assign var=p value=1} + {/if} + {assign var=stop value=$p+4} + {if $stop > $PAGINATION.total_pages} + {assign var=stop value=$PAGINATION.total_pages} + {assign var=p value=$stop-4} + {if $p < 1} + {assign var=p value=1} + {/if} + {/if} + + {while $p <= $stop} +
  • + {$p} +
  • + {assign var=p value=$p+1} + {/while} + + {if $PAGINATION.current_page < $PAGINATION.total_pages} +
  • + + + +
  • +
  • + + + +
  • + {/if} +
+
+
+ {/if} + {/if}
-
- {l s='Recent Logs' mod='ecomzone'} -
-
-
- {foreach from=$ECOMZONE_LOGS item=log} -
{$log|escape:'html':'UTF-8'}
- {/foreach} +

{l s='Automatic Sync' mod='ecomzone'}

+
+
+
+ +
+
+ + + + +
+

+ {l s='Add this URL to your server\'s crontab to run every 24 hours.' mod='ecomzone'} +

+
+
-
\ No newline at end of file +
+ +{if isset($ECOMZONE_LOGS) && $ECOMZONE_LOGS} +
+

{l s='Recent Logs' mod='ecomzone'}

+
+
+ + + {foreach from=$ECOMZONE_LOGS item=log} + + + + {/foreach} + +
{$log|escape:'html':'UTF-8'}
+
+
+
+{/if} + + + + diff --git a/ecomzone/views/templates/admin/index.php b/ecomzone/views/templates/admin/index.php new file mode 100644 index 0000000..c244971 --- /dev/null +++ b/ecomzone/views/templates/admin/index.php @@ -0,0 +1,8 @@ +