category -> product association implemented
This commit is contained in:
1 parent
0b8c5de8b0
commit
c8bcfe6bd6
41 files changed
+2222
-703
No files matched your search
+286
-160
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user