From 0b8c5de8b094d8daccacbdc7d71289fc90e8d119 Mon Sep 17 00:00:00 2001 From: dimitar Date: Tue, 4 Feb 2025 18:26:21 +0100 Subject: [PATCH] backup --- ecomzone-v5.1.zip | Bin 0 -> 10838 bytes ecomzone/classes/EcomZoneClient.php | 107 ++++++++ ecomzone/classes/EcomZoneCronTask.php | 40 +++ ecomzone/classes/EcomZoneLogger.php | 25 ++ ecomzone/classes/EcomZoneOrderSync.php | 81 ++++++ ecomzone/classes/EcomZoneProductSync.php | 271 +++++++++++++++++++ ecomzone/cron.php | 43 +++ ecomzone/ecomzone.php | 219 +++++++++++++++ ecomzone/views/templates/admin/configure.tpl | 90 ++++++ 9 files changed, 876 insertions(+) create mode 100644 ecomzone-v5.1.zip create mode 100644 ecomzone/classes/EcomZoneClient.php create mode 100644 ecomzone/classes/EcomZoneCronTask.php create mode 100644 ecomzone/classes/EcomZoneLogger.php create mode 100644 ecomzone/classes/EcomZoneOrderSync.php create mode 100644 ecomzone/classes/EcomZoneProductSync.php create mode 100644 ecomzone/cron.php create mode 100644 ecomzone/ecomzone.php create mode 100644 ecomzone/views/templates/admin/configure.tpl diff --git a/ecomzone-v5.1.zip b/ecomzone-v5.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..a3e0d61052fd42d65de90e8fddabadce414f24d2 GIT binary patch literal 10838 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 H3;6#4O1m7+ literal 0 HcmV?d00001 diff --git a/ecomzone/classes/EcomZoneClient.php b/ecomzone/classes/EcomZoneClient.php new file mode 100644 index 0000000..fe3d14f --- /dev/null +++ b/ecomzone/classes/EcomZoneClient.php @@ -0,0 +1,107 @@ +apiToken = 'klRyAdrXaxL0s6PEUp7LDlH6T8aPSCtBY8NiEHsHiWpc6646K2TZPi5KMxUg'; + $this->apiUrl = Configuration::get('ECOMZONE_API_URL'); + + if (empty($this->apiUrl)) { + throw new Exception('API URL not configured'); + } + } + + 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); + } + + public function getProduct($sku) + { + return $this->makeRequest('GET', $this->apiUrl . '/product/' . $sku); + } + + 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) + { + $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); + } +} diff --git a/ecomzone/classes/EcomZoneCronTask.php b/ecomzone/classes/EcomZoneCronTask.php new file mode 100644 index 0000000..212c893 --- /dev/null +++ b/ecomzone/classes/EcomZoneCronTask.php @@ -0,0 +1,40 @@ +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; + } + } +} diff --git a/ecomzone/classes/EcomZoneLogger.php b/ecomzone/classes/EcomZoneLogger.php new file mode 100644 index 0000000..67e6db9 --- /dev/null +++ b/ecomzone/classes/EcomZoneLogger.php @@ -0,0 +1,25 @@ +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; + } +} \ No newline at end of file diff --git a/ecomzone/classes/EcomZoneProductSync.php b/ecomzone/classes/EcomZoneProductSync.php new file mode 100644 index 0000000..73b637d --- /dev/null +++ b/ecomzone/classes/EcomZoneProductSync.php @@ -0,0 +1,271 @@ +client = new EcomZoneClient(); + } + + private function resizeImage($src, $dest, $width, $height) + { + 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); + } + + private function createImageFromType($filename, $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 saveImageFromType($image, $filename, $type) + { + 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(); + } +StockAvailable::setQuantity($product->id, 0, (int)$data['stock']); +$product->available_for_order = true; // Ensure product is available for order +$product->show_price = true; // Ensure price is shown + + // Handle image import if URL is provided + if (isset($data['image']) && !empty($data['image'])) { + $this->importProductImage($product, $data['image']); + } + + StockAvailable::setQuantity($product->id, 0, (int)$data['stock']); + + EcomZoneLogger::log("Imported product", 'INFO', [ + 'sku' => $data['sku'], + 'id' => $product->id, + 'name' => $data['product_name'] + ]); + + return true; + + } catch (Exception $e) { + EcomZoneLogger::log("Error importing product", 'ERROR', [ + 'sku' => $data['sku'], + 'error' => $e->getMessage() + ]); + return false; + } + } + + + +private function importProductImage($product, $imageUrl) +{ + try { + // Create temporary file + $tmpFile = tempnam(_PS_TMP_IMG_DIR_, 'ecomzone_'); + + // Download image + if (!copy($imageUrl, $tmpFile)) { + throw new Exception("Failed to download image from: " . $imageUrl); + } + + // Get image info + $imageInfo = getimagesize($tmpFile); + if (!$imageInfo) { + unlink($tmpFile); + throw new Exception("Invalid image file"); + } + + // Validate image dimensions and file size + if ($imageInfo[0] > 2000 || $imageInfo[1] > 2000 || filesize($tmpFile) > 5000000) { + unlink($tmpFile); + throw new Exception("Image dimensions or file size exceed limits"); + } + + // Generate unique name + $imageName = $product->reference . '-' . time() . '.' . pathinfo($imageUrl, PATHINFO_EXTENSION); + + // Delete existing images if any + $product->deleteImages(); + + // Add new image + $image = new Image(); + $image->id_product = $product->id; + $image->position = 1; + $image->cover = true; + + // Save the image to the correct directory + $imagePath = _PS_PROD_IMG_DIR_ . $image->getImgPath() . '.' . $image->image_format; + $this->createDirectoryIfNotExists(dirname($imagePath)); + if (!copy($tmpFile, $imagePath)) { + unlink($tmpFile); + throw new Exception("Failed to save image to: " . $imagePath); + } + + // Associate the image with the product + if (!$image->add()) { + unlink($tmpFile); + throw new Exception("Failed to add image to product"); + } + + // Manually resize the image and generate thumbnails + $this->resizeImage($imagePath, _PS_PROD_IMG_DIR_ . $image->getImgPath() . '-home_default.' . $image->image_format, 250, 250); + $this->resizeImage($imagePath, _PS_PROD_IMG_DIR_ . $image->getImgPath() . '-large_default.' . $image->image_format, 800, 800); + + // Cleanup + unlink($tmpFile); + EcomZoneLogger::log("Imported product image", 'INFO', [ + 'sku' => $product->reference, + 'image' => $imageUrl + ]); + + } catch (Exception $e) { + EcomZoneLogger::log("Error importing product image", 'ERROR', [ + 'sku' => $product->reference, + 'image' => $imageUrl, + 'error' => $e->getMessage() + ]); + } +} + +private function createDirectoryIfNotExists($directory) +{ + if (!is_dir($directory)) { + mkdir($directory, 0755, true); + } +} +} diff --git a/ecomzone/cron.php b/ecomzone/cron.php new file mode 100644 index 0000000..fc8a4fa --- /dev/null +++ b/ecomzone/cron.php @@ -0,0 +1,43 @@ +runCronTasks(); + echo json_encode(['success' => true, 'result' => $result]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} \ No newline at end of file diff --git a/ecomzone/ecomzone.php b/ecomzone/ecomzone.php new file mode 100644 index 0000000..e67cb82 --- /dev/null +++ b/ecomzone/ecomzone.php @@ -0,0 +1,219 @@ +name = 'ecomzone'; + $this->tab = 'market_place'; + $this->version = '1.0.0'; + $this->author = 'Your Name'; + $this->need_instance = 0; + $this->bootstrap = true; + + 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; + } + } + + 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())); + } + + private function createLogFile() + { + if (!file_exists(dirname(EcomZoneLogger::LOG_FILE))) { + mkdir(dirname(EcomZoneLogger::LOG_FILE), 0755, true); + } + + if (!file_exists(EcomZoneLogger::LOG_FILE)) { + touch(EcomZoneLogger::LOG_FILE); + chmod(EcomZoneLogger::LOG_FILE, 0666); + } + + 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'); + } + + 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('importPoducts')) { + try { + $productSync = new EcomZoneProductSync(); + $result = $productSync->importProducts(); + $output .= $this->displayConfirmation( + sprintf($this->l('Imported %d products'), $result['imported']) + ); + } 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 + ]); + + return $output . $this->display(__FILE__, 'views/templates/admin/configure.tpl'); + } + + private function getDebugInfo() + { + $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'); + + 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) + ]; + } + + private function getRecentLogs($lines = 50) + { + if (!file_exists(EcomZoneLogger::LOG_FILE)) { + return []; + } + + $logs = array_slice(file(EcomZoneLogger::LOG_FILE), -$lines); + return array_map('trim', $logs); + } + + public function hookActionOrderStatusUpdate($params) + { + $order = $params['order']; + $newOrderStatus = $params['newOrderStatus']; + + // Sync order when it's paid + if ($newOrderStatus->paid == 1) { + try { + $orderSync = new EcomZoneOrderSync(); + $result = $orderSync->syncOrder($order->id); + + // Log the result + PrestaShopLogger::addLog( + 'EcomZone order sync: ' . json_encode($result), + 1, + null, + 'Order', + $order->id, + true + ); + } catch (Exception $e) { + PrestaShopLogger::addLog( + 'EcomZone order sync error: ' . $e->getMessage(), + 3, + null, + 'Order', + $order->id, + true + ); + } + } + } + + 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(); + } + + return true; + } + + public function runCronTasks() + { + 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; + } catch (Exception $e) { + EcomZoneLogger::log('Scheduled product sync failed', 'ERROR', ['error' => $e->getMessage()]); + throw $e; + } + } +} diff --git a/ecomzone/views/templates/admin/configure.tpl b/ecomzone/views/templates/admin/configure.tpl new file mode 100644 index 0000000..fc907eb --- /dev/null +++ b/ecomzone/views/templates/admin/configure.tpl @@ -0,0 +1,90 @@ +
+
+
+ {l s='EcomZone Configuration' mod='ecomzone'} +
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+ {l s='Manual Actions' mod='ecomzone'} +
+
+
+
+
+ +
+
+
+
+
+ +
+
+ {l s='Cron Setup Instructions' mod='ecomzone'} +
+
+

{l s='Add the following command to your server crontab to run every hour:' mod='ecomzone'}

+
0 * * * * curl -s "{$shop_url}modules/ecomzone/cron.php?token={$ECOMZONE_CRON_TOKEN}"
+

{l s='Or using PHP CLI:' mod='ecomzone'}

+
0 * * * * php {$shop_root}modules/ecomzone/cron.php --token={$ECOMZONE_CRON_TOKEN}
+
+
+ +
+
+ {l s='Debug Information' mod='ecomzone'} +
+
+
+ + + {foreach from=$ECOMZONE_DEBUG_INFO key=key item=value} + + + + + {/foreach} + + + + + + + + + +
{$key|escape:'html':'UTF-8'}{$value|escape:'html':'UTF-8'}
{l s='Last Cron Run' mod='ecomzone'}{$ECOMZONE_LAST_CRON_RUN|escape:'html':'UTF-8'}
{l s='Next Scheduled Run' mod='ecomzone'}{$ECOMZONE_NEXT_CRON_RUN|escape:'html':'UTF-8'}
+
+
+
+ +
+
+ {l s='Recent Logs' mod='ecomzone'} +
+
+
+ {foreach from=$ECOMZONE_LOGS item=log} +
{$log|escape:'html':'UTF-8'}
+ {/foreach} +
+
+
\ No newline at end of file