category -> product association implemented
This commit is contained in:
parent
0b8c5de8b0
commit
c8bcfe6bd6
33
data.json
Normal file
33
data.json
Normal 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
ecomzone-v10.zip
Normal file
BIN
ecomzone-v10.zip
Normal file
Binary file not shown.
BIN
ecomzone-v11.zip
Normal file
BIN
ecomzone-v11.zip
Normal file
Binary file not shown.
BIN
ecomzone-v12.zip
Normal file
BIN
ecomzone-v12.zip
Normal file
Binary file not shown.
BIN
ecomzone-v13.zip
Normal file
BIN
ecomzone-v13.zip
Normal file
Binary file not shown.
BIN
ecomzone-v14.zip
Normal file
BIN
ecomzone-v14.zip
Normal file
Binary file not shown.
BIN
ecomzone-v15.zip
Normal file
BIN
ecomzone-v15.zip
Normal file
Binary file not shown.
BIN
ecomzone-v16.zip
Normal file
BIN
ecomzone-v16.zip
Normal file
Binary file not shown.
BIN
ecomzone-v17.zip
Normal file
BIN
ecomzone-v17.zip
Normal file
Binary file not shown.
BIN
ecomzone-v18.zip
Normal file
BIN
ecomzone-v18.zip
Normal file
Binary file not shown.
BIN
ecomzone-v19.zip
Normal file
BIN
ecomzone-v19.zip
Normal file
Binary file not shown.
BIN
ecomzone-v20.zip
Normal file
BIN
ecomzone-v20.zip
Normal file
Binary file not shown.
BIN
ecomzone-v5.2.zip
Normal file
BIN
ecomzone-v5.2.zip
Normal file
Binary file not shown.
BIN
ecomzone-v5.3.zip
Normal file
BIN
ecomzone-v5.3.zip
Normal file
Binary file not shown.
BIN
ecomzone-v6.1.zip
Normal file
BIN
ecomzone-v6.1.zip
Normal file
Binary file not shown.
BIN
ecomzone-v6.2.zip
Normal file
BIN
ecomzone-v6.2.zip
Normal file
Binary file not shown.
BIN
ecomzone-v6.zip
Normal file
BIN
ecomzone-v6.zip
Normal file
Binary file not shown.
BIN
ecomzone-v7.zip
Normal file
BIN
ecomzone-v7.zip
Normal file
Binary file not shown.
BIN
ecomzone-v8.zip
Normal file
BIN
ecomzone-v8.zip
Normal file
Binary file not shown.
BIN
ecomzone-v9.zip
Normal file
BIN
ecomzone-v9.zip
Normal file
Binary file not shown.
53
ecomzone/classes/EcomZoneApiTest.php
Normal file
53
ecomzone/classes/EcomZoneApiTest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
215
ecomzone/classes/EcomZoneCategoryHandler.php
Normal file
215
ecomzone/classes/EcomZoneCategoryHandler.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,107 +1,152 @@
|
||||
<?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');
|
||||
|
||||
if (empty($this->apiUrl)) {
|
||||
throw new Exception('API URL not configured');
|
||||
$this->apiToken = Configuration::get("ECOMZONE_API_TOKEN");
|
||||
$this->apiUrl = Configuration::get("ECOMZONE_API_URL");
|
||||
|
||||
if (empty($this->apiToken)) {
|
||||
throw new EcomZoneException(
|
||||
"API token not configured",
|
||||
EcomZoneException::API_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->apiUrl)) {
|
||||
throw new EcomZoneException(
|
||||
"API URL not configured",
|
||||
EcomZoneException::API_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
EcomZoneLogger::log("API Client initialized", "INFO", [
|
||||
"url" => $this->apiUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCatalog($page = 1, $perPage = 100)
|
||||
{
|
||||
$url = $this->apiUrl . '/catalog?page=' . $page . '&per_page=' . $perPage;
|
||||
|
||||
EcomZoneLogger::log("Making API request", 'INFO', ['url' => $url]);
|
||||
return $this->makeRequest('GET', $url);
|
||||
$url = $this->apiUrl . "/catalog";
|
||||
$params = [
|
||||
"page" => $page,
|
||||
"per_page" => $perPage,
|
||||
];
|
||||
|
||||
return $this->makeRequest("GET", $url, $params);
|
||||
}
|
||||
|
||||
public function getProduct($sku)
|
||||
{
|
||||
return $this->makeRequest('GET', $this->apiUrl . '/product/' . $sku);
|
||||
$url = $this->apiUrl . "/product/" . urlencode($sku);
|
||||
return $this->makeRequest("GET", $url);
|
||||
}
|
||||
|
||||
public function createOrder($orderData)
|
||||
{
|
||||
return $this->makeRequest('POST', $this->apiUrl . '/ordering', $orderData);
|
||||
}
|
||||
|
||||
public function getOrder($orderId)
|
||||
{
|
||||
return $this->makeRequest('GET', $this->apiUrl . '/order/' . $orderId);
|
||||
}
|
||||
|
||||
private function makeRequest($method, $url, $data = null)
|
||||
private function makeRequest($method, $url, $params = [], $data = null)
|
||||
{
|
||||
$retryCount = 0;
|
||||
$maxRetries = 5;
|
||||
$backoff = 1; // initial backoff time in seconds
|
||||
do {
|
||||
$curl = curl_init();
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $this->apiToken,
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/json',
|
||||
];
|
||||
|
||||
$options = [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_SSL_VERIFYPEER => false, // Temporarily disable SSL verification for testing
|
||||
];
|
||||
|
||||
if ($data !== null) {
|
||||
$options[CURLOPT_POSTFIELDS] = json_encode($data);
|
||||
}
|
||||
|
||||
curl_setopt_array($curl, $options);
|
||||
|
||||
$response = curl_exec($curl);
|
||||
$err = curl_error($curl);
|
||||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
|
||||
EcomZoneLogger::log("API Response", 'INFO', [
|
||||
'url' => $url,
|
||||
'method' => $method,
|
||||
'http_code' => $httpCode,
|
||||
'curl_error' => $err,
|
||||
'response' => $response
|
||||
]);
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
if ($err) {
|
||||
if (strpos($err, 'Connection timed out') !== false && $retryCount < $maxRetries) {
|
||||
EcomZoneLogger::log("Connection timed out. Retrying in $backoff seconds...", 'WARNING');
|
||||
sleep($backoff);
|
||||
$backoff *= 2; // exponential backoff
|
||||
$retryCount++;
|
||||
} else {
|
||||
throw new Exception('cURL Error: ' . $err);
|
||||
}
|
||||
} else if ($httpCode >= 400) {
|
||||
throw new Exception('API Error: HTTP ' . $httpCode . ' - ' . $response);
|
||||
} else {
|
||||
$decodedResponse = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception('Invalid JSON response: ' . json_last_error_msg());
|
||||
}
|
||||
return $decodedResponse;
|
||||
}
|
||||
} while ($retryCount < $maxRetries);
|
||||
}
|
||||
$lastError = null;
|
||||
|
||||
do {
|
||||
try {
|
||||
EcomZoneLogger::log("Making API request", "INFO", [
|
||||
"method" => $method,
|
||||
"url" => $url,
|
||||
"params" => $params,
|
||||
"retry" => $retryCount,
|
||||
]);
|
||||
|
||||
// Add query parameters
|
||||
if (!empty($params)) {
|
||||
$url .=
|
||||
(strpos($url, "?") === false ? "?" : "&") .
|
||||
http_build_query($params);
|
||||
}
|
||||
|
||||
$curl = curl_init();
|
||||
$options = [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_ENCODING => "",
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
CURLOPT_TIMEOUT => $this->timeout,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
"Authorization: Bearer " . $this->apiToken,
|
||||
"Accept: application/json",
|
||||
"Content-Type: application/json",
|
||||
],
|
||||
];
|
||||
|
||||
if ($data !== null) {
|
||||
$options[CURLOPT_POSTFIELDS] = json_encode($data);
|
||||
}
|
||||
|
||||
curl_setopt_array($curl, $options);
|
||||
$response = curl_exec($curl);
|
||||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($curl);
|
||||
curl_close($curl);
|
||||
|
||||
if ($error) {
|
||||
throw new Exception("cURL Error: " . $error);
|
||||
}
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
throw new Exception(
|
||||
"HTTP Error: " . $httpCode . " Response: " . $response
|
||||
);
|
||||
}
|
||||
|
||||
$decodedResponse = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception("Invalid JSON response");
|
||||
}
|
||||
|
||||
EcomZoneLogger::log("API request successful", "INFO", [
|
||||
"http_code" => $httpCode,
|
||||
]);
|
||||
|
||||
return $decodedResponse;
|
||||
} catch (Exception $e) {
|
||||
$lastError = $e;
|
||||
$retryCount++;
|
||||
|
||||
EcomZoneLogger::log("API request failed", "ERROR", [
|
||||
"error" => $e->getMessage(),
|
||||
"retry" => $retryCount,
|
||||
]);
|
||||
|
||||
if ($retryCount < $this->maxRetries) {
|
||||
sleep(pow(2, $retryCount));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new EcomZoneException(
|
||||
"API request failed after {$this->maxRetries} retries: " .
|
||||
$e->getMessage(),
|
||||
EcomZoneException::API_ERROR,
|
||||
$e
|
||||
);
|
||||
}
|
||||
} while ($retryCount < $this->maxRetries);
|
||||
|
||||
throw new EcomZoneException(
|
||||
"Max retries reached: " . $lastError->getMessage(),
|
||||
EcomZoneException::API_ERROR,
|
||||
$lastError
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
ecomzone/classes/EcomZoneException.php
Normal file
13
ecomzone/classes/EcomZoneException.php
Normal 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;
|
||||
}
|
||||
@ -1,25 +1,70 @@
|
||||
<?php
|
||||
if (!defined("_PS_VERSION_")) {
|
||||
exit();
|
||||
}
|
||||
|
||||
class EcomZoneLogger
|
||||
{
|
||||
const LOG_FILE = _PS_ROOT_DIR_ . '/var/logs/ecomzone.log';
|
||||
|
||||
public static function log($message, $level = 'INFO', $context = [])
|
||||
private static $logFile = 'ecomzone.log';
|
||||
private static $maxLogSize = 10485760; // 10MB
|
||||
|
||||
public static function log($message, $level = "INFO", $context = [])
|
||||
{
|
||||
$date = 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);
|
||||
|
||||
// Also log to PrestaShop
|
||||
PrestaShopLogger::addLog(
|
||||
"EcomZone: $message",
|
||||
($level === 'ERROR' ? 3 : 1),
|
||||
null,
|
||||
'EcomZone',
|
||||
null,
|
||||
true
|
||||
);
|
||||
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 = sprintf(
|
||||
"[%s] %s: %s %s\n",
|
||||
$timestamp,
|
||||
$level,
|
||||
$message,
|
||||
$contextStr
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
ecomzone/classes/EcomZoneProductImport.php
Normal file
345
ecomzone/classes/EcomZoneProductImport.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
$destImage = imagecreatetruecolor($width, $height);
|
||||
imagecopyresampled($destImage, $srcImage, 0, 0, 0, 0, $width, $height, $srcWidth, $srcHeight);
|
||||
|
||||
$this->saveImageFromType($destImage, $dest, $type);
|
||||
|
||||
imagedestroy($srcImage);
|
||||
imagedestroy($destImage);
|
||||
$page = 1;
|
||||
$totalImported = 0;
|
||||
$totalAvailable = 0;
|
||||
$errors = [];
|
||||
|
||||
try {
|
||||
EcomZoneLogger::log("Starting product import", "INFO", [
|
||||
"per_page" => $perPage,
|
||||
]);
|
||||
|
||||
do {
|
||||
$catalog = $this->client->getCatalog($page, $perPage);
|
||||
|
||||
if (!isset($catalog["data"]) || !is_array($catalog["data"])) {
|
||||
throw new EcomZoneException(
|
||||
"Invalid catalog response",
|
||||
EcomZoneException::API_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($catalog["data"] as $product) {
|
||||
try {
|
||||
if ($this->importSingleProduct($product)) {
|
||||
$totalImported++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errors[] = [
|
||||
"sku" => $product["sku"] ?? "unknown",
|
||||
"error" => $e->getMessage(),
|
||||
];
|
||||
EcomZoneLogger::log("Product import failed", "ERROR", [
|
||||
"sku" => $product["sku"] ?? "unknown",
|
||||
"error" => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$totalAvailable = $catalog["total"] ?? 0;
|
||||
$page++;
|
||||
|
||||
EcomZoneLogger::log("Page processed", "INFO", [
|
||||
"page" => $page - 1,
|
||||
"imported" => $totalImported,
|
||||
"total" => $totalAvailable,
|
||||
]);
|
||||
} while (
|
||||
isset($catalog["next_page_url"]) &&
|
||||
$catalog["next_page_url"] !== null
|
||||
);
|
||||
|
||||
$this->clearCache();
|
||||
|
||||
return [
|
||||
"success" => true,
|
||||
"imported" => $totalImported,
|
||||
"total" => $totalAvailable,
|
||||
"errors" => $errors,
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Product import process failed", "ERROR", [
|
||||
"error" => $e->getMessage(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function createImageFromType($filename, $type)
|
||||
|
||||
public function importSingleProduct(array $productData): bool
|
||||
{
|
||||
try {
|
||||
// Get SKU from catalog data
|
||||
$sku = $productData["sku"] ?? $productData["data"]["sku"] ?? null;
|
||||
|
||||
if (!$sku) {
|
||||
throw new EcomZoneException("Missing SKU in product data");
|
||||
}
|
||||
|
||||
// Fetch complete product data from product endpoint
|
||||
$fullProductData = $this->client->getProduct($sku);
|
||||
|
||||
if (!isset($fullProductData["data"])) {
|
||||
throw new EcomZoneException("Invalid product response");
|
||||
}
|
||||
|
||||
$data = $fullProductData["data"];
|
||||
|
||||
EcomZoneLogger::log("Processing product", "INFO", [
|
||||
"sku" => $sku,
|
||||
"categories" => $data["categories"] ?? null,
|
||||
"google_categories" => $data["google_categories"] ?? null
|
||||
]);
|
||||
|
||||
$this->validateProductData($data);
|
||||
|
||||
// Start transaction
|
||||
Db::getInstance()->execute("START TRANSACTION");
|
||||
|
||||
$product = $this->getOrCreateProduct($sku);
|
||||
|
||||
// Update product data
|
||||
$this->updateProductData($product, $data);
|
||||
$this->updateProductCategories($product, $data);
|
||||
$this->updateProductStock($product, $data);
|
||||
$this->processProductImages($product, $data);
|
||||
|
||||
// Commit transaction
|
||||
Db::getInstance()->execute("COMMIT");
|
||||
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
Db::getInstance()->execute("ROLLBACK");
|
||||
EcomZoneLogger::log("Product import failed", "ERROR", [
|
||||
"sku" => $sku ?? "unknown",
|
||||
"error" => $e->getMessage()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function validateProductData(array $data): void
|
||||
{
|
||||
$requiredFields = [
|
||||
'sku',
|
||||
'product_name',
|
||||
'product_price',
|
||||
'description',
|
||||
'stock'
|
||||
];
|
||||
|
||||
foreach ($requiredFields as $field) {
|
||||
if (empty($data[$field])) {
|
||||
throw new EcomZoneException(
|
||||
"Missing required field: {$field}",
|
||||
EcomZoneException::VALIDATION_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getOrCreateProduct(string $sku): Product
|
||||
{
|
||||
try {
|
||||
$productId = Product::getIdByReference($sku);
|
||||
|
||||
if ($productId) {
|
||||
$product = new Product($productId);
|
||||
if (!Validate::isLoadedObject($product)) {
|
||||
throw new Exception("Invalid product object");
|
||||
}
|
||||
return $product;
|
||||
}
|
||||
|
||||
$product = new Product();
|
||||
$product->reference = $sku;
|
||||
return $product;
|
||||
} catch (Exception $e) {
|
||||
throw new EcomZoneException(
|
||||
"Failed to get/create product: " . $e->getMessage(),
|
||||
EcomZoneException::INVALID_PRODUCT_DATA,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateProductData(Product $product, array $data): void
|
||||
{
|
||||
try {
|
||||
// Basic information
|
||||
$product->name[$this->defaultLangId] = $data['product_name'];
|
||||
$product->description[$this->defaultLangId] = $data['long_description'] ?? $data['description'];
|
||||
$product->description_short[$this->defaultLangId] = $data['description'];
|
||||
$product->price = (float)$data['product_price'];
|
||||
$product->wholesale_price = (float)($data['recommended_retail_price'] ?? 0);
|
||||
$product->reference = $data['sku'];
|
||||
$product->ean13 = $data['ean'] ?? '';
|
||||
|
||||
// Shop configuration
|
||||
$product->id_shop_default = $this->defaultShopId;
|
||||
$product->active = true;
|
||||
$product->visibility = 'both';
|
||||
$product->available_for_order = true;
|
||||
$product->show_price = true;
|
||||
$product->indexed = 1;
|
||||
|
||||
// Update dimensions if available
|
||||
if (!empty($data['dimensions'])) {
|
||||
$dimensions = json_decode($data['dimensions'], true);
|
||||
if (is_array($dimensions)) {
|
||||
$product->weight = (float)($dimensions['weight'] ?? 0);
|
||||
$product->height = (float)($dimensions['height'] ?? 0);
|
||||
$product->width = (float)($dimensions['width'] ?? 0);
|
||||
$product->depth = (float)($dimensions['length'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Set tax rules if configured
|
||||
$taxRulesGroupId = (int)Configuration::get('PS_TAX_RULES_GROUP_DEFAULT');
|
||||
if ($taxRulesGroupId) {
|
||||
$product->id_tax_rules_group = $taxRulesGroupId;
|
||||
}
|
||||
|
||||
// Save the product
|
||||
if (!$product->id) {
|
||||
if (!$product->add()) {
|
||||
throw new EcomZoneException('Failed to create product');
|
||||
}
|
||||
} else {
|
||||
if (!$product->update()) {
|
||||
throw new EcomZoneException('Failed to update product');
|
||||
}
|
||||
}
|
||||
|
||||
// Update shop association
|
||||
if (Shop::isFeatureActive()) {
|
||||
Shop::setContext(Shop::CONTEXT_SHOP, $this->defaultShopId);
|
||||
$product->id_shop_list = [$this->defaultShopId];
|
||||
$product->id_shop_default = $this->defaultShopId;
|
||||
|
||||
// Associate product with shop
|
||||
Db::getInstance()->execute('
|
||||
INSERT IGNORE INTO '._DB_PREFIX_.'product_shop
|
||||
(id_product, id_shop, price, wholesale_price)
|
||||
VALUES
|
||||
('.(int)$product->id.', '.(int)$this->defaultShopId.',
|
||||
'.(float)$product->price.', '.(float)$product->wholesale_price.')
|
||||
');
|
||||
}
|
||||
|
||||
// Process specifications if available
|
||||
if (!empty($data['specifications'])) {
|
||||
$this->updateProductFeatures($product, $data['specifications']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw new EcomZoneException(
|
||||
'Failed to update product data: ' . $e->getMessage(),
|
||||
EcomZoneException::INVALID_PRODUCT_DATA,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateProductCategories(Product $product, array $data): void
|
||||
{
|
||||
try {
|
||||
$categoryIds = [];
|
||||
|
||||
// Process main categories
|
||||
if (!empty($data['categories'])) {
|
||||
$categories = json_decode($data['categories'], true);
|
||||
if (is_array($categories)) {
|
||||
foreach ($categories as $categoryPath) {
|
||||
// Skip special categories that start with '0'
|
||||
if (strpos($categoryPath, '0') === 0) {
|
||||
continue;
|
||||
}
|
||||
// Create each category as a single path
|
||||
$categoryId = $this->categoryHandler->createCategoryPath([$categoryPath]);
|
||||
if ($categoryId) {
|
||||
$categoryIds[] = $categoryId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process Google categories
|
||||
if (!empty($data['google_categories'])) {
|
||||
$googleCategories = json_decode($data['google_categories'], true);
|
||||
if (is_array($googleCategories)) {
|
||||
$categoryId = $this->categoryHandler->createCategoryPath($googleCategories);
|
||||
if ($categoryId) {
|
||||
$categoryIds[] = $categoryId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we have at least the default category
|
||||
if (empty($categoryIds)) {
|
||||
$categoryIds[] = (int)Configuration::get('PS_HOME_CATEGORY');
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
$categoryIds = array_unique($categoryIds);
|
||||
|
||||
// Set default category
|
||||
$product->id_category_default = reset($categoryIds);
|
||||
|
||||
// Save the product to ensure it has an ID
|
||||
if (!$product->id) {
|
||||
$product->add();
|
||||
} else {
|
||||
$product->update();
|
||||
}
|
||||
|
||||
// Clear existing category associations
|
||||
Db::getInstance()->execute('
|
||||
DELETE FROM `'._DB_PREFIX_.'category_product`
|
||||
WHERE id_product = '.(int)$product->id
|
||||
);
|
||||
|
||||
// Insert new category associations
|
||||
foreach ($categoryIds as $categoryId) {
|
||||
Db::getInstance()->execute('
|
||||
INSERT INTO `'._DB_PREFIX_.'category_product`
|
||||
(id_category, id_product, position)
|
||||
VALUES
|
||||
('.(int)$categoryId.', '.(int)$product->id.', 0)
|
||||
');
|
||||
}
|
||||
|
||||
// Update shop associations if needed
|
||||
if (Shop::isFeatureActive()) {
|
||||
$product->id_shop_list = [$this->defaultShopId];
|
||||
$product->id_shop_default = $this->defaultShopId;
|
||||
}
|
||||
|
||||
EcomZoneLogger::log('Categories updated', 'DEBUG', [
|
||||
'product' => $product->reference,
|
||||
'category_ids' => $categoryIds
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw new EcomZoneException(
|
||||
'Failed to update product categories: ' . $e->getMessage(),
|
||||
EcomZoneException::CATEGORY_CREATE_ERROR,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateProductStock(Product $product, array $data): void
|
||||
{
|
||||
try {
|
||||
$quantity = (int) ($data['stock_quantity'] ?? 0);
|
||||
StockAvailable::setQuantity($product->id, 0, $quantity);
|
||||
|
||||
// Update out of stock behavior
|
||||
StockAvailable::setProductOutOfStock(
|
||||
$product->id,
|
||||
$quantity > 0 ? 2 : 0, // 2 = Allow orders, 0 = Deny orders
|
||||
$this->defaultShopId
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Failed to update stock", "WARNING", [
|
||||
"product" => $product->reference,
|
||||
"error" => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function processProductImages(Product $product, array $data): void
|
||||
{
|
||||
try {
|
||||
$imageUrls = [];
|
||||
|
||||
// Process main image
|
||||
if (!empty($data["image"])) {
|
||||
$imageUrls[] = $data["image"];
|
||||
}
|
||||
|
||||
// Process additional images
|
||||
if (!empty($data["images"])) {
|
||||
$additionalImages = json_decode($data["images"], true);
|
||||
if (is_array($additionalImages)) {
|
||||
$imageUrls = array_merge($imageUrls, $additionalImages);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($imageUrls)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete existing images if any
|
||||
$product->deleteImages();
|
||||
|
||||
foreach ($imageUrls as $index => $imageUrl) {
|
||||
$this->importProductImage($product, $imageUrl, $index === 0);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Failed to process product images", "WARNING", [
|
||||
"sku" => $product->reference,
|
||||
"error" => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function importProductImage(Product $product, string $imageUrl, bool $cover = false): void
|
||||
{
|
||||
try {
|
||||
// Create temporary file
|
||||
$tempFile = tempnam(_PS_TMP_IMG_DIR_, 'import_');
|
||||
|
||||
// Download image
|
||||
if (!copy($imageUrl, $tempFile)) {
|
||||
throw new Exception("Failed to download image: " . $imageUrl);
|
||||
}
|
||||
|
||||
// Create image object
|
||||
$image = new Image();
|
||||
$image->id_product = $product->id;
|
||||
$image->position = Image::getHighestPosition($product->id) + 1;
|
||||
$image->cover = $cover;
|
||||
|
||||
if (!$image->add()) {
|
||||
throw new Exception("Failed to create image record");
|
||||
}
|
||||
|
||||
// Create product image directory if it doesn't exist
|
||||
$imagePath = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath();
|
||||
$imageDir = dirname($imagePath);
|
||||
|
||||
if (!file_exists($imageDir)) {
|
||||
mkdir($imageDir, 0755, true);
|
||||
}
|
||||
|
||||
// Generate image
|
||||
if (!ImageManager::resize(
|
||||
$tempFile,
|
||||
$imagePath . '.jpg'
|
||||
)) {
|
||||
throw new Exception("Failed to process image");
|
||||
}
|
||||
|
||||
// Generate thumbnails
|
||||
$this->generateThumbnails($image);
|
||||
|
||||
// Clean up
|
||||
unlink($tempFile);
|
||||
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Failed to import product image", "ERROR", [
|
||||
"product" => $product->reference,
|
||||
"image_url" => $imageUrl,
|
||||
"error" => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function generateThumbnails(Image $image): void
|
||||
{
|
||||
try {
|
||||
$imageTypes = ImageType::getImagesTypes('products');
|
||||
foreach ($imageTypes as $imageType) {
|
||||
$dir = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath();
|
||||
if (!file_exists($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
ImageManager::resize(
|
||||
_PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg',
|
||||
_PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg',
|
||||
(int)$imageType['width'],
|
||||
(int)$imageType['height']
|
||||
);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Failed to generate thumbnails", "WARNING", [
|
||||
"image_id" => $image->id,
|
||||
"error" => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function generateImageType(
|
||||
string $srcPath,
|
||||
string $destPath,
|
||||
?int $width,
|
||||
?int $height
|
||||
): void {
|
||||
try {
|
||||
$this->createDirectoryIfNotExists(dirname($destPath));
|
||||
|
||||
if ($width === null || $height === null) {
|
||||
copy($srcPath, $destPath);
|
||||
return;
|
||||
}
|
||||
|
||||
$imageInfo = getimagesize($srcPath);
|
||||
if (!$imageInfo) {
|
||||
throw new Exception("Invalid image file");
|
||||
}
|
||||
|
||||
$srcImage = $this->createImageFromType($srcPath, $imageInfo[2]);
|
||||
$destImage = imagecreatetruecolor($width, $height);
|
||||
|
||||
// Preserve transparency for PNG images
|
||||
if ($imageInfo[2] === IMAGETYPE_PNG) {
|
||||
imagealphablending($destImage, false);
|
||||
imagesavealpha($destImage, true);
|
||||
$transparent = imagecolorallocatealpha(
|
||||
$destImage,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
127
|
||||
);
|
||||
imagefilledrectangle(
|
||||
$destImage,
|
||||
0,
|
||||
0,
|
||||
$width,
|
||||
$height,
|
||||
$transparent
|
||||
);
|
||||
}
|
||||
|
||||
imagecopyresampled(
|
||||
$destImage,
|
||||
$srcImage,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$width,
|
||||
$height,
|
||||
$imageInfo[0],
|
||||
$imageInfo[1]
|
||||
);
|
||||
|
||||
imagejpeg($destImage, $destPath, 95);
|
||||
|
||||
imagedestroy($srcImage);
|
||||
imagedestroy($destImage);
|
||||
} catch (Exception $e) {
|
||||
throw new EcomZoneException(
|
||||
"Failed to generate image type: " . $e->getMessage(),
|
||||
EcomZoneException::IMAGE_PROCESSING_ERROR,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function createImageFromType(string $filename, int $type)
|
||||
{
|
||||
switch ($type) {
|
||||
case IMAGETYPE_JPEG:
|
||||
@ -36,236 +565,159 @@ class EcomZoneProductSync
|
||||
throw new Exception("Unsupported image type: " . $type);
|
||||
}
|
||||
}
|
||||
|
||||
private function saveImageFromType($image, $filename, $type)
|
||||
|
||||
private function createDirectoryIfNotExists(string $directory): void
|
||||
{
|
||||
switch ($type) {
|
||||
case IMAGETYPE_JPEG:
|
||||
$this->createDirectoryIfNotExists(dirname($filename));
|
||||
imagejpeg($image, $filename);
|
||||
break;
|
||||
case IMAGETYPE_PNG:
|
||||
$this->createDirectoryIfNotExists(dirname($filename));
|
||||
imagepng($image, $filename);
|
||||
break;
|
||||
case IMAGETYPE_GIF:
|
||||
$this->createDirectoryIfNotExists(dirname($filename));
|
||||
imagegif($image, $filename);
|
||||
break;
|
||||
case IMAGETYPE_GIF:
|
||||
imagegif($image, $filename);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unsupported image type: " . $type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function importProducts($perPage = 100)
|
||||
{
|
||||
$page = 1;
|
||||
$totalImported = 0;
|
||||
$totalAvailable = 0;
|
||||
|
||||
EcomZoneLogger::log("Starting product import - perPage: $perPage");
|
||||
|
||||
EcomZoneLogger::log("Number of products to be imported per page: $perPage", 'INFO');
|
||||
|
||||
try {
|
||||
do {
|
||||
$catalog = $this->client->getCatalog($page, $perPage);
|
||||
$importedCount = 0;
|
||||
foreach ($catalog['data'] as $product) {
|
||||
if ($this->importSingleProduct($product)) {
|
||||
$importedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$totalImported += $importedCount;
|
||||
$totalAvailable = $catalog['total'];
|
||||
$page++;
|
||||
|
||||
EcomZoneLogger::log("Imported page $page", 'INFO', [
|
||||
'total_imported' => $totalImported,
|
||||
'page' => $page,
|
||||
'total_available' => $totalAvailable
|
||||
]);
|
||||
|
||||
} while ($page <= ceil($totalAvailable / $perPage) && isset($catalog['next_page_url']) && $catalog['next_page_url'] !== null);
|
||||
|
||||
EcomZoneLogger::log("Finished importing products", 'INFO', [
|
||||
'total_imported' => $totalImported,
|
||||
'total_available' => $totalAvailable
|
||||
]);
|
||||
|
||||
// Clear cache
|
||||
Tools::clearSmartyCache();
|
||||
Tools::clearXMLCache();
|
||||
Media::clearCache();
|
||||
PrestaShopAutoload::getInstance()->generateIndex();
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'imported' => $totalImported,
|
||||
'total' => $totalAvailable
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Error importing products: " . $e->getMessage(), 'ERROR');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function importSingleProduct($productData)
|
||||
{
|
||||
// Extract data from nested structure
|
||||
$data = isset($productData['data']) ? $productData['data'] : $productData;
|
||||
|
||||
if (!isset($data['sku']) || !isset($data['product_name']) || !isset($data['description']) || !isset($data['product_price'])) {
|
||||
EcomZoneLogger::log("Invalid product data", 'ERROR', ['data' => $productData]);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if product already exists by reference
|
||||
$productId = Db::getInstance()->getValue('
|
||||
SELECT id_product
|
||||
FROM ' . _DB_PREFIX_ . 'product
|
||||
WHERE reference = "' . pSQL($data['sku']) . '"
|
||||
');
|
||||
|
||||
$product = $productId ? new Product($productId) : new Product();
|
||||
|
||||
$defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT');
|
||||
|
||||
$product->reference = $data['sku'];
|
||||
$product->name[$defaultLangId] = $data['product_name'];
|
||||
$product->description[$defaultLangId] = $data['long_description'] ?? $data['description'];
|
||||
$product->description_short[$defaultLangId] = $data['description'];
|
||||
$product->price = $data['product_price'];
|
||||
$product->active = true;
|
||||
$product->quantity = (int)$data['stock'];
|
||||
$homeCategoryId = (int)Configuration::get('PS_HOME_CATEGORY');
|
||||
$product->id_category_default = $homeCategoryId;
|
||||
$product->addToCategories([$homeCategoryId]);
|
||||
$product->visibility = 'both'; // Ensure product is visible in both catalog and search
|
||||
$product->active = true;
|
||||
$product->quantity = (int)$data['stock'];
|
||||
$homeCategoryId = (int)Configuration::get('PS_HOME_CATEGORY');
|
||||
$product->id_category_default = $homeCategoryId;
|
||||
$product->addToCategories([$homeCategoryId]);
|
||||
|
||||
|
||||
// Save product first to get ID
|
||||
if (!$product->id) {
|
||||
$product->add();
|
||||
} else {
|
||||
$product->update();
|
||||
if (!is_dir($directory)) {
|
||||
if (!mkdir($directory, 0755, true)) {
|
||||
throw new Exception(
|
||||
"Failed to create directory: " . $directory
|
||||
);
|
||||
}
|
||||
StockAvailable::setQuantity($product->id, 0, (int)$data['stock']);
|
||||
$product->available_for_order = true; // Ensure product is available for order
|
||||
$product->show_price = true; // Ensure price is shown
|
||||
}
|
||||
}
|
||||
|
||||
// Handle image import if URL is provided
|
||||
if (isset($data['image']) && !empty($data['image'])) {
|
||||
$this->importProductImage($product, $data['image']);
|
||||
private function updateProductFeatures(Product $product, string $specificationsJson): void
|
||||
{
|
||||
try {
|
||||
$specifications = json_decode($specificationsJson, true);
|
||||
if (!is_array($specifications)) {
|
||||
return;
|
||||
}
|
||||
|
||||
StockAvailable::setQuantity($product->id, 0, (int)$data['stock']);
|
||||
|
||||
EcomZoneLogger::log("Imported product", 'INFO', [
|
||||
'sku' => $data['sku'],
|
||||
'id' => $product->id,
|
||||
'name' => $data['product_name']
|
||||
|
||||
foreach ($specifications as $featureName => $featureValue) {
|
||||
if (empty($featureValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert array or object to string if necessary
|
||||
if (is_array($featureValue) || is_object($featureValue)) {
|
||||
$featureValue = json_encode($featureValue);
|
||||
}
|
||||
|
||||
// Ensure we have a string value
|
||||
$featureValue = (string)$featureValue;
|
||||
|
||||
// Get or create feature using DB query
|
||||
$featureId = $this->getOrCreateFeature($featureName);
|
||||
if (!$featureId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get or create feature value
|
||||
$featureValueId = $this->getOrCreateFeatureValue($featureId, $featureValue);
|
||||
if (!$featureValueId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Associate feature with product
|
||||
$product->addFeaturesToDB(
|
||||
[$featureId => $featureValueId],
|
||||
$this->defaultLangId
|
||||
);
|
||||
}
|
||||
|
||||
EcomZoneLogger::log("Features updated successfully", "DEBUG", [
|
||||
"product" => $product->reference,
|
||||
"specifications" => $specifications
|
||||
]);
|
||||
|
||||
return true;
|
||||
|
||||
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Error importing product", 'ERROR', [
|
||||
'sku' => $data['sku'],
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return false;
|
||||
EcomZoneLogger::log(
|
||||
"Failed to update product features",
|
||||
"WARNING",
|
||||
[
|
||||
"product" => $product->reference,
|
||||
"error" => $e->getMessage(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getOrCreateFeature(string $name): ?int
|
||||
{
|
||||
// Try to find existing feature
|
||||
$featureId = (int)Db::getInstance()->getValue(
|
||||
'SELECT f.id_feature
|
||||
FROM `'._DB_PREFIX_.'feature_lang` fl
|
||||
JOIN `'._DB_PREFIX_.'feature` f ON f.id_feature = fl.id_feature
|
||||
WHERE fl.name = "'.pSQL($name).'"
|
||||
AND fl.id_lang = '.(int)$this->defaultLangId
|
||||
);
|
||||
|
||||
|
||||
private function importProductImage($product, $imageUrl)
|
||||
{
|
||||
try {
|
||||
// Create temporary file
|
||||
$tmpFile = tempnam(_PS_TMP_IMG_DIR_, 'ecomzone_');
|
||||
|
||||
// Download image
|
||||
if (!copy($imageUrl, $tmpFile)) {
|
||||
throw new Exception("Failed to download image from: " . $imageUrl);
|
||||
}
|
||||
|
||||
// Get image info
|
||||
$imageInfo = getimagesize($tmpFile);
|
||||
if (!$imageInfo) {
|
||||
unlink($tmpFile);
|
||||
throw new Exception("Invalid image file");
|
||||
}
|
||||
|
||||
// Validate image dimensions and file size
|
||||
if ($imageInfo[0] > 2000 || $imageInfo[1] > 2000 || filesize($tmpFile) > 5000000) {
|
||||
unlink($tmpFile);
|
||||
throw new Exception("Image dimensions or file size exceed limits");
|
||||
}
|
||||
|
||||
// Generate unique name
|
||||
$imageName = $product->reference . '-' . time() . '.' . pathinfo($imageUrl, PATHINFO_EXTENSION);
|
||||
|
||||
// Delete existing images if any
|
||||
$product->deleteImages();
|
||||
|
||||
// Add new image
|
||||
$image = new Image();
|
||||
$image->id_product = $product->id;
|
||||
$image->position = 1;
|
||||
$image->cover = true;
|
||||
|
||||
// Save the image to the correct directory
|
||||
$imagePath = _PS_PROD_IMG_DIR_ . $image->getImgPath() . '.' . $image->image_format;
|
||||
$this->createDirectoryIfNotExists(dirname($imagePath));
|
||||
if (!copy($tmpFile, $imagePath)) {
|
||||
unlink($tmpFile);
|
||||
throw new Exception("Failed to save image to: " . $imagePath);
|
||||
if ($featureId) {
|
||||
return $featureId;
|
||||
}
|
||||
|
||||
// Associate the image with the product
|
||||
if (!$image->add()) {
|
||||
unlink($tmpFile);
|
||||
throw new Exception("Failed to add image to product");
|
||||
// Create new feature
|
||||
$feature = new Feature();
|
||||
$feature->name = array_fill_keys(Language::getIDs(), $name);
|
||||
|
||||
return $feature->add() ? (int)$feature->id : null;
|
||||
}
|
||||
|
||||
private function getOrCreateFeatureValue(int $featureId, string $value): ?int
|
||||
{
|
||||
// Try to find existing feature value
|
||||
$valueId = (int)Db::getInstance()->getValue(
|
||||
'SELECT fv.id_feature_value
|
||||
FROM `'._DB_PREFIX_.'feature_value_lang` fvl
|
||||
JOIN `'._DB_PREFIX_.'feature_value` fv ON fv.id_feature_value = fvl.id_feature_value
|
||||
WHERE fv.id_feature = '.(int)$featureId.'
|
||||
AND fvl.value = "'.pSQL($value).'"
|
||||
AND fvl.id_lang = '.(int)$this->defaultLangId
|
||||
);
|
||||
|
||||
if ($valueId) {
|
||||
return $valueId;
|
||||
}
|
||||
|
||||
// Manually resize the image and generate thumbnails
|
||||
$this->resizeImage($imagePath, _PS_PROD_IMG_DIR_ . $image->getImgPath() . '-home_default.' . $image->image_format, 250, 250);
|
||||
$this->resizeImage($imagePath, _PS_PROD_IMG_DIR_ . $image->getImgPath() . '-large_default.' . $image->image_format, 800, 800);
|
||||
|
||||
// Cleanup
|
||||
unlink($tmpFile);
|
||||
EcomZoneLogger::log("Imported product image", 'INFO', [
|
||||
'sku' => $product->reference,
|
||||
'image' => $imageUrl
|
||||
]);
|
||||
// Create new feature value
|
||||
$featureValue = new FeatureValue();
|
||||
$featureValue->id_feature = $featureId;
|
||||
$featureValue->value = array_fill_keys(Language::getIDs(), $value);
|
||||
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Error importing product image", 'ERROR', [
|
||||
'sku' => $product->reference,
|
||||
'image' => $imageUrl,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return $featureValue->add() ? (int)$featureValue->id : null;
|
||||
}
|
||||
|
||||
private function updateProductDimensions(
|
||||
Product $product,
|
||||
string $dimensionsJson
|
||||
): void {
|
||||
try {
|
||||
$dimensions = json_decode($dimensionsJson, true);
|
||||
if (!is_array($dimensions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product->weight = (float) ($dimensions["weight"] ?? 0);
|
||||
$product->height = (float) ($dimensions["height"] ?? 0);
|
||||
$product->width = (float) ($dimensions["width"] ?? 0);
|
||||
$product->depth = (float) ($dimensions["length"] ?? 0);
|
||||
|
||||
$product->update();
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log(
|
||||
"Failed to update product dimensions",
|
||||
"WARNING",
|
||||
[
|
||||
"product" => $product->reference,
|
||||
"error" => $e->getMessage(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function clearCache(): void
|
||||
{
|
||||
Tools::clearAllCache();
|
||||
Tools::clearSmartyCache();
|
||||
Tools::clearXMLCache();
|
||||
Media::clearCache();
|
||||
PrestaShopAutoload::getInstance()->generateIndex();
|
||||
}
|
||||
}
|
||||
|
||||
private function createDirectoryIfNotExists($directory)
|
||||
{
|
||||
if (!is_dir($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
$token = Configuration::get('ECOMZONE_CRON_TOKEN');
|
||||
echo $token;
|
||||
|
||||
|
||||
11
ecomzone/classes/interfaces/ICategory.php
Normal file
11
ecomzone/classes/interfaces/ICategory.php
Normal 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;
|
||||
}
|
||||
10
ecomzone/classes/interfaces/IProductSync.php
Normal file
10
ecomzone/classes/interfaces/IProductSync.php
Normal file
@ -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
ecomzone/config.xml
Normal file
13
ecomzone/config.xml
Normal 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>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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', '') &&
|
||||
Configuration::updateValue('ECOMZONE_CRON_TOKEN', Tools::encrypt(uniqid()));
|
||||
if (!parent::install()
|
||||
|| !$this->registerHook('actionCronJob')
|
||||
|| !$this->registerHook('actionProductUpdate')
|
||||
|| !$this->registerHook('actionProductDelete')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set default configuration values
|
||||
Configuration::updateValue('ECOMZONE_API_URL', 'https://dropship.ecomzone.eu/api');
|
||||
Configuration::updateValue('ECOMZONE_CRON_TOKEN', Tools::encrypt(uniqid()));
|
||||
Configuration::updateValue('ECOMZONE_LAST_SYNC', '');
|
||||
|
||||
// Create required directories
|
||||
$this->createRequiredDirectories();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createLogFile()
|
||||
private function createRequiredDirectories()
|
||||
{
|
||||
if (!file_exists(dirname(EcomZoneLogger::LOG_FILE))) {
|
||||
mkdir(dirname(EcomZoneLogger::LOG_FILE), 0755, true);
|
||||
$dirs = [
|
||||
_PS_MODULE_DIR_ . $this->name . '/log',
|
||||
_PS_MODULE_DIR_ . $this->name . '/tmp'
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if (!file_exists($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!file_exists(EcomZoneLogger::LOG_FILE)) {
|
||||
touch(EcomZoneLogger::LOG_FILE);
|
||||
chmod(EcomZoneLogger::LOG_FILE, 0666);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function uninstall()
|
||||
{
|
||||
return parent::uninstall() &&
|
||||
Configuration::deleteByName('ECOMZONE_API_TOKEN') &&
|
||||
Configuration::deleteByName('ECOMZONE_API_URL') &&
|
||||
Configuration::deleteByName('ECOMZONE_CRON_TOKEN') &&
|
||||
Configuration::deleteByName('ECOMZONE_LAST_CRON_RUN');
|
||||
Configuration::deleteByName("ECOMZONE_API_URL") &&
|
||||
Configuration::deleteByName("ECOMZONE_API_TOKEN") &&
|
||||
Configuration::deleteByName("ECOMZONE_CRON_TOKEN") &&
|
||||
Configuration::deleteByName("ECOMZONE_LAST_SYNC");
|
||||
}
|
||||
|
||||
public function getContent()
|
||||
{
|
||||
$output = '';
|
||||
|
||||
if (Tools::isSubmit('submitEcomZoneModule')) {
|
||||
Configuration::updateValue('ECOMZONE_API_TOKEN', Tools::getValue('ECOMZONE_API_TOKEN'));
|
||||
$output .= $this->displayConfirmation($this->l('Settings updated'));
|
||||
}
|
||||
|
||||
// Handle manual product import
|
||||
if (Tools::isSubmit('importP<Down>oducts')) {
|
||||
try {
|
||||
$productSync = new EcomZoneProductSync();
|
||||
$result = $productSync->importProducts();
|
||||
$output = "";
|
||||
|
||||
if (Tools::isSubmit("submitEcomZoneModule")) {
|
||||
$apiToken = Tools::getValue("ECOMZONE_API_TOKEN");
|
||||
if (Configuration::updateValue("ECOMZONE_API_TOKEN", $apiToken)) {
|
||||
$output .= $this->displayConfirmation(
|
||||
sprintf($this->l('Imported %d products'), $result['imported'])
|
||||
$this->trans(
|
||||
"Settings updated",
|
||||
[],
|
||||
"Modules.Ecomzone.Admin"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle product fetch request
|
||||
if (Tools::isSubmit("fetchProducts") || Tools::getValue("page")) {
|
||||
try {
|
||||
$page = (int) Tools::getValue("page", 1);
|
||||
$perPage = 10;
|
||||
|
||||
$result = $this->makeApiRequest(
|
||||
"catalog?page={$page}&per_page={$perPage}"
|
||||
);
|
||||
|
||||
if (!empty($result["data"])) {
|
||||
$totalPages = ceil($result["total"] / $perPage);
|
||||
|
||||
$this->context->smarty->assign([
|
||||
"API_PRODUCTS" => $result["data"],
|
||||
"PAGINATION" => [
|
||||
"total_pages" => $totalPages,
|
||||
"current_page" => $page,
|
||||
"total_items" => $result["total"],
|
||||
],
|
||||
]);
|
||||
|
||||
if (Tools::isSubmit("fetchProducts")) {
|
||||
$output .= $this->displayConfirmation(
|
||||
$this->trans(
|
||||
"Products fetched successfully",
|
||||
[],
|
||||
"Modules.Ecomzone.Admin"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$output .= $this->displayError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Add debug info
|
||||
$debugInfo = $this->getDebugInfo();
|
||||
|
||||
$shopUrl = Tools::getShopDomainSsl(true);
|
||||
$shopRoot = _PS_ROOT_DIR_;
|
||||
|
||||
$this->context->smarty->assign([
|
||||
'ECOMZONE_API_TOKEN' => Configuration::get('ECOMZONE_API_TOKEN'),
|
||||
'ECOMZONE_CRON_TOKEN' => Configuration::get('ECOMZONE_CRON_TOKEN'),
|
||||
'ECOMZONE_DEBUG_INFO' => $debugInfo,
|
||||
'ECOMZONE_LOGS' => $this->getRecentLogs(),
|
||||
'shop_url' => $shopUrl,
|
||||
'shop_root' => $shopRoot
|
||||
"module_dir" => $this->_path,
|
||||
"ECOMZONE_API_TOKEN" => Configuration::get("ECOMZONE_API_TOKEN"),
|
||||
"ECOMZONE_CRON_TOKEN" => Configuration::get("ECOMZONE_CRON_TOKEN"),
|
||||
"ECOMZONE_LAST_SYNC" => Configuration::get("ECOMZONE_LAST_SYNC"),
|
||||
"current_url" =>
|
||||
$this->context->link->getAdminLink("AdminModules", false) .
|
||||
"&configure=" .
|
||||
$this->name .
|
||||
"&tab_module=" .
|
||||
$this->tab .
|
||||
"&module_name=" .
|
||||
$this->name,
|
||||
"shop_url" => $this->context->link->getBaseLink(),
|
||||
]);
|
||||
|
||||
return $output . $this->display(__FILE__, 'views/templates/admin/configure.tpl');
|
||||
|
||||
return $output .
|
||||
$this->display(__FILE__, "views/templates/admin/configure.tpl");
|
||||
}
|
||||
|
||||
private function getDebugInfo()
|
||||
private function makeApiRequest($endpoint, $method = "GET", $data = null)
|
||||
{
|
||||
$lastCronRun = Configuration::get('ECOMZONE_LAST_CRON_RUN');
|
||||
$nextRun = !empty($lastCronRun) ?
|
||||
date('Y-m-d H:i:s', strtotime($lastCronRun) + 3600) :
|
||||
$this->l('Not scheduled yet');
|
||||
$apiUrl = Configuration::get("ECOMZONE_API_URL");
|
||||
$apiToken = Configuration::get("ECOMZONE_API_TOKEN");
|
||||
|
||||
return [
|
||||
'php_version' => PHP_VERSION,
|
||||
'prestashop_version' => _PS_VERSION_,
|
||||
'module_version' => $this->version,
|
||||
'curl_enabled' => function_exists('curl_version'),
|
||||
'api_url' => Configuration::get('ECOMZONE_API_URL'),
|
||||
'last_sync' => Configuration::get('ECOMZONE_LAST_SYNC'),
|
||||
'last_cron_run' => $lastCronRun ?: $this->l('Never'),
|
||||
'next_cron_run' => $nextRun,
|
||||
'log_file' => EcomZoneLogger::LOG_FILE,
|
||||
'log_file_exists' => file_exists(EcomZoneLogger::LOG_FILE),
|
||||
'log_file_writable' => is_writable(EcomZoneLogger::LOG_FILE)
|
||||
if (empty($apiToken)) {
|
||||
throw new Exception(
|
||||
$this->trans(
|
||||
"API token not configured",
|
||||
[],
|
||||
"Modules.Ecomzone.Admin"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$curl = curl_init();
|
||||
$url = rtrim($apiUrl, "/") . "/" . ltrim($endpoint, "/");
|
||||
|
||||
$options = [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_ENCODING => "",
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
"Authorization: Bearer " . $apiToken,
|
||||
"Accept: application/json",
|
||||
"Content-Type: application/json",
|
||||
],
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
];
|
||||
}
|
||||
|
||||
private function getRecentLogs($lines = 50)
|
||||
{
|
||||
if (!file_exists(EcomZoneLogger::LOG_FILE)) {
|
||||
return [];
|
||||
if ($data !== null) {
|
||||
$options[CURLOPT_POSTFIELDS] = json_encode($data);
|
||||
}
|
||||
|
||||
$logs = array_slice(file(EcomZoneLogger::LOG_FILE), -$lines);
|
||||
return array_map('trim', $logs);
|
||||
}
|
||||
|
||||
public function hookActionOrderStatusUpdate($params)
|
||||
{
|
||||
$order = $params['order'];
|
||||
$newOrderStatus = $params['newOrderStatus'];
|
||||
curl_setopt_array($curl, $options);
|
||||
|
||||
// Sync order when it's paid
|
||||
if ($newOrderStatus->paid == 1) {
|
||||
try {
|
||||
$orderSync = new EcomZoneOrderSync();
|
||||
$result = $orderSync->syncOrder($order->id);
|
||||
|
||||
// Log the result
|
||||
PrestaShopLogger::addLog(
|
||||
'EcomZone order sync: ' . json_encode($result),
|
||||
1,
|
||||
null,
|
||||
'Order',
|
||||
$order->id,
|
||||
true
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
PrestaShopLogger::addLog(
|
||||
'EcomZone order sync error: ' . $e->getMessage(),
|
||||
3,
|
||||
null,
|
||||
'Order',
|
||||
$order->id,
|
||||
true
|
||||
);
|
||||
}
|
||||
$response = curl_exec($curl);
|
||||
$err = curl_error($curl);
|
||||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
if ($err) {
|
||||
throw new Exception("cURL Error: " . $err);
|
||||
}
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
$errorData = json_decode($response, true);
|
||||
$errorMessage = isset($errorData['error'])
|
||||
? $errorData['error']
|
||||
: "HTTP Error: " . $httpCode;
|
||||
throw new EcomZoneException(
|
||||
$errorMessage,
|
||||
EcomZoneException::API_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
$decodedResponse = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception("Invalid JSON response");
|
||||
}
|
||||
|
||||
return $decodedResponse;
|
||||
}
|
||||
|
||||
public function hookActionCronJob($params)
|
||||
{
|
||||
$cronTask = new EcomZoneCronTask();
|
||||
|
||||
// Check if it's time to run
|
||||
$lastRun = Configuration::get('ECOMZONE_LAST_CRON_RUN');
|
||||
if (empty($lastRun) || (strtotime($lastRun) + $cronTask->cron_frequency) <= time()) {
|
||||
return $cronTask->run();
|
||||
try {
|
||||
$lastSync = Configuration::get("ECOMZONE_LAST_SYNC");
|
||||
$syncInterval = 24 * 3600; // 24 hours
|
||||
|
||||
if (
|
||||
!empty($lastSync) &&
|
||||
strtotime($lastSync) + $syncInterval > time()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$page = 1;
|
||||
$perPage = 100;
|
||||
$totalImported = 0;
|
||||
$errors = [];
|
||||
|
||||
do {
|
||||
$result = $this->makeApiRequest(
|
||||
"catalog?page={$page}&per_page={$perPage}"
|
||||
);
|
||||
|
||||
if (empty($result["data"])) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($result["data"] as $product) {
|
||||
try {
|
||||
if ($this->importSingleProduct($product)) {
|
||||
$totalImported++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errors[] = [
|
||||
"sku" => $product["sku"] ?? "unknown",
|
||||
"error" => $e->getMessage()
|
||||
];
|
||||
}
|
||||
// Clear memory after each product
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
$page++;
|
||||
} while (!empty($result["next_page_url"]));
|
||||
|
||||
Configuration::updateValue(
|
||||
"ECOMZONE_LAST_SYNC",
|
||||
date("Y-m-d H:i:s")
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Cron job execution failed", "ERROR", [
|
||||
"error" => $e->getMessage(),
|
||||
"trace" => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function runCronTasks()
|
||||
public function importSingleProduct(array $productData): bool
|
||||
{
|
||||
try {
|
||||
EcomZoneLogger::log('Starting scheduled product sync');
|
||||
|
||||
$lastRun = Configuration::get('ECOMZONE_LAST_CRON_RUN');
|
||||
$frequency = 3600; // 1 hour in seconds
|
||||
|
||||
if (!empty($lastRun) && (strtotime($lastRun) + $frequency) > time()) {
|
||||
EcomZoneLogger::log('Skipping cron - too soon since last run');
|
||||
return false;
|
||||
}
|
||||
|
||||
$productSync = new EcomZoneProductSync();
|
||||
$result = $productSync->importProducts();
|
||||
|
||||
Configuration::updateValue('ECOMZONE_LAST_CRON_RUN', date('Y-m-d H:i:s'));
|
||||
EcomZoneLogger::log('Scheduled product sync completed', 'INFO', $result);
|
||||
|
||||
return $result;
|
||||
$importer = new EcomZoneProductImport();
|
||||
return $importer->importProduct($productData);
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log('Scheduled product sync failed', 'ERROR', ['error' => $e->getMessage()]);
|
||||
throw $e;
|
||||
EcomZoneLogger::log("Product import failed", "ERROR", [
|
||||
"sku" => $productData['data']['sku'] ?? 'unknown',
|
||||
"error" => $e->getMessage()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function importProductImage(
|
||||
Product $product,
|
||||
string $imageUrl,
|
||||
bool $cover = false
|
||||
): void {
|
||||
// Implementation of the method
|
||||
}
|
||||
|
||||
private function updateBasicInfo(Product $product, array $data): void
|
||||
{
|
||||
try {
|
||||
// Basic information
|
||||
$product->name[$this->defaultLangId] = $data["product_name"];
|
||||
$product->description[$this->defaultLangId] = $data["long_description"] ?? $data["description"];
|
||||
$product->description_short[$this->defaultLangId] = $data["description"];
|
||||
$product->price = (float) $data["product_price"];
|
||||
$product->reference = $data["sku"];
|
||||
$product->ean13 = $data["ean"] ?? "";
|
||||
|
||||
// Shop configuration
|
||||
$product->id_shop_default = $this->defaultShopId;
|
||||
$product->active = true;
|
||||
$product->visibility = "both";
|
||||
$product->available_for_order = true;
|
||||
$product->show_price = true;
|
||||
$product->indexed = 1;
|
||||
|
||||
// Critical: Save the product
|
||||
if (!$product->save()) {
|
||||
throw new EcomZoneException(
|
||||
"Failed to save product",
|
||||
EcomZoneException::INVALID_PRODUCT_DATA
|
||||
);
|
||||
}
|
||||
|
||||
// Add to default shop
|
||||
$product->addToShop($this->defaultShopId);
|
||||
} catch (Exception $e) {
|
||||
throw new EcomZoneException(
|
||||
"Failed to update product info: " . $e->getMessage(),
|
||||
EcomZoneException::INVALID_PRODUCT_DATA,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
ecomzone/index.php
Normal file
8
ecomzone/index.php
Normal 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
ecomzone/interfaces/ICategory.php
Normal file
11
ecomzone/interfaces/ICategory.php
Normal 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
ecomzone/interfaces/IProductSync.php
Normal file
12
ecomzone/interfaces/IProductSync.php
Normal 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;
|
||||
}
|
||||
|
||||
@ -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">
|
||||
<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>
|
||||
|
||||
{* 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-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">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{foreach from=$ECOMZONE_DEBUG_INFO key=key item=value}
|
||||
{if isset($API_PRODUCTS) && $API_PRODUCTS}
|
||||
<div class="table-responsive">
|
||||
<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>
|
||||
{/foreach}
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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">
|
||||
<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}
|
||||
<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>
|
||||
</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 style="font-family: monospace;">{$log|escape:'html':'UTF-8'}</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<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
ecomzone/views/templates/admin/index.php
Normal file
8
ecomzone/views/templates/admin/index.php
Normal 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
ecomzone/views/templates/index.php
Normal file
8
ecomzone/views/templates/index.php
Normal 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
te,p.md
Normal file
27
te,p.md
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user