diff --git a/ecomzone-v3.1.zip b/ecomzone-v3.1.zip new file mode 100644 index 0000000..fd616ef Binary files /dev/null and b/ecomzone-v3.1.zip differ diff --git a/ecomzone-v5.1.zip b/ecomzone-v5.1.zip new file mode 100644 index 0000000..a3e0d61 Binary files /dev/null and b/ecomzone-v5.1.zip differ diff --git a/ecomzone-v5.zip b/ecomzone-v5.zip new file mode 100644 index 0000000..7f63e14 Binary files /dev/null and b/ecomzone-v5.zip differ diff --git a/ecomzone/classes/EcomZoneClient.php b/ecomzone/classes/EcomZoneClient.php new file mode 100644 index 0000000..fe3d14f --- /dev/null +++ b/ecomzone/classes/EcomZoneClient.php @@ -0,0 +1,107 @@ +apiToken = 'klRyAdrXaxL0s6PEUp7LDlH6T8aPSCtBY8NiEHsHiWpc6646K2TZPi5KMxUg'; + $this->apiUrl = Configuration::get('ECOMZONE_API_URL'); + + if (empty($this->apiUrl)) { + throw new Exception('API URL not configured'); + } + } + + 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); + } + + public function getProduct($sku) + { + return $this->makeRequest('GET', $this->apiUrl . '/product/' . $sku); + } + + 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) + { + $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); + } +} diff --git a/ecomzone/classes/EcomZoneCronTask.php b/ecomzone/classes/EcomZoneCronTask.php new file mode 100644 index 0000000..1efff7a --- /dev/null +++ b/ecomzone/classes/EcomZoneCronTask.php @@ -0,0 +1,40 @@ +name = 'ecomzone_cron'; + $this->title = $this->l('EcomZone Product Sync'); + $this->cron_frequency = 3600 * 2; // Run every hour + } + + 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 = $productSync->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/EcomZoneLogger.php b/ecomzone/classes/EcomZoneLogger.php new file mode 100644 index 0000000..67e6db9 --- /dev/null +++ b/ecomzone/classes/EcomZoneLogger.php @@ -0,0 +1,25 @@ +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/EcomZoneProductSync.php b/ecomzone/classes/EcomZoneProductSync.php new file mode 100644 index 0000000..4ce10f6 --- /dev/null +++ b/ecomzone/classes/EcomZoneProductSync.php @@ -0,0 +1,270 @@ +client = new EcomZoneClient(); + } + + private function resizeImage($src, $dest, $width, $height) + { + 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); + } + + private function createImageFromType($filename, $type) + { + switch ($type) { + case IMAGETYPE_JPEG: + return imagecreatefromjpeg($filename); + case IMAGETYPE_PNG: + return imagecreatefrompng($filename); + case IMAGETYPE_GIF: + return imagecreatefromgif($filename); + default: + throw new Exception("Unsupported image type: " . $type); + } + } + + private function saveImageFromType($image, $filename, $type) + { + 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(); + } +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']); + } + + StockAvailable::setQuantity($product->id, 0, (int)$data['stock']); + + EcomZoneLogger::log("Imported product", 'INFO', [ + 'sku' => $data['sku'], + 'id' => $product->id, + 'name' => $data['product_name'] + ]); + + return true; + + } catch (Exception $e) { + EcomZoneLogger::log("Error importing product", 'ERROR', [ + 'sku' => $data['sku'], + 'error' => $e->getMessage() + ]); + return false; + } + } + + + +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); + } + + // Associate the image with the product + if (!$image->add()) { + unlink($tmpFile); + throw new Exception("Failed to add image to product"); + } + + // 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 + ]); + + } catch (Exception $e) { + EcomZoneLogger::log("Error importing product image", 'ERROR', [ + 'sku' => $product->reference, + 'image' => $imageUrl, + 'error' => $e->getMessage() + ]); + } +} + +private function createDirectoryIfNotExists($directory) +{ + if (!is_dir($directory)) { + mkdir($directory, 0755, true); + } +} +} diff --git a/ecomzone/cron.php b/ecomzone/cron.php new file mode 100644 index 0000000..fc8a4fa --- /dev/null +++ b/ecomzone/cron.php @@ -0,0 +1,43 @@ +runCronTasks(); + echo json_encode(['success' => true, 'result' => $result]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} \ No newline at end of file diff --git a/ecomzone/ecomzone.php b/ecomzone/ecomzone.php new file mode 100644 index 0000000..e67cb82 --- /dev/null +++ b/ecomzone/ecomzone.php @@ -0,0 +1,219 @@ +name = 'ecomzone'; + $this->tab = 'market_place'; + $this->version = '1.0.0'; + $this->author = 'Your Name'; + $this->need_instance = 0; + $this->bootstrap = true; + + 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; + } + } + + 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())); + } + + private function createLogFile() + { + if (!file_exists(dirname(EcomZoneLogger::LOG_FILE))) { + mkdir(dirname(EcomZoneLogger::LOG_FILE), 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'); + } + + 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 .= $this->displayConfirmation( + sprintf($this->l('Imported %d products'), $result['imported']) + ); + } 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 + ]); + + return $output . $this->display(__FILE__, 'views/templates/admin/configure.tpl'); + } + + private function getDebugInfo() + { + $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'); + + 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) + ]; + } + + private function getRecentLogs($lines = 50) + { + if (!file_exists(EcomZoneLogger::LOG_FILE)) { + return []; + } + + $logs = array_slice(file(EcomZoneLogger::LOG_FILE), -$lines); + return array_map('trim', $logs); + } + + public function hookActionOrderStatusUpdate($params) + { + $order = $params['order']; + $newOrderStatus = $params['newOrderStatus']; + + // 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 + ); + } + } + } + + 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(); + } + + return true; + } + + public function runCronTasks() + { + 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; + } catch (Exception $e) { + EcomZoneLogger::log('Scheduled product sync failed', 'ERROR', ['error' => $e->getMessage()]); + throw $e; + } + } +} diff --git a/ecomzone/views/templates/admin/configure.tpl b/ecomzone/views/templates/admin/configure.tpl new file mode 100644 index 0000000..fc907eb --- /dev/null +++ b/ecomzone/views/templates/admin/configure.tpl @@ -0,0 +1,90 @@ +
+
+
+ {l s='EcomZone Configuration' mod='ecomzone'} +
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+ {l s='Manual Actions' 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} + + + + + {/foreach} + + + + + + + + + +
{$key|escape:'html':'UTF-8'}{$value|escape:'html':'UTF-8'}
{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'}
+
+
+
+ +
+
+ {l s='Recent Logs' mod='ecomzone'} +
+
+
+ {foreach from=$ECOMZONE_LOGS item=log} +
{$log|escape:'html':'UTF-8'}
+ {/foreach} +
+
+
\ No newline at end of file