848 lines
30 KiB
PHP
848 lines
30 KiB
PHP
<?php
|
|
if (!defined("_PS_VERSION_")) {
|
|
exit();
|
|
}
|
|
|
|
require_once dirname(__FILE__) . '/interfaces/IProductSync.php';
|
|
|
|
class EcomZoneProductSync implements IProductSync
|
|
{
|
|
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");
|
|
}
|
|
|
|
public function importProducts(int $perPage = 100): array
|
|
{
|
|
try {
|
|
// Get or initialize sync state
|
|
$syncState = $this->getSyncState();
|
|
|
|
// If this is a new sync (page 1 and no imports), reset everything
|
|
if (empty($syncState) || ($syncState['current_page'] ?? 1) === 1 && ($syncState['total_imported'] ?? 0) === 0) {
|
|
$this->resetSyncState();
|
|
$syncState = [];
|
|
}
|
|
|
|
$page = $syncState['current_page'] ?? 1;
|
|
$totalImported = $syncState['total_imported'] ?? 0;
|
|
$totalAvailable = $syncState['total_available'] ?? 0;
|
|
$errors = $syncState['errors'] ?? [];
|
|
$maxProductsPerBatch = 20; // Process 20 products at a time to avoid timeout
|
|
|
|
EcomZoneLogger::log("Starting/Resuming product import", "INFO", [
|
|
"page" => $page,
|
|
"per_page" => $perPage,
|
|
"total_imported" => $totalImported,
|
|
"is_new_sync" => empty($syncState)
|
|
]);
|
|
|
|
// Get catalog for current page
|
|
$catalog = $this->client->getCatalog($page, $perPage);
|
|
|
|
if (!isset($catalog["data"]) || !is_array($catalog["data"])) {
|
|
throw new EcomZoneException(
|
|
"Invalid catalog response",
|
|
EcomZoneException::API_ERROR
|
|
);
|
|
}
|
|
|
|
// Update total available on first page or if it changes
|
|
if ($page === 1 || ($catalog["total"] ?? 0) !== $totalAvailable) {
|
|
$totalAvailable = $catalog["total"] ?? 0;
|
|
}
|
|
|
|
$productsProcessed = 0;
|
|
$batchErrors = [];
|
|
|
|
// Process only a batch of products
|
|
foreach ($catalog["data"] as $product) {
|
|
if ($productsProcessed >= $maxProductsPerBatch) {
|
|
break;
|
|
}
|
|
|
|
try {
|
|
if ($this->importSingleProduct($product)) {
|
|
$totalImported++;
|
|
}
|
|
} catch (Exception $e) {
|
|
$batchErrors[] = [
|
|
"sku" => $product["sku"] ?? "unknown",
|
|
"error" => $e->getMessage(),
|
|
];
|
|
EcomZoneLogger::log("Product import failed", "ERROR", [
|
|
"sku" => $product["sku"] ?? "unknown",
|
|
"error" => $e->getMessage(),
|
|
]);
|
|
}
|
|
$productsProcessed++;
|
|
}
|
|
|
|
// Update errors array with new batch errors
|
|
$errors = array_merge($errors, $batchErrors);
|
|
|
|
// Calculate progress
|
|
$progress = [
|
|
'current_page' => $page,
|
|
'total_imported' => $totalImported,
|
|
'total_available' => $totalAvailable,
|
|
'errors' => $errors,
|
|
'is_complete' => false,
|
|
'products_processed_on_page' => $productsProcessed
|
|
];
|
|
|
|
// Check if we need to continue to next page
|
|
if ($productsProcessed < count($catalog["data"])) {
|
|
// Still more products to process on this page
|
|
$progress['products_processed_on_page'] = $productsProcessed;
|
|
} else {
|
|
// Move to next page if available
|
|
if (isset($catalog["next_page_url"]) && $catalog["next_page_url"] !== null) {
|
|
$progress['current_page'] = $page + 1;
|
|
$progress['products_processed_on_page'] = 0;
|
|
} else {
|
|
$progress['is_complete'] = true;
|
|
}
|
|
}
|
|
|
|
// Save progress
|
|
$this->saveSyncState($progress);
|
|
|
|
// Clear cache if complete
|
|
if ($progress['is_complete']) {
|
|
$this->clearCache();
|
|
}
|
|
|
|
return [
|
|
"success" => true,
|
|
"imported" => $totalImported,
|
|
"total" => $totalAvailable,
|
|
"errors" => $errors,
|
|
"is_complete" => $progress['is_complete'],
|
|
"current_page" => $progress['current_page']
|
|
];
|
|
} catch (Exception $e) {
|
|
EcomZoneLogger::log("Product import process failed", "ERROR", [
|
|
"error" => $e->getMessage(),
|
|
]);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
public function importSingleProduct(array $productData): bool
|
|
{
|
|
try {
|
|
// Get SKU from catalog data
|
|
$sku = $productData["sku"] ?? $productData["data"]["sku"] ?? null;
|
|
|
|
if (!$sku) {
|
|
throw new EcomZoneException("Missing SKU in product data");
|
|
}
|
|
|
|
// Fetch complete product data from product endpoint
|
|
$fullProductData = $this->client->getProduct($sku);
|
|
|
|
if (!isset($fullProductData["data"])) {
|
|
throw new EcomZoneException("Invalid product response");
|
|
}
|
|
|
|
$data = $fullProductData["data"];
|
|
|
|
EcomZoneLogger::log("Processing product", "INFO", [
|
|
"sku" => $sku,
|
|
"categories" => $data["categories"] ?? null,
|
|
"google_categories" => $data["google_categories"] ?? null
|
|
]);
|
|
|
|
$this->validateProductData($data);
|
|
|
|
// Start transaction
|
|
Db::getInstance()->execute("START TRANSACTION");
|
|
|
|
$product = $this->getOrCreateProduct($sku);
|
|
|
|
// Update product data
|
|
$this->updateProductData($product, $data);
|
|
$this->updateProductCategories($product, $data);
|
|
$this->updateProductStock($product, $data);
|
|
$this->processProductImages($product, $data);
|
|
|
|
// Commit transaction
|
|
Db::getInstance()->execute("COMMIT");
|
|
|
|
return true;
|
|
} catch (Exception $e) {
|
|
Db::getInstance()->execute("ROLLBACK");
|
|
EcomZoneLogger::log("Product import failed", "ERROR", [
|
|
"sku" => $sku ?? "unknown",
|
|
"error" => $e->getMessage()
|
|
]);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function validateProductData(array $data): void
|
|
{
|
|
$requiredFields = [
|
|
'sku',
|
|
'product_name',
|
|
'product_price',
|
|
'description',
|
|
'stock'
|
|
];
|
|
|
|
foreach ($requiredFields as $field) {
|
|
if (empty($data[$field])) {
|
|
throw new EcomZoneException(
|
|
"Missing required field: {$field}",
|
|
EcomZoneException::VALIDATION_ERROR
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function getOrCreateProduct(string $sku): Product
|
|
{
|
|
try {
|
|
$productId = Product::getIdByReference($sku);
|
|
|
|
if ($productId) {
|
|
$product = new Product($productId);
|
|
if (!Validate::isLoadedObject($product)) {
|
|
throw new Exception("Invalid product object");
|
|
}
|
|
return $product;
|
|
}
|
|
|
|
$product = new Product();
|
|
$product->reference = $sku;
|
|
return $product;
|
|
} catch (Exception $e) {
|
|
throw new EcomZoneException(
|
|
"Failed to get/create product: " . $e->getMessage(),
|
|
EcomZoneException::INVALID_PRODUCT_DATA,
|
|
$e
|
|
);
|
|
}
|
|
}
|
|
|
|
private function updateProductData(Product $product, array $data): void
|
|
{
|
|
try {
|
|
// Basic information
|
|
$product->name[$this->defaultLangId] = $data['product_name'];
|
|
$product->description[$this->defaultLangId] = $data['long_description'] ?? $data['description'];
|
|
$product->description_short[$this->defaultLangId] = $data['description'];
|
|
|
|
// Update prices
|
|
$product->wholesale_price = (float)$data['product_price']; // Original price as wholesale price
|
|
$product->price = (float)($data['recommended_retail_price'] ?? $data['product_price']); // Recommended retail price for shop display
|
|
|
|
$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'] ?? 0);
|
|
|
|
// Make sure product is saved first
|
|
if (!$product->id) {
|
|
$product->save();
|
|
}
|
|
|
|
// Update stock quantity
|
|
StockAvailable::setQuantity(
|
|
$product->id,
|
|
0, // Combination ID (0 for products without combinations)
|
|
$quantity
|
|
);
|
|
|
|
// Update out of stock behavior
|
|
StockAvailable::setProductOutOfStock(
|
|
$product->id,
|
|
2, // Always allow orders
|
|
$this->defaultShopId
|
|
);
|
|
|
|
EcomZoneLogger::log("Stock updated", "DEBUG", [
|
|
"product" => $product->reference,
|
|
"quantity" => $quantity
|
|
]);
|
|
|
|
} 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 = [];
|
|
|
|
EcomZoneLogger::log("Raw product data for images", "DEBUG", [
|
|
"product" => $product->reference,
|
|
"data" => json_encode($data)
|
|
]);
|
|
|
|
// Handle different possible data structures
|
|
if (isset($data['images']) && is_string($data['images'])) {
|
|
$images = json_decode($data['images'], true);
|
|
if (json_last_error() === JSON_ERROR_NONE && is_array($images)) {
|
|
$imageUrls = array_merge($imageUrls, $images);
|
|
}
|
|
} elseif (isset($data['images']) && is_array($data['images'])) {
|
|
$imageUrls = array_merge($imageUrls, $data['images']);
|
|
}
|
|
|
|
// Add main image if available
|
|
if (!empty($data['image'])) {
|
|
array_unshift($imageUrls, $data['image']); // Add as first image
|
|
}
|
|
|
|
// Filter out empty or invalid URLs
|
|
$imageUrls = array_filter($imageUrls, function($url) {
|
|
return !empty($url) && filter_var($url, FILTER_VALIDATE_URL);
|
|
});
|
|
|
|
if (empty($imageUrls)) {
|
|
EcomZoneLogger::log("No valid images found for product", "WARNING", [
|
|
"product" => $product->reference,
|
|
"data" => json_encode($data)
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// Delete existing images if any
|
|
$product->deleteImages();
|
|
|
|
$successCount = 0;
|
|
foreach ($imageUrls as $index => $imageUrl) {
|
|
try {
|
|
EcomZoneLogger::log("Processing image", "DEBUG", [
|
|
"product" => $product->reference,
|
|
"image_url" => $imageUrl,
|
|
"index" => $index
|
|
]);
|
|
|
|
$this->importProductImage($product, $imageUrl, $index === 0);
|
|
$successCount++;
|
|
} catch (Exception $e) {
|
|
EcomZoneLogger::log("Failed to process image", "WARNING", [
|
|
"product" => $product->reference,
|
|
"image_url" => $imageUrl,
|
|
"error" => $e->getMessage()
|
|
]);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ($successCount === 0 && !empty($imageUrls)) {
|
|
throw new Exception(sprintf(
|
|
"Failed to process any images for the product. Attempted %d images.",
|
|
count($imageUrls)
|
|
));
|
|
}
|
|
|
|
EcomZoneLogger::log("Processed product images", "INFO", [
|
|
"product" => $product->reference,
|
|
"total_images" => count($imageUrls),
|
|
"successful_imports" => $successCount
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
EcomZoneLogger::log("Failed to process product images", "ERROR", [
|
|
"sku" => $product->reference,
|
|
"error" => $e->getMessage(),
|
|
"data" => json_encode($data)
|
|
]);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
private function importProductImage(Product $product, string $imageUrl, bool $cover = false): void
|
|
{
|
|
try {
|
|
// Create temporary directory if it doesn't exist
|
|
$tempDir = _PS_MODULE_DIR_ . 'ecomzone/tmp/';
|
|
if (!is_dir($tempDir)) {
|
|
mkdir($tempDir, 0755, true);
|
|
chmod($tempDir, 0755);
|
|
}
|
|
|
|
// Clean URL - handle double escaped slashes
|
|
$cleanUrl = stripslashes(stripslashes(trim($imageUrl)));
|
|
|
|
EcomZoneLogger::log("Processing image URL", "DEBUG", [
|
|
"product" => $product->reference,
|
|
"original_url" => $imageUrl,
|
|
"cleaned_url" => $cleanUrl
|
|
]);
|
|
|
|
// Create temp file
|
|
$tempFile = tempnam($tempDir, 'import_');
|
|
if ($tempFile) {
|
|
chmod($tempFile, 0644);
|
|
}
|
|
|
|
// Use the client class to download the image
|
|
$imageResult = $this->client->downloadImage($cleanUrl);
|
|
$imageData = $imageResult['data'];
|
|
$contentType = $imageResult['content_type'];
|
|
|
|
// Save image data to temp file
|
|
file_put_contents($tempFile, $imageData);
|
|
|
|
// Verify the downloaded file is a valid image
|
|
$imageInfo = @getimagesize($tempFile);
|
|
if (!$imageInfo) {
|
|
throw new Exception("Downloaded file is not a valid image. Content type: $contentType");
|
|
}
|
|
|
|
// 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)) {
|
|
if (!mkdir($imageDir, 0755, true)) {
|
|
throw new Exception("Failed to create image directory: " . $imageDir);
|
|
}
|
|
chmod($imageDir, 0755);
|
|
}
|
|
|
|
// Generate image
|
|
if (!ImageManager::resize(
|
|
$tempFile,
|
|
$imagePath . '.jpg',
|
|
null,
|
|
null,
|
|
'jpg'
|
|
)) {
|
|
throw new Exception("Failed to process image");
|
|
}
|
|
|
|
// Ensure the generated image has correct permissions
|
|
chmod($imagePath . '.jpg', 0644);
|
|
|
|
// Generate thumbnails
|
|
$this->generateThumbnails($image);
|
|
|
|
// Clean up
|
|
if (file_exists($tempFile)) {
|
|
unlink($tempFile);
|
|
}
|
|
|
|
EcomZoneLogger::log("Image imported successfully", "DEBUG", [
|
|
"product" => $product->reference,
|
|
"image_url" => $cleanUrl,
|
|
"mime_type" => $imageInfo['mime']
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
EcomZoneLogger::log("Failed to import product image", "ERROR", [
|
|
"product" => $product->reference,
|
|
"image_url" => $imageUrl,
|
|
"clean_url" => $cleanUrl ?? '',
|
|
"error" => $e->getMessage(),
|
|
"content_type" => $contentType ?? null
|
|
]);
|
|
|
|
// Clean up on error
|
|
if (isset($tempFile) && file_exists($tempFile)) {
|
|
unlink($tempFile);
|
|
}
|
|
|
|
// Delete image record if it was created
|
|
if (isset($image) && $image->id) {
|
|
$image->delete();
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
private function generateThumbnails(Image $image): void
|
|
{
|
|
try {
|
|
$imageTypes = ImageType::getImagesTypes('products');
|
|
$sourceFile = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg';
|
|
|
|
if (!file_exists($sourceFile)) {
|
|
throw new Exception("Source image file not found");
|
|
}
|
|
|
|
foreach ($imageTypes as $imageType) {
|
|
$destination = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg';
|
|
$dir = dirname($destination);
|
|
|
|
if (!file_exists($dir)) {
|
|
if (!mkdir($dir, 0755, true)) {
|
|
throw new Exception("Failed to create thumbnail directory: " . $dir);
|
|
}
|
|
chmod($dir, 0755);
|
|
}
|
|
|
|
if (!ImageManager::resize(
|
|
$sourceFile,
|
|
$destination,
|
|
(int)$imageType['width'],
|
|
(int)$imageType['height']
|
|
)) {
|
|
throw new Exception("Failed to generate thumbnail: " . $imageType['name']);
|
|
}
|
|
|
|
// Set proper permissions for the thumbnail
|
|
chmod($destination, 0644);
|
|
}
|
|
} catch (Exception $e) {
|
|
EcomZoneLogger::log("Failed to generate thumbnails", "WARNING", [
|
|
"image_id" => $image->id,
|
|
"error" => $e->getMessage()
|
|
]);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
private function updateProductFeatures(Product $product, string $specificationsJson): void
|
|
{
|
|
try {
|
|
$specifications = json_decode($specificationsJson, true);
|
|
if (!is_array($specifications)) {
|
|
return;
|
|
}
|
|
|
|
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
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
EcomZoneLogger::log(
|
|
"Failed to update product features",
|
|
"WARNING",
|
|
[
|
|
"product" => $product->reference,
|
|
"error" => $e->getMessage(),
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
private function getOrCreateFeature(string $name): ?int
|
|
{
|
|
// Try to find existing feature
|
|
$featureId = (int)Db::getInstance()->getValue(
|
|
'SELECT f.id_feature
|
|
FROM `'._DB_PREFIX_.'feature_lang` fl
|
|
JOIN `'._DB_PREFIX_.'feature` f ON f.id_feature = fl.id_feature
|
|
WHERE fl.name = "'.pSQL($name).'"
|
|
AND fl.id_lang = '.(int)$this->defaultLangId
|
|
);
|
|
|
|
if ($featureId) {
|
|
return $featureId;
|
|
}
|
|
|
|
// Create new feature
|
|
$feature = new Feature();
|
|
$feature->name = array_fill_keys(Language::getIDs(), $name);
|
|
|
|
return $feature->add() ? (int)$feature->id : null;
|
|
}
|
|
|
|
private function getOrCreateFeatureValue(int $featureId, string $value): ?int
|
|
{
|
|
// Try to find existing feature value
|
|
$valueId = (int)Db::getInstance()->getValue(
|
|
'SELECT fv.id_feature_value
|
|
FROM `'._DB_PREFIX_.'feature_value_lang` fvl
|
|
JOIN `'._DB_PREFIX_.'feature_value` fv ON fv.id_feature_value = fvl.id_feature_value
|
|
WHERE fv.id_feature = '.(int)$featureId.'
|
|
AND fvl.value = "'.pSQL($value).'"
|
|
AND fvl.id_lang = '.(int)$this->defaultLangId
|
|
);
|
|
|
|
if ($valueId) {
|
|
return $valueId;
|
|
}
|
|
|
|
// Create new feature value
|
|
$featureValue = new FeatureValue();
|
|
$featureValue->id_feature = $featureId;
|
|
$featureValue->value = array_fill_keys(Language::getIDs(), $value);
|
|
|
|
return $featureValue->add() ? (int)$featureValue->id : null;
|
|
}
|
|
|
|
private function updateProductDimensions(
|
|
Product $product,
|
|
string $dimensionsJson
|
|
): void {
|
|
try {
|
|
$dimensions = json_decode($dimensionsJson, true);
|
|
if (!is_array($dimensions)) {
|
|
return;
|
|
}
|
|
|
|
$product->weight = (float) ($dimensions["weight"] ?? 0);
|
|
$product->height = (float) ($dimensions["height"] ?? 0);
|
|
$product->width = (float) ($dimensions["width"] ?? 0);
|
|
$product->depth = (float) ($dimensions["length"] ?? 0);
|
|
|
|
$product->update();
|
|
} catch (Exception $e) {
|
|
EcomZoneLogger::log(
|
|
"Failed to update product dimensions",
|
|
"WARNING",
|
|
[
|
|
"product" => $product->reference,
|
|
"error" => $e->getMessage(),
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
private function clearCache(): void
|
|
{
|
|
Tools::clearAllCache();
|
|
Tools::clearSmartyCache();
|
|
Tools::clearXMLCache();
|
|
Media::clearCache();
|
|
PrestaShopAutoload::getInstance()->generateIndex();
|
|
}
|
|
|
|
private function getSyncState(): array
|
|
{
|
|
$state = Configuration::get('ECOMZONE_SYNC_STATE');
|
|
return $state ? json_decode($state, true) : [];
|
|
}
|
|
|
|
private function saveSyncState(array $state): void
|
|
{
|
|
Configuration::updateValue('ECOMZONE_SYNC_STATE', json_encode($state));
|
|
}
|
|
|
|
public function resetSyncState(): void
|
|
{
|
|
// Delete both sync state and progress
|
|
Configuration::deleteByName('ECOMZONE_SYNC_STATE');
|
|
Configuration::deleteByName('ECOMZONE_SYNC_PROGRESS');
|
|
Configuration::deleteByName('ECOMZONE_LAST_SYNC');
|
|
|
|
EcomZoneLogger::log("Sync state reset", "INFO");
|
|
}
|
|
}
|
|
|
|
$token = Configuration::get('ECOMZONE_CRON_TOKEN');
|
|
echo $token;
|
|
|