category -> product association implemented

This commit is contained in:
echo committed 2025-02-16 20:02:41 +01:00
1 parent 0b8c5de8b0
commit c8bcfe6bd6
41 files changed
+2153 -634

No files matched your search

+33
View File
@@ -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
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+53
View File
@@ -0,0 +1,53 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneApiTest
{
private $client;
public function __construct()
{
$this->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;
}
}
}
@@ -0,0 +1,215 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneCategoryHandler implements ICategory
{
private int $defaultLangId;
private array $categoryCache = [];
private array $categoryLinkRewriteCache = [];
private int $defaultShopId;
public function __construct()
{
$this->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;
}
}
+101 -56
View File
@@ -1,67 +1,92 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneClient
{
private $apiToken;
private $apiUrl;
private $maxRetries = 3;
private $timeout = 30;
public function __construct()
{
// Hardcoded API token for testing
$this->apiToken = 'klRyAdrXaxL0s6PEUp7LDlH6T8aPSCtBY8NiEHsHiWpc6646K2TZPi5KMxUg';
$this->apiUrl = Configuration::get('ECOMZONE_API_URL');
$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 Exception('API URL not configured');
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;
$url = $this->apiUrl . "/catalog";
$params = [
"page" => $page,
"per_page" => $perPage,
];
EcomZoneLogger::log("Making API request", 'INFO', ['url' => $url]);
return $this->makeRequest('GET', $url);
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',
];
$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_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_SSL_VERIFYPEER => false, // Temporarily disable SSL verification for testing
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $this->apiToken,
"Accept: application/json",
"Content-Type: application/json",
],
];
if ($data !== null) {
@@ -69,39 +94,59 @@ class EcomZoneClient
}
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
]);
$error = curl_error($curl);
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);
if ($error) {
throw new Exception("cURL Error: " . $error);
}
} else if ($httpCode >= 400) {
throw new Exception('API Error: HTTP ' . $httpCode . ' - ' . $response);
} else {
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: ' . json_last_error_msg());
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;
}
} while ($retryCount < $maxRetries);
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
);
}
}
-40
View File
@@ -1,40 +0,0 @@
<?php
class EcomZoneCronTask extends ModuleGrid
{
public function __construct()
{
parent::__construct();
$this->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;
}
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneException extends Exception
{
public const INVALID_PRODUCT_DATA = 1001;
public const CATEGORY_CREATE_ERROR = 1002;
public const IMAGE_PROCESSING_ERROR = 1003;
public const API_ERROR = 1004;
public const VALIDATION_ERROR = 1005;
}
+56 -11
View File
@@ -1,25 +1,70 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneLogger
{
const LOG_FILE = _PS_ROOT_DIR_ . '/var/logs/ecomzone.log';
private static $logFile = 'ecomzone.log';
private static $maxLogSize = 10485760; // 10MB
public static function log($message, $level = 'INFO', $context = [])
public static function log($message, $level = "INFO", $context = [])
{
$date = date('Y-m-d H:i:s');
try {
$logDir = _PS_MODULE_DIR_ . 'ecomzone/log/';
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$contextStr = !empty($context) ? json_encode($context) : '';
$logMessage = "[$date] [$level] $message $contextStr\n";
file_put_contents(self::LOG_FILE, $logMessage, FILE_APPEND);
$logMessage = sprintf(
"[%s] %s: %s %s\n",
$timestamp,
$level,
$message,
$contextStr
);
// Also log to PrestaShop
file_put_contents(
$logDir . self::$logFile,
$logMessage,
FILE_APPEND
);
} catch (Exception $e) {
// Fallback to PrestaShop's logger
PrestaShopLogger::addLog(
"EcomZone: $message",
($level === 'ERROR' ? 3 : 1),
null,
'EcomZone',
null,
true
($level === 'ERROR' ? 3 : 1)
);
}
}
private static function initLogFile()
{
if (!file_exists(self::$logFile)) {
$logDir = dirname(self::$logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
touch(self::$logFile);
chmod(self::$logFile, 0666);
}
}
private static function rotateLogIfNeeded()
{
if (!file_exists(self::$logFile)) {
return;
}
if (filesize(self::$logFile) > self::$maxLogSize) {
$backup = self::$logFile . "." . date("Y-m-d-H-i-s") . ".bak";
rename(self::$logFile, $backup);
touch(self::$logFile);
chmod(self::$logFile, 0666);
}
}
}
-81
View File
@@ -1,81 +0,0 @@
<?php
class EcomZoneOrderSync
{
private $client;
public function __construct()
{
$this->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;
}
}
+345
View File
@@ -0,0 +1,345 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneProductImport
{
private $module;
private $defaultLangId;
private $defaultShopId;
public function __construct()
{
$this->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
);
}
}
}
+672 -220
View File
@@ -1,29 +1,558 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneProductSync
class EcomZoneProductSync implements IProductSync
{
private $client;
private EcomZoneClient $client;
private EcomZoneCategoryHandler $categoryHandler;
private int $defaultLangId;
private int $defaultShopId;
public function __construct()
{
$this->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);
$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;
}
}
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);
imagecopyresampled($destImage, $srcImage, 0, 0, 0, 0, $width, $height, $srcWidth, $srcHeight);
$this->saveImageFromType($destImage, $dest, $type);
// 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($filename, $type)
private function createImageFromType(string $filename, int $type)
{
switch ($type) {
case IMAGETYPE_JPEG:
@@ -37,235 +566,158 @@ class EcomZoneProductSync
}
}
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);
if (!is_dir($directory)) {
if (!mkdir($directory, 0755, true)) {
throw new Exception(
"Failed to create directory: " . $directory
);
}
}
}
public function importProducts($perPage = 100)
private function updateProductFeatures(Product $product, string $specificationsJson): void
{
$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++;
$specifications = json_decode($specificationsJson, true);
if (!is_array($specifications)) {
return;
}
}
$totalImported += $importedCount;
$totalAvailable = $catalog['total'];
$page++;
foreach ($specifications as $featureName => $featureValue) {
if (empty($featureValue)) {
continue;
}
EcomZoneLogger::log("Imported page $page", 'INFO', [
'total_imported' => $totalImported,
'page' => $page,
'total_available' => $totalAvailable
]);
// Convert array or object to string if necessary
if (is_array($featureValue) || is_object($featureValue)) {
$featureValue = json_encode($featureValue);
}
} while ($page <= ceil($totalAvailable / $perPage) && isset($catalog['next_page_url']) && $catalog['next_page_url'] !== null);
// Ensure we have a string value
$featureValue = (string)$featureValue;
EcomZoneLogger::log("Finished importing products", 'INFO', [
'total_imported' => $totalImported,
'total_available' => $totalAvailable
// 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
]);
// Clear cache
} catch (Exception $e) {
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
);
if ($featureId) {
return $featureId;
}
// 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;
}
// Create new feature value
$featureValue = new FeatureValue();
$featureValue->id_feature = $featureId;
$featureValue->value = array_fill_keys(Language::getIDs(), $value);
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();
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);
}
}
}
$token = Configuration::get('ECOMZONE_CRON_TOKEN');
echo $token;
+11
View File
@@ -0,0 +1,11 @@
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
interface ICategory
{
public function processCategoriesFromString(string $categories): array;
public function createCategory(string $name, ?int $parentId = null): int;
public function getCategoryIdByName(string $name, ?int $parentId = null): ?int;
}
@@ -0,0 +1,10 @@
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
interface IProductSync
{
public function importProducts(int $perPage = 100): array;
public function importSingleProduct(array $productData): bool;
}
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ecomzone</name>
<displayName><![CDATA[EcomZone Dropshipping]]></displayName>
<version><![CDATA[1.0.0]]></version>
<description><![CDATA[Integration with EcomZone Dropshipping API]]></description>
<author><![CDATA[EcomZone]]></author>
<tab><![CDATA[market_place]]></tab>
<is_configurable>1</is_configurable>
<need_instance>0</need_instance>
<limited_countries></limited_countries>
</module>
+101 -21
View File
@@ -1,24 +1,50 @@
<?php
include(dirname(__FILE__) . '/../../config/config.inc.php');
include(dirname(__FILE__) . '/../../init.php');
ob_start(); // Start output buffering
// Include the module class
require_once dirname(__FILE__) . '/ecomzone.php';
// Include PrestaShop configuration
require_once dirname(__FILE__) . "/../../config/config.inc.php";
require_once dirname(__FILE__) . "/../../init.php";
// Ensure module is loaded
$module = Module::getInstanceByName('ecomzone');
if (!$module) {
die('Module not found');
// Autoloader for module classes
spl_autoload_register(function ($className) {
$classMap = [
'EcomZoneException' => '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()]);
EcomZoneLogger::log("Cron execution failed", "ERROR", [
"error" => $e->getMessage(),
"trace" => $e->getTraceAsString()
]);
sendJsonResponse([
"success" => false,
"error" => $e->getMessage()
], 500);
}
+272 -146
View File
@@ -1,219 +1,345 @@
<?php
if (!defined('_PS_VERSION_')) {
exit;
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZone extends Module
class Ecomzone extends Module
{
public function __construct()
{
$this->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', '') &&
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', '');
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);
}
// Create required directories
$this->createRequiredDirectories();
return true;
}
private function createRequiredDirectories()
{
$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);
}
}
}
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 = '';
$output = "";
if (Tools::isSubmit('submitEcomZoneModule')) {
Configuration::updateValue('ECOMZONE_API_TOKEN', Tools::getValue('ECOMZONE_API_TOKEN'));
$output .= $this->displayConfirmation($this->l('Settings updated'));
if (Tools::isSubmit("submitEcomZoneModule")) {
$apiToken = Tools::getValue("ECOMZONE_API_TOKEN");
if (Configuration::updateValue("ECOMZONE_API_TOKEN", $apiToken)) {
$output .= $this->displayConfirmation(
$this->trans(
"Settings updated",
[],
"Modules.Ecomzone.Admin"
)
);
}
}
// Handle manual product import
if (Tools::isSubmit('importP<Down>oducts')) {
// Handle product fetch request
if (Tools::isSubmit("fetchProducts") || Tools::getValue("page")) {
try {
$productSync = new EcomZoneProductSync();
$result = $productSync->importProducts();
$output .= $this->displayConfirmation(
sprintf($this->l('Imported %d products'), $result['imported'])
$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,
];
if ($data !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($data);
}
private function getRecentLogs($lines = 50)
{
if (!file_exists(EcomZoneLogger::LOG_FILE)) {
return [];
curl_setopt_array($curl, $options);
$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);
}
$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
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();
try {
$lastSync = Configuration::get("ECOMZONE_LAST_SYNC");
$syncInterval = 24 * 3600; // 24 hours
// 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();
if (
!empty($lastSync) &&
strtotime($lastSync) + $syncInterval > time()
) {
return;
}
return true;
$page = 1;
$perPage = 100;
$totalImported = 0;
$errors = [];
do {
$result = $this->makeApiRequest(
"catalog?page={$page}&per_page={$perPage}"
);
if (empty($result["data"])) {
break;
}
public function runCronTasks()
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()
]);
}
}
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');
$importer = new EcomZoneProductImport();
return $importer->importProduct($productData);
} catch (Exception $e) {
EcomZoneLogger::log("Product import failed", "ERROR", [
"sku" => $productData['data']['sku'] ?? 'unknown',
"error" => $e->getMessage()
]);
return false;
}
}
$productSync = new EcomZoneProductSync();
$result = $productSync->importProducts();
private function importProductImage(
Product $product,
string $imageUrl,
bool $cover = false
): void {
// Implementation of the method
}
Configuration::updateValue('ECOMZONE_LAST_CRON_RUN', date('Y-m-d H:i:s'));
EcomZoneLogger::log('Scheduled product sync completed', 'INFO', $result);
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"] ?? "";
return $result;
// 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) {
EcomZoneLogger::log('Scheduled product sync failed', 'ERROR', ['error' => $e->getMessage()]);
throw $e;
throw new EcomZoneException(
"Failed to update product info: " . $e->getMessage(),
EcomZoneException::INVALID_PRODUCT_DATA,
$e
);
}
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit();
+11
View File
@@ -0,0 +1,11 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
interface ICategory
{
public function createCategory(string $name, ?int $parentId = null): int;
public function getCategoryIdByName(string $name): ?int;
public function processCategoriesFromString(string $categoriesJson): array;
}
+12
View File
@@ -0,0 +1,12 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
interface IProductSync
{
public function importProducts(int $perPage = 100): array;
public function importSingleProduct(array $productData): bool;
}
+184 -59
View File
@@ -1,90 +1,215 @@
<form method="post" action="{$current|escape:'html':'UTF-8'}&token={$token|escape:'html':'UTF-8'}">
<div class="panel">
<div class="panel-heading">
<i class="icon-cogs"></i> {l s='EcomZone Configuration' mod='ecomzone'}
</div>
<div class="form-wrapper">
{* views/templates/admin/configure.tpl *}
<div class="panel">
<h3><i class="icon icon-cogs"></i> {l s='EcomZone Configuration' mod='ecomzone'}</h3>
<form method="post" class="form-horizontal">
<div class="form-group">
<label class="control-label col-lg-3">{l s='API Token' mod='ecomzone'}</label>
<div class="col-lg-9">
<input type="text" name="ECOMZONE_API_TOKEN" value="{$ECOMZONE_API_TOKEN}" />
<div class="col-lg-6">
<input type="text" name="ECOMZONE_API_TOKEN"
value="{$ECOMZONE_API_TOKEN|escape:'html':'UTF-8'}"
class="form-control" />
</div>
</div>
<div class="form-group">
<label class="control-label col-lg-3">{l s='Last Sync' mod='ecomzone'}</label>
<div class="col-lg-6">
<p class="form-control-static">
{if $ECOMZONE_LAST_SYNC}
{$ECOMZONE_LAST_SYNC|escape:'html':'UTF-8'}
{else}
{l s='Never' mod='ecomzone'}
{/if}
</p>
</div>
</div>
<div class="panel-footer">
<button type="submit" name="submitEcomZoneModule" class="btn btn-default pull-right">
<i class="process-icon-save"></i> {l s='Save' mod='ecomzone'}
</button>
</div>
</div>
</form>
</form>
</div>
<div class="panel">
<div class="panel-heading">
<i class="icon-cogs"></i> {l s='Manual Actions' mod='ecomzone'}
</div>
<div class="form-wrapper">
<h3><i class="icon icon-cloud-download"></i> {l s='Product Import' mod='ecomzone'}</h3>
<div class="panel-body">
<form method="post" class="form-horizontal">
<div class="form-group">
<div class="col-lg-9 col-lg-offset-3">
<button type="submit" name="importProducts" class="btn btn-default">
<i class="process-icon-download"></i> {l s='Import Products' mod='ecomzone'}
<button type="submit" name="fetchProducts" class="btn btn-primary">
<i class="process-icon-download"></i> {l s='Fetch Products' mod='ecomzone'}
</button>
</div>
</div>
</form>
</div>
</div>
<div class="panel">
<div class="panel-heading">
<i class="icon-clock"></i> {l s='Cron Setup Instructions' mod='ecomzone'}
</div>
<div class="form-wrapper">
<p>{l s='Add the following command to your server crontab to run every hour:' mod='ecomzone'}</p>
<pre>0 * * * * curl -s "{$shop_url}modules/ecomzone/cron.php?token={$ECOMZONE_CRON_TOKEN}"</pre>
<p>{l s='Or using PHP CLI:' mod='ecomzone'}</p>
<pre>0 * * * * php {$shop_root}modules/ecomzone/cron.php --token={$ECOMZONE_CRON_TOKEN}</pre>
</div>
</div>
<div class="panel">
<div class="panel-heading">
<i class="icon-info"></i> {l s='Debug Information' mod='ecomzone'}
</div>
<div class="form-wrapper">
{if isset($API_PRODUCTS) && $API_PRODUCTS}
<div class="table-responsive">
<table class="table">
<tbody>
{foreach from=$ECOMZONE_DEBUG_INFO key=key item=value}
<table class="table table-striped">
<thead>
<tr>
<td><strong>{$key|escape:'html':'UTF-8'}</strong></td>
<td>{$value|escape:'html':'UTF-8'}</td>
<th>{l s='SKU' mod='ecomzone'}</th>
<th>{l s='Name' mod='ecomzone'}</th>
<th>{l s='Price' mod='ecomzone'}</th>
<th>{l s='Stock' mod='ecomzone'}</th>
<th class="text-right">{l s='Actions' mod='ecomzone'}</th>
</tr>
</thead>
<tbody>
{foreach from=$API_PRODUCTS item=product}
<tr>
<td>{$product.sku|escape:'html':'UTF-8'}</td>
<td>{$product.product_name|escape:'html':'UTF-8'}</td>
<td>{$product.product_price|escape:'html':'UTF-8'}</td>
<td>{$product.stock|escape:'html':'UTF-8'}</td>
<td class="text-right">
<form method="post" style="display:inline;">
<input type="hidden" name="import_sku"
value="{$product.sku|escape:'html':'UTF-8'}" />
<button type="submit" name="importSingleProduct"
class="btn btn-xs btn-primary"
title="{l s='Import this product' mod='ecomzone'}">
<i class="icon-download"></i> {l s='Import' mod='ecomzone'}
</button>
</form>
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
{if isset($PAGINATION)}
<div class="row">
<div class="col-lg-12 text-center">
<ul class="pagination">
{if $PAGINATION.current_page > 1}
<li>
<a href="{$current_url|escape:'html':'UTF-8'}&page=1" title="{l s='First page' mod='ecomzone'}">
<i class="icon-double-angle-left"></i>
</a>
</li>
<li>
<a href="{$current_url|escape:'html':'UTF-8'}&page={$PAGINATION.current_page-1}"
title="{l s='Previous page' mod='ecomzone'}">
<i class="icon-angle-left"></i>
</a>
</li>
{/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}
<li {if $p == $PAGINATION.current_page}class="active"{/if}>
<a href="{$current_url|escape:'html':'UTF-8'}&page={$p}">{$p}</a>
</li>
{assign var=p value=$p+1}
{/while}
{if $PAGINATION.current_page < $PAGINATION.total_pages}
<li>
<a href="{$current_url|escape:'html':'UTF-8'}&page={$PAGINATION.current_page+1}"
title="{l s='Next page' mod='ecomzone'}">
<i class="icon-angle-right"></i>
</a>
</li>
<li>
<a href="{$current_url|escape:'html':'UTF-8'}&page={$PAGINATION.total_pages}"
title="{l s='Last page' mod='ecomzone'}">
<i class="icon-double-angle-right"></i>
</a>
</li>
{/if}
</ul>
</div>
</div>
{/if}
{/if}
</div>
</div>
<div class="panel">
<h3><i class="icon icon-clock-o"></i> {l s='Automatic Sync' mod='ecomzone'}</h3>
<div class="panel-body">
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-lg-3">{l s='Cron URL' mod='ecomzone'}</label>
<div class="col-lg-9">
<div class="input-group">
<input type="text" class="form-control" readonly="readonly"
value="{$shop_url}modules/ecomzone/cron.php?token={$ECOMZONE_CRON_TOKEN|escape:'html':'UTF-8'}" />
<span class="input-group-btn">
<button class="btn btn-default" type="button" onclick="selectText(this.parentElement.previousElementSibling)">
<i class="icon-copy"></i> {l s='Copy' mod='ecomzone'}
</button>
</span>
</div>
<p class="help-block">
{l s='Add this URL to your server\'s crontab to run every 24 hours.' mod='ecomzone'}
</p>
</div>
</div>
</div>
</div>
</div>
{if isset($ECOMZONE_LOGS) && $ECOMZONE_LOGS}
<div class="panel">
<h3><i class="icon icon-list"></i> {l s='Recent Logs' mod='ecomzone'}</h3>
<div class="panel-body">
<div class="log-container" style="max-height: 400px; overflow-y: auto;">
<table class="table table-striped">
<tbody>
{foreach from=$ECOMZONE_LOGS item=log}
<tr>
<td><strong>{l s='Last Cron Run' mod='ecomzone'}</strong></td>
<td>{$ECOMZONE_LAST_CRON_RUN|escape:'html':'UTF-8'}</td>
</tr>
<tr>
<td><strong>{l s='Next Scheduled Run' mod='ecomzone'}</strong></td>
<td>{$ECOMZONE_NEXT_CRON_RUN|escape:'html':'UTF-8'}</td>
<td style="font-family: monospace;">{$log|escape:'html':'UTF-8'}</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
</div>
</div>
{/if}
<div class="panel">
<div class="panel-heading">
<i class="icon-list"></i> {l s='Recent Logs' mod='ecomzone'}
</div>
<div class="form-wrapper">
<div class="log-container" style="max-height: 400px; overflow-y: auto;">
{foreach from=$ECOMZONE_LOGS item=log}
<div class="log-line">{$log|escape:'html':'UTF-8'}</div>
{/foreach}
</div>
</div>
</div>
<script type="text/javascript">
function selectText(element) {
element.select();
try {
document.execCommand('copy');
showSuccessMessage('{l s='Copied to clipboard!' mod='ecomzone'}');
} catch (err) {
showErrorMessage('{l s='Failed to copy. Please copy manually.' mod='ecomzone'}');
}
}
</script>
<style type="text/css">
.log-container {
background: #f8f8f8;
border: 1px solid #ddd;
border-radius: 3px;
}
.log-container table {
margin-bottom: 0;
}
.input-group .form-control {
font-family: monospace;
font-size: 12px;
}
</style>
+8
View File
@@ -0,0 +1,8 @@
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit();
+8
View File
@@ -0,0 +1,8 @@
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit();
+27
View File
@@ -0,0 +1,27 @@
{
"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
}
}