From b1b2ef5949eeb862cbcda1a653b63aaa821a1277 Mon Sep 17 00:00:00 2001 From: O K Date: Fri, 12 Dec 2025 09:28:40 +0200 Subject: [PATCH] first commit --- admin/controller/payment/hutko.php | 302 ++++++++++++++++++ admin/language/en-gb/payment/hutko.php | 114 +++++++ admin/language/ru-ru/payment/hutko.php | 104 ++++++ admin/language/uk-ua/payment/hutko.php | 217 +++++++++++++ admin/model/payment/hutko.php | 26 ++ admin/view/image/payment/hutko.png | Bin 0 -> 1443 bytes admin/view/template/payment/hutko.twig | 151 +++++++++ .../payment/hutko_order_info_panel.twig | 101 ++++++ catalog/controller/payment/hutko.php | 128 ++++++++ catalog/language/en-gb/payment/hutko.php | 14 + catalog/language/ru-ru/payment/hutko.php | 16 + catalog/language/uk-ua/payment/hutko.php | 16 + catalog/model/payment/hutko.php | 76 +++++ catalog/view/template/payment/hutko.twig | 30 ++ install.json | 8 + 15 files changed, 1303 insertions(+) create mode 100644 admin/controller/payment/hutko.php create mode 100644 admin/language/en-gb/payment/hutko.php create mode 100644 admin/language/ru-ru/payment/hutko.php create mode 100644 admin/language/uk-ua/payment/hutko.php create mode 100644 admin/model/payment/hutko.php create mode 100644 admin/view/image/payment/hutko.png create mode 100644 admin/view/template/payment/hutko.twig create mode 100644 admin/view/template/payment/hutko_order_info_panel.twig create mode 100644 catalog/controller/payment/hutko.php create mode 100644 catalog/language/en-gb/payment/hutko.php create mode 100644 catalog/language/ru-ru/payment/hutko.php create mode 100644 catalog/language/uk-ua/payment/hutko.php create mode 100644 catalog/model/payment/hutko.php create mode 100644 catalog/view/template/payment/hutko.twig create mode 100644 install.json diff --git a/admin/controller/payment/hutko.php b/admin/controller/payment/hutko.php new file mode 100644 index 0000000..0b0b708 --- /dev/null +++ b/admin/controller/payment/hutko.php @@ -0,0 +1,302 @@ +load->language('extension/hutko/payment/hutko'); + + $this->document->setTitle($this->language->get('heading_title')); + + $data['breadcrumbs'] = []; + $data['breadcrumbs'][] = [ + 'text' => $this->language->get('text_home'), + 'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token']) + ]; + $data['breadcrumbs'][] = [ + 'text' => $this->language->get('text_extension'), + 'href' => $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment') + ]; + $data['breadcrumbs'][] = [ + 'text' => $this->language->get('heading_title'), + 'href' => $this->url->link('extension/hutko/payment/hutko', 'user_token=' . $this->session->data['user_token']) + ]; + + // Save action + $data['save'] = $this->url->link('extension/hutko/payment/hutko.save', 'user_token=' . $this->session->data['user_token']); + $data['back'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment'); + + // Configuration Fields + $fields = [ + 'payment_hutko_merchant_id', + 'payment_hutko_secret_key', + 'payment_hutko_shipping_include', + 'payment_hutko_shipping_product_name', + 'payment_hutko_shipping_product_code', + 'payment_hutko_new_order_status_id', + 'payment_hutko_success_status_id', + 'payment_hutko_declined_status_id', + 'payment_hutko_expired_status_id', + 'payment_hutko_refunded_status_id', + 'payment_hutko_include_discount_to_total', + 'payment_hutko_status', + 'payment_hutko_sort_order', + 'payment_hutko_geo_zone_id', + 'payment_hutko_total', + 'payment_hutko_save_logs' + ]; + + foreach ($fields as $field) { + $data[$field] = $this->config->get($field); + } + + // Defaults + if (is_null($data['payment_hutko_shipping_product_name'])) $data['payment_hutko_shipping_product_name'] = 'Package material'; + if (is_null($data['payment_hutko_shipping_product_code'])) $data['payment_hutko_shipping_product_code'] = '0_0_1'; + if (is_null($data['payment_hutko_total'])) $data['payment_hutko_total'] = '0.01'; + + $this->load->model('localisation/order_status'); + $data['order_statuses'] = $this->model_localisation_order_status->getOrderStatuses(); + + $this->load->model('localisation/geo_zone'); + $data['geo_zones'] = $this->model_localisation_geo_zone->getGeoZones(); + + $data['log_content'] = $this->displayLastDayLog(); + + $data['header'] = $this->load->controller('common/header'); + $data['column_left'] = $this->load->controller('common/column_left'); + $data['footer'] = $this->load->controller('common/footer'); + + $this->response->setOutput($this->load->view('extension/hutko/payment/hutko', $data)); + } + + public function save(): void { + $this->load->language('extension/hutko/payment/hutko'); + + $json = []; + + if (!$this->user->hasPermission('modify', 'extension/hutko/payment/hutko')) { + $json['error']['warning'] = $this->language->get('error_permission'); + } + + // Validation + if (empty($this->request->post['payment_hutko_merchant_id']) || !is_numeric($this->request->post['payment_hutko_merchant_id'])) { + $json['error']['payment_hutko_merchant_id'] = $this->language->get('error_merchant_id_numeric'); + } + + $key = $this->request->post['payment_hutko_secret_key'] ?? ''; + if (empty($key) || ($key != 'test' && (strlen($key) < 10 || is_numeric($key)))) { + $json['error']['payment_hutko_secret_key'] = $this->language->get('error_secret_key_invalid'); + } + + if (!$json) { + $this->load->model('setting/setting'); + $this->model_setting_setting->editSetting('payment_hutko', $this->request->post); + $json['success'] = $this->language->get('text_success'); + } + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + public function install(): void { + $this->load->model('extension/hutko/payment/hutko'); + $this->model_extension_hutko_payment_hutko->install(); + + // OC4 Event Registration + $this->load->model('setting/event'); + + $event_code = 'hutko_order_info'; + $event_trigger = 'admin/view/sale/order_info/after'; + $event_action = 'extension/hutko/payment/hutko.order_info'; + + // OC 4.0.2.0 introduced the array signature for addEvent + if (version_compare(VERSION, '4.0.2.0', '>=')) { + $this->model_setting_event->addEvent([ + 'code' => $event_code, + 'description' => 'Hutko Payment Info Panel', + 'trigger' => $event_trigger, + 'action' => $event_action, + 'status' => 1, + 'sort_order' => 0 + ]); + } else { + // Legacy argument style for 4.0.0.0 - 4.0.1.x + $this->model_setting_event->addEvent($event_code, $event_trigger, $event_action, 1, 0); + } + } + + public function uninstall(): void { + $this->load->model('extension/hutko/payment/hutko'); + $this->model_extension_hutko_payment_hutko->uninstall(); + + $this->load->model('setting/event'); + $this->model_setting_event->deleteEventByCode('hutko_order_info'); + } + + // Event Handler for Admin Order View + public function order_info(string &$route, array &$args, string &$output): void { + if (!isset($args['order_id'])) return; + + $this->load->model('sale/order'); + $order_info = $this->model_sale_order->getOrder((int)$args['order_id']); + + // FIX: Check if payment_code exists and matches either 'hutko' or 'hutko.hutko' + if ($order_info && isset($order_info['payment_code']) && ($order_info['payment_code'] == 'hutko' || $order_info['payment_code'] == 'hutko.hutko')) { + $this->load->language('extension/hutko/payment/hutko'); + $this->load->model('extension/hutko/payment/hutko'); + + $hutko_order = $this->model_extension_hutko_payment_hutko->getHutkoOrder((int)$args['order_id']); + + $data['hutko_transaction_ref'] = $hutko_order['hutko_transaction_ref'] ?? ''; + $data['order_id'] = (int)$args['order_id']; + $data['user_token'] = $this->session->data['user_token']; + + // URLs for AJAX actions + $data['refund_url'] = $this->url->link('extension/hutko/payment/hutko.refund', 'user_token=' . $this->session->data['user_token']); + $data['status_url'] = $this->url->link('extension/hutko/payment/hutko.status', 'user_token=' . $this->session->data['user_token']); + + // Language Data + $data['text_payment_information'] = $this->language->get('text_payment_information'); + $data['text_hutko_transaction_ref_label'] = $this->language->get('text_hutko_transaction_ref_label'); + $data['hutko_transaction_ref_display'] = $data['hutko_transaction_ref'] ?: $this->language->get('text_not_available'); + $data['text_not_available'] = $this->language->get('text_not_available'); + + $data['text_hutko_refund_title'] = $this->language->get('text_hutko_refund_title'); + $data['entry_refund_amount'] = $this->language->get('entry_refund_amount'); + $data['entry_refund_comment'] = $this->language->get('entry_refund_comment'); + $data['button_hutko_refund'] = $this->language->get('button_hutko_refund'); + $data['text_confirm_refund'] = $this->language->get('text_confirm_refund'); + + $data['text_hutko_status_title'] = $this->language->get('text_hutko_status_title'); + $data['button_hutko_status_check'] = $this->language->get('button_hutko_status_check'); + + $content = $this->load->view('extension/hutko/payment/hutko_order', $data); + + // Inject content before the History tab/card + $pos = strpos($output, '
load->language('extension/hutko/payment/hutko'); + $this->load->model('extension/hutko/payment/hutko'); + $this->load->model('sale/order'); + + $json = []; + $order_id = (int)($this->request->post['order_id'] ?? 0); + $amount = (float)($this->request->post['refund_amount'] ?? 0); + $comment = (string)($this->request->post['refund_comment'] ?? ''); + + $hutko_order = $this->model_extension_hutko_payment_hutko->getHutkoOrder($order_id); + $order_info = $this->model_sale_order->getOrder($order_id); + + if ($hutko_order && $order_info && $amount > 0) { + $data = [ + 'order_id' => $hutko_order['hutko_transaction_ref'], + 'merchant_id' => $this->config->get('payment_hutko_merchant_id'), + 'version' => '1.0', + 'amount' => round($amount * 100), + 'currency' => $order_info['currency_code'], + 'comment' => $comment + ]; + $data['signature'] = $this->sign($data); + + $response = $this->api($this->refund_url, $data); + + if (($response['response']['reverse_status'] ?? '') === 'approved') { + $json['success'] = $this->language->get('text_refund_success'); + $msg = sprintf($this->language->get('text_refund_success_comment'), $hutko_order['hutko_transaction_ref'], $amount, $comment); + $this->model_sale_order->addHistory($order_id, $this->config->get('payment_hutko_refunded_status_id'), $msg, true); + } else { + $json['error'] = $response['response']['error_message'] ?? 'Unknown API Error'; + $this->logOC("Refund Failed: " . json_encode($response)); + } + } else { + $json['error'] = $this->language->get('error_invalid_request'); + } + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + public function status(): void { + $this->load->language('extension/hutko/payment/hutko'); + $json = []; + + $ref = $this->request->post['hutko_transaction_ref'] ?? ''; + if ($ref) { + $data = [ + 'order_id' => $ref, + 'merchant_id' => $this->config->get('payment_hutko_merchant_id'), + 'version' => '1.0', + ]; + $data['signature'] = $this->sign($data); + $response = $this->api($this->status_url, $data); + + if (($response['response']['response_status'] ?? '') === 'success') { + $json['success'] = $this->language->get('text_status_success'); + + unset($response['response']['response_signature_string'], $response['response']['signature']); + $json['data'] = $response['response']; + } else { + $json['error'] = $response['response']['error_message'] ?? 'API Error'; + } + } else { + $json['error'] = $this->language->get('error_missing_params'); + } + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + // Helpers + private function sign(array $data): string { + $key = $this->config->get('payment_hutko_secret_key'); + $filtered = array_filter($data, function ($v) { return $v !== '' && $v !== null; }); + ksort($filtered); + $str = $key; + foreach ($filtered as $v) $str .= '|' . $v; + return sha1($str); + } + + private function api(string $url, array $data): array { + if ($this->config->get('payment_hutko_save_logs')) $this->logOC('Req: ' . json_encode($data)); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['request' => $data])); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + $res = curl_exec($ch); + // curl_close($ch); + + if ($this->config->get('payment_hutko_save_logs')) $this->logOC('Res: ' . $res); + return json_decode($res, true) ?: []; + } + + private function displayLastDayLog() { + if (!$this->config->get('payment_hutko_save_logs')) return 'Logging Disabled'; + $file = DIR_LOGS . 'error.log'; + if (!file_exists($file)) return 'Log empty'; + + $lines = file($file); + $output = []; + // Get last 50 lines that match "Hutko" + for ($i = count($lines) - 1; $i >= 0 && count($output) < 50; $i--) { + if (strpos($lines[$i], 'Hutko') !== false) $output[] = htmlspecialchars($lines[$i], ENT_QUOTES, 'UTF-8'); + } + return implode('
', $output); + } + + private function logOC($message) { + $this->log->write('Hutko Payment: ' . $message); + } +} \ No newline at end of file diff --git a/admin/language/en-gb/payment/hutko.php b/admin/language/en-gb/payment/hutko.php new file mode 100644 index 0000000..2c6664a --- /dev/null +++ b/admin/language/en-gb/payment/hutko.php @@ -0,0 +1,114 @@ +Hutko'; // You'll need a hutko.png in admin/view/image/payment/ +$_['text_enabled'] = 'Enabled'; +$_['text_disabled'] = 'Disabled'; +$_['text_yes'] = 'Yes'; +$_['text_no'] = 'No'; +$_['text_info_merchant'] = 'Use "1700243" for test setup.'; +$_['text_info_secret'] = 'Use "test" for test setup.'; +$_['text_logs_disabled'] = 'Logging is currently disabled. Enable "Save Logs" to see logs.'; +$_['text_no_logs_found'] = 'No Hutko specific log entries found in the main log file for today, or logging is disabled.'; +$_['text_log_file_not_found'] = 'Log file (%s) not found.'; +$_['text_refund_success_comment'] = 'Refund Successful for ID: %s. Amount: %s. Comment: %s'; +$_['text_refund_failed_comment'] = 'Refund Attempt Failed for ID: %s. Reason: %s'; +$_['text_refund_success'] = 'Refund processed successfully via Hutko.'; +$_['text_refund_api_error'] = 'Hutko API Error: %s'; +$_['text_status_success'] = 'Status retrieved successfully from Hutko.'; +$_['text_status_api_error'] = 'Hutko API Error fetching status: %s'; +$_['text_unknown_error'] = 'An unknown error occurred.'; + + +// Entry +$_['entry_merchant_id'] = 'Merchant ID'; +$_['entry_secret_key'] = 'Secret Key'; +$_['entry_new_order_status'] = 'New Order Status'; +$_['entry_success_status'] = 'Successful Payment Status'; +$_['entry_declined_status'] = 'Declined Payment Status'; +$_['entry_expired_status'] = 'Expired Payment Status'; +$_['entry_refunded_status'] = 'Refunded Payment Status'; +$_['entry_shipping_include'] = 'Include Shipping Cost'; +$_['entry_shipping_product_name'] = 'Shipping Fiscal Name'; +$_['entry_shipping_product_code'] = 'Shipping Fiscal Code'; +$_['entry_show_cards_logo'] = 'Show Visa/MasterCard Logo'; +$_['entry_save_logs'] = 'Save Logs'; +$_['entry_include_discount_to_total'] = 'Include Discounts in Total (for API)'; +$_['entry_total'] = 'Minimum Order Total'; +$_['entry_geo_zone'] = 'Geo Zone'; +$_['entry_status'] = 'Status'; +$_['entry_sort_order'] = 'Sort Order'; + +// Help +$_['help_total'] = 'The checkout total the order must reach before this payment method becomes active.'; +$_['help_new_order_status'] = 'Status for new orders before payment redirection.'; +$_['help_success_status'] = 'Status for successfully paid orders.'; +$_['help_shipping_include'] = 'Include shipping cost as a separate item in the payment request details.'; +$_['help_shipping_product_name'] = 'Name of product/service to use in fiscalization for shipping amount.'; +$_['help_shipping_product_code'] = 'Code of product/service to use in fiscalization for shipping amount.'; +$_['help_show_cards_logo'] = 'Display Visa/MasterCard logos next to the payment method name on checkout.'; +$_['help_save_logs'] = 'Log API communication and callbacks to the system log file.'; +$_['help_include_discount_to_total'] = 'If Yes, order discounts will be subtracted from the payment total, this may prevent fiscalization.'; + + +// Error +$_['error_permission'] = 'Warning: You do not have permission to modify Hutko payment module!'; +$_['error_merchant_id_required'] = 'Merchant ID is required!'; +$_['error_merchant_id_numeric'] = 'Merchant ID must be numeric!'; +$_['error_secret_key_required'] = 'Secret Key is required!'; +$_['error_secret_key_invalid'] = 'Secret key must be "test" or at least 10 characters long and not entirely numeric.'; +$_['error_invalid_request'] = 'Invalid request data for refund/status.'; +$_['error_missing_params'] = 'Missing required parameters for refund/status.'; + + +// Tab +$_['tab_general'] = 'General'; +$_['tab_order_statuses'] = 'Order Statuses'; +$_['tab_fiscalization'] = 'Fiscalization'; +$_['tab_advanced'] = 'Advanced'; +$_['tab_logs'] = 'Logs'; +$_['text_payment_information'] = 'Payments History'; + + +$_['text_not_available'] = 'N/A'; +$_['text_hutko_transaction_ref_label'] = 'Hutko Transaction ID'; +$_['text_hutko_refund_title'] = 'Hutko Refund'; +$_['text_hutko_status_title'] = 'Hutko Status Check'; +$_['button_hutko_refund'] = 'Process Hutko Refund'; +$_['button_hutko_status_check'] = 'Check Hutko Payment Status'; +$_['entry_refund_amount'] = 'Refund Amount'; +$_['entry_refund_comment'] = 'Refund Comment (optional)'; + +$_['text_refund_success_comment'] = 'Refund for Hutko ID %s successful. Amount: %s. Comment: %s'; +$_['text_refund_failed_comment'] = 'Refund attempt for Hutko ID %s failed. Gateway error: %s'; +$_['text_refund_api_error'] = 'Hutko Refund API Error: %s'; +$_['text_status_api_error'] = 'Hutko Status API Error: %s'; +$_['text_unknown_error'] = 'An unknown error occurred with the API.'; + + +$_['error_missing_order_id'] = 'Error: Order ID is missing from the request.'; +$_['error_hutko_transaction_ref_not_found_db'] = 'Error: Hutko Transaction ID not found in database for this order.'; +$_['error_hutko_transaction_ref_missing'] = 'Error: Hutko Transaction ID is required for this operation.'; +$_['error_invalid_refund_amount'] = 'Error: Invalid refund amount. Must be greater than 0.'; +$_['error_missing_refund_amount'] = 'Error: Refund amount is required.'; + +// For catalog side (checkout process) +$_['error_payment_data_build'] = 'Error: Could not prepare payment data. Please try again or contact support.'; +$_['error_api_communication'] = 'Error: Could not communicate with the payment gateway. Please try again.'; +$_['text_redirecting_comment'] = 'Redirecting to Hutko. Hutko Order ID: %s. URL: %s'; + +// For callback +$_['text_payment_approved'] = 'Payment Approved by Hutko.'; +$_['text_payment_declined'] = 'Payment Declined by Hutko.'; +$_['text_payment_expired'] = 'Payment Expired at Hutko.'; +$_['text_payment_processing'] = 'Payment is Processing at Hutko.'; +$_['text_confirm_refund'] = 'Are you sure you want to refund this transaction via Hutko? This action cannot be undone.'; + +$_['text_loading'] = 'Loading...'; +$_['error_order_not_found'] = 'Error: Order not found.'; diff --git a/admin/language/ru-ru/payment/hutko.php b/admin/language/ru-ru/payment/hutko.php new file mode 100644 index 0000000..b65e2a7 --- /dev/null +++ b/admin/language/ru-ru/payment/hutko.php @@ -0,0 +1,104 @@ +Hutko'; // Вам понадобится hutko.png в admin/view/image/payment/ +$_['text_enabled'] = 'Включено'; +$_['text_disabled'] = 'Отключено'; +$_['text_yes'] = 'Да'; +$_['text_no'] = 'Нет'; +$_['text_info_merchant'] = 'Используйте "1700243" для теста.'; +$_['text_info_secret'] = 'Используйте "test" для теста.'; +$_['text_logs_disabled'] = 'Ведение журнала в настоящее время отключено. Включите "Сохранить журналы", чтобы увидеть журналы.'; +$_['text_no_logs_found'] = 'В главном файле журнала на сегодня не найдено никаких записей журнала Hutko, или ведение журнала отключено.'; +$_['text_log_file_not_found'] = 'Файл журнала (%s) не найден.'; +$_['text_refund_success_comment'] = 'Возмещение выполнен успешно для ID: %s. Сумма: %s. Комментарий: %s'; +$_['text_refund_failed_comment'] = 'Попытка возмещения не удалась для ID: %s. Причина: %s'; +$_['text_refund_success'] = 'Возмещение успешно обработано через Hutko.'; +$_['text_refund_api_error'] = 'Ошибка API Hutko: %s'; +$_['text_status_success'] = 'Статус успешно получен от Hutko.'; +$_['text_status_api_error'] = 'Ошибка API Hutko при получении статуса: %s'; +$_['text_unknown_error'] = 'Произошла неизвестная ошибка.'; + +$_['entry_merchant_id'] = 'ID продавца'; +$_['entry_secret_key'] = 'Секретный ключ'; +$_['entry_new_order_status'] = 'Статус нового заказа'; +$_['entry_success_status'] = 'Статус при успешном платеже'; +$_['entry_declined_status'] = 'Статус при отклоненном платеже'; +$_['entry_expired_status'] = 'Статус при просроченном платеже'; +$_['entry_refunded_status'] = 'Статус при возмещении платеже'; +$_['entry_shipping_include'] = 'Включить стоимость доставки'; +$_['entry_shipping_product_name'] = 'Наименование доставки в фискальном чеке'; +$_['entry_shipping_product_code'] = 'Код доставки в фискальном чеке'; +$_['entry_show_cards_logo'] = 'Показать логотип Visa/MasterCard'; +$_['entry_save_logs'] = 'Сохранять журналы'; +$_['entry_include_discount_to_total'] = 'Включить скидки в общую сумму (для API)'; +$_['entry_total'] = 'Минимальная сумма заказа'; +$_['entry_geo_zone'] = 'Геозона'; +$_['entry_status'] = 'Статус'; +$_['entry_sort_order'] = 'Порядок сортировки'; + + +$_['help_total'] = 'Сумма, которую должен достичь заказ, прежде чем этот способ оплаты станет активным.'; +$_['help_new_order_status'] = 'Статус для новых заказов до получения платежа.'; +$_['help_success_status'] = 'Статус для успешно оплаченных заказов.'; +$_['help_shipping_include'] = 'Включить стоимость доставки в суму платежа.'; +$_['help_shipping_product_name'] = 'Название продукта/услуги для использования при фискализации для суммы доставки.'; +$_['help_shipping_product_code'] = 'Код продукта/услуги для использования при фискализации для суммы доставки.'; +$_['help_show_cards_logo'] = 'Отображать логотипы Visa/MasterCard рядом с названием способа оплаты при оформлении заказа.'; +$_['help_save_logs'] = 'Записывать коммуникацию API и обратные вызовы в системный файл журнала.'; +$_['help_include_discount_to_total'] = 'Если да, скидки по заказу будут вычтены из общей суммы платежа, это может помешать фискализации.'; + + +$_['error_permission'] = 'Внимание: у вас нет разрешения на изменение платежного модуля Hutko!'; +$_['error_merchant_id_required'] = 'Требуется идентификатор продавца!'; +$_['error_merchant_id_numeric'] = 'Идентификатор продавца должен быть числовым!'; +$_['error_secret_key_required'] = 'Требуется секретный ключ!'; +$_['error_secret_key_invalid'] = 'Секретный ключ должен быть "test" или содержать не менее 10 символов и не состоять полностью из цифр.'; +$_['error_invalid_request'] = 'Недопустимые данные запроса на возмещения/статус.'; +$_['error_missing_params'] = 'Отсутствуют обязательные параметры для возмещения/статуса.'; + + +$_['tab_general'] = 'Общие'; +$_['tab_order_statuses'] = 'Статусы заказов'; +$_['tab_fiscalization'] = 'Фискализация'; +$_['tab_advanced'] = 'Дополнительно'; +$_['tab_logs'] = 'Журналы'; +$_['text_payment_information'] = 'История платежей'; +$_['text_not_available'] = 'Н/Д'; +$_['text_hutko_transaction_ref_label'] = 'Идентификатор заказа в Hutko'; +$_['text_hutko_refund_title'] = 'Возмещение Hutko'; +$_['text_hutko_status_title'] = 'Проверка статуса Hutko'; +$_['button_hutko_refund'] = 'Обработать возмещение через Hutko'; +$_['button_hutko_status_check'] = 'Проверить статус платежа Hutko'; +$_['entry_refund_amount'] = 'Сумма возмещения'; +$_['entry_refund_comment'] = 'Комментарий к возмещению (необязательно)'; +$_['text_refund_success_comment'] = 'Возмещение средств по ID %s успешно. Сумма: %s. Комментарий: %s'; +$_['text_refund_failed_comment'] = 'Попытка возмещения средств по ID %s не удалась. Ошибка шлюза: %s'; +$_['text_refund_api_error'] = 'Ошибка API возмещения Hutko: %s'; +$_['text_status_api_error'] = 'Ошибка API статуса Hutko: %s'; +$_['text_unknown_error'] = 'Произошла неизвестная ошибка API.'; +$_['error_missing_order_id'] = 'Ошибка: в запросе отсутствует идентификатор заказа.'; +$_['error_hutko_transaction_ref_not_found_db'] = 'Ошибка: идентификатор заказа Hutko не найден в базе данных для этого заказа.'; +$_['error_hutko_transaction_ref_missing'] = 'Ошибка: идентификатор заказа Hutko требуется для этой операции.'; +$_['error_invalid_refund_amount'] = 'Ошибка: недопустимая сумма возврата. Должна быть больше 0.'; +$_['error_missing_refund_amount'] = 'Ошибка: требуется сумма возврата.'; + +$_['error_payment_data_build'] = 'Ошибка: не удалось подготовить данные платежа. Повторите попытку или обратитесь в службу поддержки.'; +$_['error_api_communication'] = 'Ошибка: не удалось связаться с платежным шлюзом. Повторите попытку.'; +$_['text_redirecting_comment'] = 'Перенаправление на Hutko. Идентификатор заказа Hutko: %s. URL: %s'; + +// Для обратного вызова +$_['text_payment_approved'] = 'Платеж одобрен Hutko.'; +$_['text_payment_declined'] = 'Платеж отклонен Hutko.'; +$_['text_payment_expired'] = 'Срок платежа истек в Hutko.'; +$_['text_payment_processing'] = 'Платеж обрабатывается в Hutko.'; +$_['text_confirm_refund'] = 'Вы уверены, что хотите возместить оплату через Hutko? Это действие нельзя отменить.'; +$_['text_loading'] = 'Загрузка...'; +$_['error_order_not_found'] = 'Ошибка: заказ не найден.'; \ No newline at end of file diff --git a/admin/language/uk-ua/payment/hutko.php b/admin/language/uk-ua/payment/hutko.php new file mode 100644 index 0000000..aca10f1 --- /dev/null +++ b/admin/language/uk-ua/payment/hutko.php @@ -0,0 +1,217 @@ +Hutko'; // Вам понадобится hutko.png в admin/view/image/payment/ +$_['text_enabled'] = 'Включено'; +$_['text_disabled'] = 'Отключено'; +$_['text_yes'] = 'Да'; +$_['text_no'] = 'Нет'; +$_['text_info_merchant'] = 'Используйте "1700243" для теста.'; +$_['text_info_secret'] = 'Используйте "test" для теста.'; +$_['text_logs_disabled'] = 'Ведение журнала в настоящее время отключено. Включите "Сохранить журналы", чтобы увидеть журналы.'; +$_['text_no_logs_found'] = 'В главном файле журнала на сегодня не найдено никаких записей журнала Hutko, или ведение журнала отключено.'; +$_['text_log_file_not_found'] = 'Файл журнала (%s) не найден.'; +$_['text_refund_success_comment'] = 'Возмещение выполнен успешно для ID: %s. Сумма: %s. Комментарий: %s'; +$_['text_refund_failed_comment'] = 'Попытка возмещения не удалась для ID: %s. Причина: %s'; +$_['text_refund_success'] = 'Возмещение успешно обработано через Hutko.'; +$_['text_refund_api_error'] = 'Ошибка API Hutko: %s'; +$_['text_status_success'] = 'Статус успешно получен от Hutko.'; +$_['text_status_api_error'] = 'Ошибка API Hutko при получении статуса: %s'; +$_['text_unknown_error'] = 'Произошла неизвестная ошибка.'; + +$_['entry_merchant_id'] = 'ID продавца'; +$_['entry_secret_key'] = 'Секретный ключ'; +$_['entry_new_order_status'] = 'Статус нового заказа'; +$_['entry_success_status'] = 'Статус при успешном платеже'; +$_['entry_declined_status'] = 'Статус при отклоненном платеже'; +$_['entry_expired_status'] = 'Статус при просроченном платеже'; +$_['entry_refunded_status'] = 'Статус при возмещении платеже'; +$_['entry_shipping_include'] = 'Включить стоимость доставки'; +$_['entry_shipping_product_name'] = 'Наименование доставки в фискальном чеке'; +$_['entry_shipping_product_code'] = 'Код доставки в фискальном чеке'; +$_['entry_show_cards_logo'] = 'Показать логотип Visa/MasterCard'; +$_['entry_save_logs'] = 'Сохранять журналы'; +$_['entry_include_discount_to_total'] = 'Включить скидки в общую сумму (для API)'; +$_['entry_total'] = 'Минимальная сумма заказа'; +$_['entry_geo_zone'] = 'Геозона'; +$_['entry_status'] = 'Статус'; +$_['entry_sort_order'] = 'Порядок сортировки'; + + +$_['help_total'] = 'Сумма, которую должен достичь заказ, прежде чем этот способ оплаты станет активным.'; +$_['help_new_order_status'] = 'Статус для новых заказов до получения платежа.'; +$_['help_success_status'] = 'Статус для успешно оплаченных заказов.'; +$_['help_shipping_include'] = 'Включить стоимость доставки в суму платежа.'; +$_['help_shipping_product_name'] = 'Название продукта/услуги для использования при фискализации для суммы доставки.'; +$_['help_shipping_product_code'] = 'Код продукта/услуги для использования при фискализации для суммы доставки.'; +$_['help_show_cards_logo'] = 'Отображать логотипы Visa/MasterCard рядом с названием способа оплаты при оформлении заказа.'; +$_['help_save_logs'] = 'Записывать коммуникацию API и обратные вызовы в системный файл журнала.'; +$_['help_include_discount_to_total'] = 'Если да, скидки по заказу будут вычтены из общей суммы платежа, это может помешать фискализации.'; + + +$_['error_permission'] = 'Внимание: у вас нет разрешения на изменение платежного модуля Hutko!'; +$_['error_merchant_id_required'] = 'Требуется идентификатор продавца!'; +$_['error_merchant_id_numeric'] = 'Идентификатор продавца должен быть числовым!'; +$_['error_secret_key_required'] = 'Требуется секретный ключ!'; +$_['error_secret_key_invalid'] = 'Секретный ключ должен быть "test" или содержать не менее 10 символов и не состоять полностью из цифр.'; +$_['error_invalid_request'] = 'Недопустимые данные запроса на возмещения/статус.'; +$_['error_missing_params'] = 'Отсутствуют обязательные параметры для возмещения/статуса.'; + + +$_['tab_general'] = 'Общие'; +$_['tab_order_statuses'] = 'Статусы заказов'; +$_['tab_fiscalization'] = 'Фискализация'; +$_['tab_advanced'] = 'Дополнительно'; +$_['tab_logs'] = 'Журналы'; +$_['text_payment_information'] = 'История платежей'; +$_['text_not_available'] = 'Н/Д'; +$_['text_hutko_transaction_ref_label'] = 'Идентификатор заказа в Hutko'; +$_['text_hutko_refund_title'] = 'Возмещение Hutko'; +$_['text_hutko_status_title'] = 'Проверка статуса Hutko'; +$_['button_hutko_refund'] = 'Обработать возмещение через Hutko'; +$_['button_hutko_status_check'] = 'Проверить статус платежа Hutko'; +$_['entry_refund_amount'] = 'Сумма возмещения'; +$_['entry_refund_comment'] = 'Комментарий к возмещению (необязательно)'; +$_['text_refund_success_comment'] = 'Возмещение средств по ID %s успешно. Сумма: %s. Комментарий: %s'; +$_['text_refund_failed_comment'] = 'Попытка возмещения средств по ID %s не удалась. Ошибка шлюза: %s'; +$_['text_refund_api_error'] = 'Ошибка API возмещения Hutko: %s'; +$_['text_status_api_error'] = 'Ошибка API статуса Hutko: %s'; +$_['text_unknown_error'] = 'Произошла неизвестная ошибка API.'; +$_['error_missing_order_id'] = 'Ошибка: в запросе отсутствует идентификатор заказа.'; +$_['error_hutko_transaction_ref_not_found_db'] = 'Ошибка: идентификатор заказа Hutko не найден в базе данных для этого заказа.'; +$_['error_hutko_transaction_ref_missing'] = 'Ошибка: идентификатор заказа Hutko требуется для этой операции.'; +$_['error_invalid_refund_amount'] = 'Ошибка: недопустимая сумма возврата. Должна быть больше 0.'; +$_['error_missing_refund_amount'] = 'Ошибка: требуется сумма возврата.'; + +$_['error_payment_data_build'] = 'Ошибка: не удалось подготовить данные платежа. Повторите попытку или обратитесь в службу поддержки.'; +$_['error_api_communication'] = 'Ошибка: не удалось связаться с платежным шлюзом. Повторите попытку.'; +$_['text_redirecting_comment'] = 'Перенаправление на Hutko. Идентификатор заказа Hutko: %s. URL: %s'; + +// Для обратного вызова +$_['text_payment_approved'] = 'Платеж одобрен Hutko.'; +$_['text_payment_declined'] = 'Платеж отклонен Hutko.'; +$_['text_payment_expired'] = 'Срок платежа истек в Hutko.'; +$_['text_payment_processing'] = 'Платеж обрабатывается в Hutko.'; +$_['text_confirm_refund'] = 'Вы уверены, что хотите возместить оплату через Hutko? Это действие нельзя отменить.'; +$_['text_loading'] = 'Загрузка...'; +$_['error_order_not_found'] = 'Ошибка: заказ не найден.'; + + + + + + + + + + + + +$_['heading_title'] = 'Платежі Hutko'; + + +$_['text_extension'] = 'Розширення'; +$_['text_success'] = 'Успішно: Ви змінили налаштування модуля оплати Hutko!'; +$_['text_edit'] = 'Змінити налаштуваннь Hutko'; + +$_['text_hutko'] = 'Hutkodb->query(" + CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "hutko_order` ( + `hutko_order_pk_id` INT(11) NOT NULL AUTO_INCREMENT, + `order_id` INT(11) NOT NULL, + `hutko_transaction_ref` VARCHAR(255) NOT NULL, + `date_added` DATETIME NOT NULL, + PRIMARY KEY (`hutko_order_pk_id`), + UNIQUE KEY `idx_order_id` (`order_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + "); + } + + public function uninstall(): void { + // table drop optional + } + + public function getHutkoOrder(int $order_id): array { + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "hutko_order` WHERE `order_id` = '" . (int)$order_id . "'"); + return $query->row; + } +} \ No newline at end of file diff --git a/admin/view/image/payment/hutko.png b/admin/view/image/payment/hutko.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0ce029059ada71d9a217c78e8a3931e0702a8b GIT binary patch literal 1443 zcmZ8gYd92G7(Pfs>}-TZMX}}D3}J|jof#R#G}u8Tw?X7GO{F3t<(kW4Q(Bj>3R4Kt z%`j5B$O@r6Q!$LWxsJhHcC@>H_B_vf-uJxEd(QdseP0IF?XZ%9rUC#!3FCyqN!V-y zTVy1=!I$_{0HjhQkVq^BiPXUoV+avZp#bdbFUq%b>PBey1bDcheQiEEe9%RnY03(K zpWL2_)%~E*U~|K7P4Rw>$n^^2 z&vD~clp&hiYkcgQlWWEaHYJeeb}tw|6I_K*RJ+WYv#l}C6-<7|wcPvX;{Hez=RCOC zra@E52rCI3qiRtHZ?#g+m%;m-o17EeYzcqKX6$Mw#|vJx&op(K-y!^L=dP`sjG&R!B>ldwi@83ghUfZ9~v{;tn|`(@fvjAAGmc`0ao@ z5SzzVw+V+fLl-d@o;LJcPkw0`8~1s*(2how(}*_<@i?n*f20BnCi-V?5fNgO_igm$ z;jvzZOkM>UXVpp3h5A;pmQA@FHjCYF-XOXJI{Jy-y2=!LNjdzmuAXSISiD$VwLUc? zoLgY~#B#zCL@WHY!7-LcGWPS zul|u}VTl`)Di(=Wsc#t;xLKsXkiily^X3wBr&JECv^-}AoSE8gxmb8dED$c;tQF81 z?BIC1w(a`pglKJ@*Z7zlnJi?_@&0@`t%qF5xxIYn0bSE(?rg?V={+VkoUV0nv7kcm zZJJltM&Io)7oRny0iRUfzxtw|zQdMb{hKU9U-(jqXdjUq05!=QIF{DjSdjH%m1nrUnV^P4b8#kj@1X zLjg80BAR6ChW6C4Fol`IwIXI~C7J9{Zwvv0F$7yA2U(ygB@3h^%2@)ffAz+k?UR!E z?r%&QOiCI+BCv7SW(Fe3^5$N&2jNCS{32R2EPP&0F=RQW%zipAeA zgZwVSsLJZBouqLpF(?O5Cl_{Mm`4`^a`mXH;!!^o%Gets>#16wu5?kRF^K#0#H!fb z?fRb4V;-azF>47OpA*YoP2r4-lP=}#ZZuYNQL5Lku5BtR!k6PzBCB^8;JjL7+%4`l zrdGlu{lS^90!7XRK#u>UbR2|7RsNJuS1YT0*xDkKe81Zrx%)OT)8H~W`n)_@=k1m& zUBwf;UOIu-3cAv8GPj`G|gWg?ijK4%dX&&^xVC9 zupgTTw1bGw?_B%!!t%|m+=C58vim!~79`ku`_x!d`wS5wr~Hoxp)0M6JTL5NZZ8U9 zW}l22EOqW{+iK<6{-BS37+HnEC6K~b0guov>zqO-uw~KeSPy} zk=&SNiov|`DJ@E2FW26Y>}oea)^z~!!--6bd_YUH%*hADhhZt32JmC%4k@3s*d;ol jTa&tdpCk!bk3^8N4xH6D + +
+
+
+ + {{ text_edit }}
+
+
+ + +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
{{ help_total }}
+
+
+
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
{{ log_content }}
+
+
+
+
+
+
+
+{{ footer }} diff --git a/admin/view/template/payment/hutko_order_info_panel.twig b/admin/view/template/payment/hutko_order_info_panel.twig new file mode 100644 index 0000000..fb36aa0 --- /dev/null +++ b/admin/view/template/payment/hutko_order_info_panel.twig @@ -0,0 +1,101 @@ +
+
+ {{ text_payment_information }} (Hutko) +
+
+ + + + + +
{{ text_hutko_transaction_ref_label }}{{ hutko_transaction_ref_display }}
+ + {% if hutko_transaction_ref_display != text_not_available %} +
+
{{ text_hutko_refund_title }}
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
{{ text_hutko_status_title }}
+ +
+ {% endif %} +
+
+ + \ No newline at end of file diff --git a/catalog/controller/payment/hutko.php b/catalog/controller/payment/hutko.php new file mode 100644 index 0000000..7c04cb1 --- /dev/null +++ b/catalog/controller/payment/hutko.php @@ -0,0 +1,128 @@ +load->language('extension/hutko/payment/hutko'); + return $this->load->view('extension/hutko/payment/hutko', ['language' => $this->config->get('config_language')]); + } + + public function confirm(): void { + $this->load->language('extension/hutko/payment/hutko'); + $this->load->model('checkout/order'); + + $json = []; + + if (!isset($this->session->data['order_id'])) { + $json['error'] = 'Session missing'; + $json['redirect'] = $this->url->link('checkout/failure', 'language=' . $this->config->get('config_language'), true); + } else { + $order_info = $this->model_checkout_order->getOrder($this->session->data['order_id']); + + if (!$order_info) { + $json['error'] = 'Order missing'; + } else { + // Build API Payload + $request_data = $this->buildRequest($order_info); + + // Save Ref + $this->load->model('extension/hutko/payment/hutko'); + $this->model_extension_hutko_payment_hutko->addHutkoOrder($order_info['order_id'], $request_data['order_id']); + + // API Call + $response = $this->api($this->checkout_url, $request_data); + + if (($response['response']['response_status'] ?? '') === 'success' && !empty($response['response']['checkout_url'])) { + // Set to Pending/Initiated + $this->model_checkout_order->addHistory($order_info['order_id'], $this->config->get('payment_hutko_new_order_status_id'), 'Redirecting to Hutko', false); + + // Return Redirect URL to frontend JS + $json['redirect'] = $response['response']['checkout_url']; + } else { + $json['error'] = $response['response']['error_message'] ?? $this->language->get('error_api_communication'); + } + } + } + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + public function callback(): void { + $input = file_get_contents("php://input"); + $data = json_decode($input, true); + + if (!$data || !$this->validate($data)) { + http_response_code(400); + exit('Invalid Request'); + } + + $parts = explode('#', $data['order_id']); + $order_id = (int)$parts[0]; + + $this->load->model('checkout/order'); + $order_info = $this->model_checkout_order->getOrder($order_id); + + if ($order_info) { + $status = $data['order_status'] ?? ''; + + // Map statuses + if ($status === 'approved') { + $this->model_checkout_order->addHistory($order_id, $this->config->get('payment_hutko_success_status_id'), 'Hutko Confirmed', true); + echo "OK"; + } elseif ($status === 'declined') { + $this->model_checkout_order->addHistory($order_id, $this->config->get('payment_hutko_declined_status_id'), 'Declined', true); + echo "Declined"; + } else { + echo "Status update received"; + } + } + } + + private function buildRequest($order) { + $ref = $order['order_id'] . '#' . time(); + $total = (int)round($order['total'] * 100); // Send in cents + + $data = [ + 'order_id' => $ref, + 'merchant_id' => $this->config->get('payment_hutko_merchant_id'), + 'amount' => $total, + 'currency' => $order['currency_code'], + 'order_desc' => 'Order #' . $order['order_id'], + 'response_url' => $this->url->link('checkout/success', 'language=' . $this->config->get('config_language'), true), + 'server_callback_url' => $this->url->link('extension/hutko/payment/hutko.callback', '', true), + 'reservation_data' => base64_encode(json_encode(['products' => []])) // simplified for brevity + ]; + + $data['signature'] = $this->sign($data); + return $data; + } + + private function sign($data) { + $key = $this->config->get('payment_hutko_secret_key'); + $arr = array_filter($data, function($v){ return $v !== '' && $v !== null; }); + ksort($arr); + $str = $key; + foreach($arr as $v) $str .= '|' . $v; + return sha1($str); + } + + private function validate($data) { + $sig = $data['signature'] ?? ''; + unset($data['signature'], $data['response_signature_string']); + return hash_equals($this->sign($data), $sig); + } + + private function api($url, $data) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['request' => $data])); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + $res = curl_exec($ch); + curl_close($ch); + return json_decode($res, true) ?: []; + } +} \ No newline at end of file diff --git a/catalog/language/en-gb/payment/hutko.php b/catalog/language/en-gb/payment/hutko.php new file mode 100644 index 0000000..92e546b --- /dev/null +++ b/catalog/language/en-gb/payment/hutko.php @@ -0,0 +1,14 @@ +getMethod($address); + + // Only return the method if it actually has data + if ($method_data) { + return $method_data; + } + + return []; + } + + public function getMethod(array $address = []): array + { + $this->load->language('extension/hutko/payment/hutko'); + $allowed_currencies = ['UAH', 'USD', 'EUR', 'GBP', 'CZK']; + if (!in_array(strtoupper($this->session->data['currency']), $allowed_currencies)) { + $status = false; + } + // 1. Validate Address (Safeguard against undefined keys) + $country_id = isset($address['country_id']) ? (int)$address['country_id'] : 0; + $zone_id = isset($address['zone_id']) ? (int)$address['zone_id'] : 0; + + $status = true; + + // 2. Check Geo Zone + if ($this->config->get('payment_hutko_geo_zone_id')) { + $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "zone_to_geo_zone WHERE geo_zone_id = '" . (int)$this->config->get('payment_hutko_geo_zone_id') . "' AND country_id = '" . $country_id . "' AND (zone_id = '" . $zone_id . "' OR zone_id = '0')"); + + if (!$query->num_rows) { + $status = false; + } + } + + // 3. Check Order Total + if ($this->config->get('payment_hutko_total') > 0 && $this->config->get('payment_hutko_total') > $this->cart->getTotal()) { + $status = false; + } + + // 4. Return Data + $method_data = []; + + if ($status && $this->config->get('payment_hutko_status')) { + $option_data = []; + + $option_data['hutko'] = [ + 'code' => 'hutko.hutko', + 'name' => $this->language->get('text_title') + ]; + + // FORCE (int) casting and default value to prevent "Undefined array key" error + $sort_order = (int)$this->config->get('payment_hutko_sort_order') ?? 1; + + $method_data = [ + 'code' => 'hutko', + 'name' => $this->language->get('text_title'), + 'option' => $option_data, + 'sort_order' => $sort_order + ]; + } + + return $method_data; + } + + public function addHutkoOrder($order_id, $ref) + { + $this->db->query("INSERT INTO `" . DB_PREFIX . "hutko_order` SET `order_id` = '" . (int)$order_id . "', `hutko_transaction_ref` = '" . $this->db->escape($ref) . "', `date_added` = NOW() ON DUPLICATE KEY UPDATE `hutko_transaction_ref` = '" . $this->db->escape($ref) . "'"); + } +} diff --git a/catalog/view/template/payment/hutko.twig b/catalog/view/template/payment/hutko.twig new file mode 100644 index 0000000..1eb9eb8 --- /dev/null +++ b/catalog/view/template/payment/hutko.twig @@ -0,0 +1,30 @@ +
+ +
+ \ No newline at end of file diff --git a/install.json b/install.json new file mode 100644 index 0000000..d603421 --- /dev/null +++ b/install.json @@ -0,0 +1,8 @@ +{ + "name": "Hutko Payments", + "version": "1.0.0", + "author": "Hutko", + "link": "https://hutko.org", + "instruction": "", + "code": "hutko" +} \ No newline at end of file