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 { $page = 1; $totalImported = 0; $totalAvailable = 0; $errors = []; try { EcomZoneLogger::log("Starting product import", "INFO", [ "per_page" => $perPage, ]); do { $catalog = $this->client->getCatalog($page, $perPage); if (!isset($catalog["data"]) || !is_array($catalog["data"])) { throw new EcomZoneException( "Invalid catalog response", EcomZoneException::API_ERROR ); } foreach ($catalog["data"] as $product) { try { if ($this->importSingleProduct($product)) { $totalImported++; } } catch (Exception $e) { $errors[] = [ "sku" => $product["sku"] ?? "unknown", "error" => $e->getMessage(), ]; EcomZoneLogger::log("Product import failed", "ERROR", [ "sku" => $product["sku"] ?? "unknown", "error" => $e->getMessage(), ]); } } $totalAvailable = $catalog["total"] ?? 0; $page++; EcomZoneLogger::log("Page processed", "INFO", [ "page" => $page - 1, "imported" => $totalImported, "total" => $totalAvailable, ]); } while ( isset($catalog["next_page_url"]) && $catalog["next_page_url"] !== null ); $this->clearCache(); return [ "success" => true, "imported" => $totalImported, "total" => $totalAvailable, "errors" => $errors, ]; } catch (Exception $e) { EcomZoneLogger::log("Product import process failed", "ERROR", [ "error" => $e->getMessage(), ]); throw $e; } } public function importSingleProduct(array $productData): bool { try { // Get SKU from catalog data $sku = $productData["sku"] ?? $productData["data"]["sku"] ?? null; if (!$sku) { throw new EcomZoneException("Missing SKU in product data"); } // Fetch complete product data from product endpoint $fullProductData = $this->client->getProduct($sku); if (!isset($fullProductData["data"])) { throw new EcomZoneException("Invalid product response"); } $data = $fullProductData["data"]; EcomZoneLogger::log("Processing product", "INFO", [ "sku" => $sku, "categories" => $data["categories"] ?? null, "google_categories" => $data["google_categories"] ?? null ]); $this->validateProductData($data); // Start transaction Db::getInstance()->execute("START TRANSACTION"); $product = $this->getOrCreateProduct($sku); // Update product data $this->updateProductData($product, $data); $this->updateProductCategories($product, $data); $this->updateProductStock($product, $data); $this->processProductImages($product, $data); // Commit transaction Db::getInstance()->execute("COMMIT"); return true; } catch (Exception $e) { Db::getInstance()->execute("ROLLBACK"); EcomZoneLogger::log("Product import failed", "ERROR", [ "sku" => $sku ?? "unknown", "error" => $e->getMessage() ]); return false; } } private function validateProductData(array $data): void { $requiredFields = [ 'sku', 'product_name', 'product_price', 'description', 'stock' ]; foreach ($requiredFields as $field) { if (empty($data[$field])) { throw new EcomZoneException( "Missing required field: {$field}", EcomZoneException::VALIDATION_ERROR ); } } } private function getOrCreateProduct(string $sku): Product { try { $productId = Product::getIdByReference($sku); if ($productId) { $product = new Product($productId); if (!Validate::isLoadedObject($product)) { throw new Exception("Invalid product object"); } return $product; } $product = new Product(); $product->reference = $sku; return $product; } catch (Exception $e) { throw new EcomZoneException( "Failed to get/create product: " . $e->getMessage(), EcomZoneException::INVALID_PRODUCT_DATA, $e ); } } private function updateProductData(Product $product, array $data): void { try { // Basic information $product->name[$this->defaultLangId] = $data['product_name']; $product->description[$this->defaultLangId] = $data['long_description'] ?? $data['description']; $product->description_short[$this->defaultLangId] = $data['description']; $product->price = (float)$data['product_price']; $product->wholesale_price = (float)($data['recommended_retail_price'] ?? 0); $product->reference = $data['sku']; $product->ean13 = $data['ean'] ?? ''; // Shop configuration $product->id_shop_default = $this->defaultShopId; $product->active = true; $product->visibility = 'both'; $product->available_for_order = true; $product->show_price = true; $product->indexed = 1; // Update dimensions if available if (!empty($data['dimensions'])) { $dimensions = json_decode($data['dimensions'], true); if (is_array($dimensions)) { $product->weight = (float)($dimensions['weight'] ?? 0); $product->height = (float)($dimensions['height'] ?? 0); $product->width = (float)($dimensions['width'] ?? 0); $product->depth = (float)($dimensions['length'] ?? 0); } } // Set tax rules if configured $taxRulesGroupId = (int)Configuration::get('PS_TAX_RULES_GROUP_DEFAULT'); if ($taxRulesGroupId) { $product->id_tax_rules_group = $taxRulesGroupId; } // Save the product if (!$product->id) { if (!$product->add()) { throw new EcomZoneException('Failed to create product'); } } else { if (!$product->update()) { throw new EcomZoneException('Failed to update product'); } } // Update shop association if (Shop::isFeatureActive()) { Shop::setContext(Shop::CONTEXT_SHOP, $this->defaultShopId); $product->id_shop_list = [$this->defaultShopId]; $product->id_shop_default = $this->defaultShopId; // Associate product with shop Db::getInstance()->execute(' INSERT IGNORE INTO '._DB_PREFIX_.'product_shop (id_product, id_shop, price, wholesale_price) VALUES ('.(int)$product->id.', '.(int)$this->defaultShopId.', '.(float)$product->price.', '.(float)$product->wholesale_price.') '); } // Process specifications if available if (!empty($data['specifications'])) { $this->updateProductFeatures($product, $data['specifications']); } } catch (Exception $e) { throw new EcomZoneException( 'Failed to update product data: ' . $e->getMessage(), EcomZoneException::INVALID_PRODUCT_DATA, $e ); } } private function updateProductCategories(Product $product, array $data): void { try { $categoryIds = []; // Process main categories if (!empty($data['categories'])) { $categories = json_decode($data['categories'], true); if (is_array($categories)) { foreach ($categories as $categoryPath) { // Skip special categories that start with '0' if (strpos($categoryPath, '0') === 0) { continue; } // Create each category as a single path $categoryId = $this->categoryHandler->createCategoryPath([$categoryPath]); if ($categoryId) { $categoryIds[] = $categoryId; } } } } // Process Google categories if (!empty($data['google_categories'])) { $googleCategories = json_decode($data['google_categories'], true); if (is_array($googleCategories)) { $categoryId = $this->categoryHandler->createCategoryPath($googleCategories); if ($categoryId) { $categoryIds[] = $categoryId; } } } // Ensure we have at least the default category if (empty($categoryIds)) { $categoryIds[] = (int)Configuration::get('PS_HOME_CATEGORY'); } // Remove duplicates $categoryIds = array_unique($categoryIds); // Set default category $product->id_category_default = reset($categoryIds); // Save the product to ensure it has an ID if (!$product->id) { $product->add(); } else { $product->update(); } // Clear existing category associations Db::getInstance()->execute(' DELETE FROM `'._DB_PREFIX_.'category_product` WHERE id_product = '.(int)$product->id ); // Insert new category associations foreach ($categoryIds as $categoryId) { Db::getInstance()->execute(' INSERT INTO `'._DB_PREFIX_.'category_product` (id_category, id_product, position) VALUES ('.(int)$categoryId.', '.(int)$product->id.', 0) '); } // Update shop associations if needed if (Shop::isFeatureActive()) { $product->id_shop_list = [$this->defaultShopId]; $product->id_shop_default = $this->defaultShopId; } EcomZoneLogger::log('Categories updated', 'DEBUG', [ 'product' => $product->reference, 'category_ids' => $categoryIds ]); } catch (Exception $e) { throw new EcomZoneException( 'Failed to update product categories: ' . $e->getMessage(), EcomZoneException::CATEGORY_CREATE_ERROR, $e ); } } private function updateProductStock(Product $product, array $data): void { try { $quantity = (int) ($data['stock_quantity'] ?? 0); StockAvailable::setQuantity($product->id, 0, $quantity); // Update out of stock behavior StockAvailable::setProductOutOfStock( $product->id, $quantity > 0 ? 2 : 0, // 2 = Allow orders, 0 = Deny orders $this->defaultShopId ); } catch (Exception $e) { EcomZoneLogger::log("Failed to update stock", "WARNING", [ "product" => $product->reference, "error" => $e->getMessage() ]); } } private function processProductImages(Product $product, array $data): void { try { $imageUrls = []; // Process main image if (!empty($data["image"])) { $imageUrls[] = $data["image"]; } // Process additional images if (!empty($data["images"])) { $additionalImages = json_decode($data["images"], true); if (is_array($additionalImages)) { $imageUrls = array_merge($imageUrls, $additionalImages); } } if (empty($imageUrls)) { return; } // Delete existing images if any $product->deleteImages(); foreach ($imageUrls as $index => $imageUrl) { $this->importProductImage($product, $imageUrl, $index === 0); } } catch (Exception $e) { EcomZoneLogger::log("Failed to process product images", "WARNING", [ "sku" => $product->reference, "error" => $e->getMessage(), ]); } } private function importProductImage(Product $product, string $imageUrl, bool $cover = false): void { try { // Create temporary file $tempFile = tempnam(_PS_TMP_IMG_DIR_, 'import_'); // Download image if (!copy($imageUrl, $tempFile)) { throw new Exception("Failed to download image: " . $imageUrl); } // Create image object $image = new Image(); $image->id_product = $product->id; $image->position = Image::getHighestPosition($product->id) + 1; $image->cover = $cover; if (!$image->add()) { throw new Exception("Failed to create image record"); } // Create product image directory if it doesn't exist $imagePath = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath(); $imageDir = dirname($imagePath); if (!file_exists($imageDir)) { mkdir($imageDir, 0755, true); } // Generate image if (!ImageManager::resize( $tempFile, $imagePath . '.jpg' )) { throw new Exception("Failed to process image"); } // Generate thumbnails $this->generateThumbnails($image); // Clean up unlink($tempFile); } catch (Exception $e) { EcomZoneLogger::log("Failed to import product image", "ERROR", [ "product" => $product->reference, "image_url" => $imageUrl, "error" => $e->getMessage() ]); } } private function generateThumbnails(Image $image): void { try { $imageTypes = ImageType::getImagesTypes('products'); foreach ($imageTypes as $imageType) { $dir = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath(); if (!file_exists($dir)) { mkdir($dir, 0755, true); } ImageManager::resize( _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg', _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg', (int)$imageType['width'], (int)$imageType['height'] ); } } catch (Exception $e) { EcomZoneLogger::log("Failed to generate thumbnails", "WARNING", [ "image_id" => $image->id, "error" => $e->getMessage() ]); } } private function generateImageType( string $srcPath, string $destPath, ?int $width, ?int $height ): void { try { $this->createDirectoryIfNotExists(dirname($destPath)); if ($width === null || $height === null) { copy($srcPath, $destPath); return; } $imageInfo = getimagesize($srcPath); if (!$imageInfo) { throw new Exception("Invalid image file"); } $srcImage = $this->createImageFromType($srcPath, $imageInfo[2]); $destImage = imagecreatetruecolor($width, $height); // 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: return imagecreatefromjpeg($filename); case IMAGETYPE_PNG: return imagecreatefrompng($filename); case IMAGETYPE_GIF: return imagecreatefromgif($filename); default: throw new Exception("Unsupported image type: " . $type); } } private function createDirectoryIfNotExists(string $directory): void { if (!is_dir($directory)) { if (!mkdir($directory, 0755, true)) { throw new Exception( "Failed to create directory: " . $directory ); } } } 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(); } } $token = Configuration::get('ECOMZONE_CRON_TOKEN'); echo $token;