From cae15fb881bde427fb44863457a5c60d30e11280 Mon Sep 17 00:00:00 2001
From: O K
Date: Thu, 29 May 2025 10:54:49 +0300
Subject: [PATCH] first commit
---
controllers/front/callback.php | 214 +++++++++
controllers/front/index.php | 20 +
controllers/front/redirect.php | 47 ++
controllers/front/result.php | 185 ++++++++
controllers/index.php | 20 +
hutko.php | 688 +++++++++++++++++++++++++++++
index.php | 21 +
logo.png | Bin 0 -> 7934 bytes
readme.md | 199 +++++++++
views/img/hutko_logo_cards.svg | 87 ++++
views/img/index.php | 20 +
views/img/logo.png | Bin 0 -> 7934 bytes
views/index.php | 20 +
views/templates/front/hutko.tpl | 14 +
views/templates/front/index.php | 20 +
views/templates/front/redirect.tpl | 29 ++
views/templates/index.php | 20 +
17 files changed, 1604 insertions(+)
create mode 100644 controllers/front/callback.php
create mode 100644 controllers/front/index.php
create mode 100644 controllers/front/redirect.php
create mode 100644 controllers/front/result.php
create mode 100644 controllers/index.php
create mode 100644 hutko.php
create mode 100644 index.php
create mode 100644 logo.png
create mode 100644 readme.md
create mode 100644 views/img/hutko_logo_cards.svg
create mode 100644 views/img/index.php
create mode 100644 views/img/logo.png
create mode 100644 views/index.php
create mode 100644 views/templates/front/hutko.tpl
create mode 100644 views/templates/front/index.php
create mode 100644 views/templates/front/redirect.tpl
create mode 100644 views/templates/index.php
diff --git a/controllers/front/callback.php b/controllers/front/callback.php
new file mode 100644
index 0000000..7450410
--- /dev/null
+++ b/controllers/front/callback.php
@@ -0,0 +1,214 @@
+getRequestBody();
+
+ // If request body is empty, log and exit.
+ if (empty($requestBody)) {
+ PrestaShopLogger::addLog('Hutko Callback: Empty request body received.', 2, null, 'Cart', null, true);
+ exit('Empty request');
+ }
+
+ // 2. Validate the request signature and required fields.
+ // Ensure all expected fields are present before proceeding with validation.
+ $requiredFields = ['order_id', 'amount', 'order_status', 'signature', 'merchant_id'];
+ foreach ($requiredFields as $field) {
+ if (!isset($requestBody[$field])) {
+ PrestaShopLogger::addLog('Hutko Callback: Missing required field in request: ' . $field, 2, null, 'Cart', null, true);
+ exit('Missing parameter: ' . $field);
+ }
+ }
+
+ // Assuming validateResponse returns true on success, or a string error message on failure.
+ $isSignatureValid = $this->module->validateResponse($requestBody);
+ if ($isSignatureValid !== true) {
+ PrestaShopLogger::addLog('Hutko Callback: Invalid signature. Error: ' . $isSignatureValid, 2, null, 'Cart', null, true);
+ exit('Invalid signature');
+ }
+
+ // 3. Extract cart ID and load the cart.
+ // The order_id is expected to be in the format "cartID|timestamp".
+ $transaction_id = $requestBody['order_id'];
+ $orderIdParamParts = explode($this->module->order_separator, $transaction_id);
+ $cartId = (int)$orderIdParamParts[0]; // Ensure it's an integer
+
+ $cart = new Cart($cartId);
+
+ // Validate cart object.
+ if (!Validate::isLoadedObject($cart)) {
+ PrestaShopLogger::addLog('Hutko Callback: Cart not found for ID: ' . $cartId, 3, null, 'Cart', $cartId, true);
+ exit('Cart not found');
+ }
+
+ // 4. Determine the amount received from the callback.
+ $amountReceived = round((float)$requestBody['amount'] / 100, 2);
+
+ // 5. Check if the order already exists for this cart.
+ $orderId = Order::getIdByCartId($cart->id);
+ $orderExists = (bool)$orderId;
+
+ // 6. If the order doesn't exist, attempt to validate it using postponeCallback.
+ // This handles the scenario where the callback arrives before the customer returns to the site.
+ if (!$orderExists) {
+ // The callback function will check for order existence again right before validation
+ // to handle potential race conditions.
+ $validationCallback = function () use ($cart, $amountReceived, $transaction_id) {
+ // Re-check if the order exists right before validation in case the result controller
+ // created it in the interim while we were waiting for the second digit.
+ if (Order::getIdByCartId($cart->id)) {
+ return true; // Order already exists, no need to validate again.
+ }
+ // If order still doesn't exist, proceed with validation.
+ $idState = (int)Configuration::get('HUTKO_SUCCESS_STATUS_ID');
+ return $this->module->validateOrderFromCart((int)$cart->id, $amountReceived, $transaction_id, $idState);
+ };
+
+ // Postpone validation to seconds ending in 8 to avoid collision with result controller (ending in 3).
+ $validationResult = $this->module->postponeCallback($validationCallback, 8);
+
+ // Re-fetch order ID after potential validation.
+ $orderId = Order::getIdByCartId($cart->id);
+
+ if (!$orderId || !$validationResult) {
+ PrestaShopLogger::addLog('Hutko Callback: Order validation failed for cart ID: ' . $cart->id, 2, null, 'Cart', $cart->id, true);
+ exit('Order validation failed');
+ }
+ }
+
+ // If we reached here, an order should exist. Load it.
+ $order = new Order($orderId);
+ if (!Validate::isLoadedObject($order)) {
+ PrestaShopLogger::addLog('Hutko Callback: Order could not be loaded for ID: ' . $orderId, 3, null, 'Order', $orderId, true);
+ exit('Order not found after validation');
+ }
+
+ // 7. Handle payment status from the callback.
+ $orderStatusCallback = $requestBody['order_status'];
+ $currentOrderState = (int)$order->getCurrentState();
+
+ switch ($orderStatusCallback) {
+ case 'approved':
+ $expectedState = (int)Configuration::get('HUTKO_SUCCESS_STATUS_ID');
+ // Only change state if it's not already the success state or "Payment accepted".
+ // "Payment accepted" (PS_OS_PAYMENT) might be set by validateOrderFromCart.
+ if ($currentOrderState !== $expectedState && $currentOrderState !== (int)Configuration::get('PS_OS_PAYMENT')) {
+ $this->module->updateOrderStatus($orderId, $expectedState, 'Payment approved by Hutko.');
+ }
+ exit('OK');
+ break;
+
+ case 'declined':
+ $expectedState = (int)Configuration::get('PS_OS_ERROR');
+ // Only change state if it's not already the error state.
+ if ($currentOrderState !== $expectedState) {
+ $this->module->updateOrderStatus($orderId, $expectedState, 'Payment ' . $orderStatusCallback . ' by Hutko.');
+ }
+ exit('Order ' . $orderStatusCallback);
+ break;
+ case 'expired':
+ $expectedState = (int)Configuration::get('PS_OS_ERROR');
+ // Only change state if it's not already the error state.
+ if ($currentOrderState !== $expectedState) {
+ $this->module->updateOrderStatus($orderId, $expectedState, 'Payment ' . $orderStatusCallback . ' by Hutko.');
+ }
+ exit('Order ' . $orderStatusCallback);
+ break;
+
+ case 'processing':
+ // If the order is still processing, we might want to update its status
+ // to a specific 'processing' state if available, or just acknowledge.
+ // For now, if it's not already in a success/error state, set it to 'processing'.
+ $processingState = (int)Configuration::get('PS_OS_PAYMENT'); // Or a custom 'processing' state
+ if ($currentOrderState !== $processingState && $currentOrderState !== (int)Configuration::get('HUTKO_SUCCESS_STATUS_ID') && $currentOrderState !== (int)Configuration::get('PS_OS_ERROR')) {
+ $this->module->updateOrderStatus($orderId, $processingState, 'Payment processing by Hutko.');
+ }
+ exit('Processing');
+ break;
+
+ default:
+ // Log unexpected status and exit with an error.
+ PrestaShopLogger::addLog('Hutko Callback: Unexpected order status received: ' . $orderStatusCallback . ' for order ID: ' . $orderId, 3, null, 'Order', $orderId, true);
+ exit('Unexpected status');
+ break;
+ }
+ } catch (Exception $e) {
+ // Log any uncaught exceptions and exit with the error message.
+ PrestaShopLogger::addLog('Hutko Callback Error: ' . $e->getMessage(), 3, null, 'HutkoCallbackModuleFrontController', null, true);
+ exit($e->getMessage());
+ }
+ }
+
+ /**
+ * Helper method to parse the request body from POST or raw input.
+ *
+ * @return array The parsed request body.
+ */
+ private function getRequestBody(): array
+ {
+ // Prioritize $_POST for form data.
+ if (!empty($_POST)) {
+ return $_POST;
+ }
+
+ // Fallback to raw input for JSON payloads, common for callbacks.
+ $jsonBody = json_decode(Tools::file_get_contents("php://input"), true);
+ if (is_array($jsonBody)) {
+ return $jsonBody;
+ }
+
+ return [];
+ }
+}
diff --git a/controllers/front/index.php b/controllers/front/index.php
new file mode 100644
index 0000000..55c9b2c
--- /dev/null
+++ b/controllers/front/index.php
@@ -0,0 +1,20 @@
+context->smarty->assign([
+ 'hutko_url' => $this->module->checkout_url, // The URL of the Hutko payment gateway.
+ 'hutko_inputs' => $this->module->buildInputs(), // An array of input parameters required by Hutko.
+ ]);
+
+ // Set the template to be used for displaying the redirection form.
+ $this->setTemplate('module:' . $this->module->name . '/views/templates/front/redirect.tpl');
+ }
+}
diff --git a/controllers/front/result.php b/controllers/front/result.php
new file mode 100644
index 0000000..93bd42a
--- /dev/null
+++ b/controllers/front/result.php
@@ -0,0 +1,185 @@
+module->order_separator, $transaction_id);
+ $cartId = (int)$cartIdParts[0];
+
+ // Validate extracted cart ID. It must be a numeric value.
+ if (!is_numeric($cartId)) {
+ $this->errors[] = Tools::displayError($this->trans('Invalid cart ID received.', [], 'Modules.Hutko.Shop'));
+ $this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id));
+ return; // Stop execution after redirection
+ }
+
+ // Load the cart object.
+ $cart = new Cart($cartId);
+
+ // Verify that the cart belongs to the current customer to prevent unauthorized access.
+ if (!Validate::isLoadedObject($cart) || $cart->id_customer != $this->context->customer->id) {
+ $this->errors[] = Tools::displayError($this->trans('Access denied to this order.', [], 'Modules.Hutko.Shop'));
+ Tools::redirect('/'); // Redirect to home or a more appropriate error page
+ }
+
+ // Handle different payment statuses.
+ switch ($orderStatus) {
+ case 'declined':
+ $this->errors[] = Tools::displayError($this->trans('Your payment was declined. Please try again or use a different payment method.', [], 'Modules.Hutko.Shop'));
+ $this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id));
+ break;
+
+ case 'expired':
+ $this->errors[] = Tools::displayError($this->trans('Your payment has expired. Please try again.', [], 'Modules.Hutko.Shop'));
+ $this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id));
+ break;
+
+ case 'processing':
+ // For 'processing' status, we need to poll for order creation.
+ // This loop will try to find the order for a limited time to avoid
+ // exceeding PHP execution limits.
+ $maxAttempts = 10; // Max 10 attempts
+ $sleepTime = 5; // Sleep 5 seconds between attempts (total max 50 seconds)
+ $orderFound = false;
+ $orderId = 0;
+
+ for ($i = 0; $i < $maxAttempts; $i++) {
+ $orderId = Order::getIdByCartId($cart->id);
+ if ($orderId) {
+ $orderFound = true;
+ break; // Order found, exit loop
+ }
+ // If not found, wait for a few seconds before retrying.
+ sleep($sleepTime);
+ }
+
+ if ($orderFound) {
+ // Order found, redirect to confirmation page.
+ Tools::redirect($this->context->link->getPageLink('order-confirmation', true, $this->context->language->id, [
+ 'id_cart' => $cart->id,
+ 'id_module' => $this->module->id,
+ 'id_order' => $orderId,
+ 'key' => $this->context->customer->secure_key,
+ ]));
+ } else {
+ // Order not found after multiple attempts, assume it's still processing or failed silently.
+ $this->errors[] = Tools::displayError($this->trans('Your payment is still processing. Please check your order history later.', [], 'Modules.Hutko.Shop'));
+ $this->redirectWithNotifications($this->context->link->getPageLink('order-history', true, $this->context->language->id));
+ }
+ break;
+
+ case 'approved':
+ $orderId = Order::getIdByCartId($cart->id);
+
+ // If the order doesn't exist yet, validate it.
+ // The postponeCallback is used here to avoid race conditions with the callback controller
+ // (which might be trying to validate the order on seconds ending in 8, while this
+ // controller tries on seconds ending in 3).
+ if (!$orderId) {
+ // Define the validation logic to be executed by postponeCallback.
+ // This callback will first check if the order exists, and only
+ // validate if it doesn't, to avoid race conditions.
+ $validationCallback = function () use ($cart, $amountReceived, $transaction_id) {
+ // Re-check if the order exists right before validation in case the callback
+ // controller created it in the interim while we were waiting for the second digit.
+ if (Order::getIdByCartId($cart->id)) {
+ return true; // Order already exists, no need to validate again.
+ }
+ $idState = (int)Configuration::get('HUTKO_SUCCESS_STATUS_ID');
+ // If order still doesn't exist, proceed with validation.
+ return $this->module->validateOrderFromCart((int)$cart->id, $amountReceived, $transaction_id, $idState);
+ };
+
+ // Postpone the execution of the validation callback until the second ends in 3.
+ $validationResult = $this->module->postponeCallback($validationCallback, 3);
+
+ // After the postponed callback has run, try to get the order ID again.
+ $orderId = Order::getIdByCartId($cart->id);
+
+ // If validation failed or order still not found, add an error.
+ if (!$orderId || !$validationResult) {
+ $this->errors[] = Tools::displayError($this->trans('Payment approved but order could not be created. Please contact support.', [], 'Modules.Hutko.Shop'));
+ $this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id));
+ break;
+ }
+ }
+
+ // If order exists (either found initially or created by validation), redirect to confirmation.
+ Tools::redirect($this->context->link->getPageLink('order-confirmation', true, $this->context->language->id, [
+ 'id_cart' => $cart->id,
+ 'id_module' => $this->module->id,
+ 'id_order' => $orderId,
+ 'key' => $this->context->customer->secure_key,
+ ]));
+ break;
+
+ default:
+ // For any unexpected status, redirect to order history with a generic error.
+ $this->errors[] = Tools::displayError($this->trans('An unexpected payment status was received. Please check your order history.', [], 'Modules.Hutko.Shop'));
+ $this->redirectWithNotifications($this->context->link->getPageLink('order-history', true, $this->context->language->id));
+ break;
+ }
+
+ // This part should ideally not be reached if all cases are handled with redirects.
+ // However, as a fallback, if any errors were accumulated without a specific redirect,
+ // redirect to the order page.
+ if (count($this->errors)) {
+ $this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id));
+ }
+ }
+}
diff --git a/controllers/index.php b/controllers/index.php
new file mode 100644
index 0000000..55c9b2c
--- /dev/null
+++ b/controllers/index.php
@@ -0,0 +1,20 @@
+name = 'hutko';
+ $this->tab = 'payments_gateways';
+ $this->version = '1.1.0';
+ $this->author = 'Hutko';
+ $this->bootstrap = true;
+ $this->ps_versions_compliancy = array('min' => '1.7', 'max' => _PS_VERSION_);
+ $this->is_eu_compatible = 1;
+
+ parent::__construct();
+ $this->displayName = $this->trans('Hutko Payments', array(), 'Modules.Hutko.Admin');
+ $this->description = $this->trans('Hutko is a payment platform whose main function is to provide internet acquiring.
+ Payment gateway supports EUR, USD, PLN, GBP, UAH, RUB and +100 other currencies.', array(), 'Modules.Hutko.Admin');
+ }
+
+ public function install()
+ {
+ return parent::install()
+ && $this->registerHook('paymentOptions');
+ }
+
+ public function uninstall()
+ {
+ foreach ($this->settingsList as $val) {
+ if (!Configuration::deleteByName($val)) {
+ return false;
+ }
+ }
+ if (!parent::uninstall()) {
+ return false;
+ }
+ return true;
+ }
+
+
+ /**
+ * Load the configuration form
+ */
+ public function getContent()
+ {
+ /**
+ * If values have been submitted in the form, process.
+ */
+ $err = '';
+ if (((bool)Tools::isSubmit('submitHutkoModule')) == true) {
+ $this->postValidation();
+ if (!sizeof($this->postErrors)) {
+ $this->postProcess();
+ } else {
+ foreach ($this->postErrors as $error) {
+ $err .= $this->displayError($error);
+ }
+ }
+ }
+
+ return $err . $this->renderForm();
+ }
+
+ /**
+ * Create the form that will be displayed in the configuration of your module.
+ */
+ protected function renderForm()
+ {
+ $helper = new HelperForm();
+
+ $helper->show_toolbar = false;
+ $helper->table = $this->table;
+ $helper->module = $this;
+ $helper->default_form_language = $this->context->language->id;
+ $helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG', 0);
+
+ $helper->identifier = $this->identifier;
+ $helper->submit_action = 'submitHutkoModule';
+ $helper->currentIndex = $this->context->link->getAdminLink('AdminModules', false)
+ . '&configure=' . $this->name . '&tab_module=' . $this->tab . '&module_name=' . $this->name;
+ $helper->token = Tools::getAdminTokenLite('AdminModules');
+
+ $helper->tpl_vars = array(
+ 'fields_value' => $this->getConfigFormValues(), /* Add values for your inputs */
+ 'languages' => $this->context->controller->getLanguages(),
+ 'id_language' => $this->context->language->id,
+ );
+
+ return $helper->generateForm(array($this->getConfigForm()));
+ }
+
+ /**
+ * Create the structure of your form.
+ */
+ protected function getConfigForm()
+ {
+ global $cookie;
+
+ $options = [];
+
+ foreach (OrderState::getOrderStates($cookie->id_lang) as $state) { // getting all Prestashop statuses
+ if (empty($state['module_name'])) {
+ $options[] = ['status_id' => $state['id_order_state'], 'name' => $state['name'] . " [ID: $state[id_order_state]]"];
+ }
+ }
+
+ return array(
+ 'form' => array(
+ 'legend' => array(
+ 'title' => $this->trans('Please specify the Hutko account details for customers', array(), 'Modules.Hutko.Admin'),
+ 'icon' => 'icon-cogs',
+ ),
+ 'input' => array(
+ array(
+ 'col' => 4,
+ 'type' => 'text',
+ 'prefix' => '',
+ 'desc' => $this->trans('Enter a merchant id', array(), 'Modules.Hutko.Admin'),
+ 'name' => 'HUTKO_MERCHANT',
+ 'label' => $this->trans('Merchant ID', array(), 'Modules.Hutko.Admin'),
+ ),
+ array(
+ 'col' => 4,
+ 'type' => 'text',
+ 'prefix' => '',
+ 'name' => 'HUTKO_SECRET_KEY',
+ 'desc' => $this->trans('Enter a secret key', array(), 'Modules.Hutko.Admin'),
+ 'label' => $this->trans('Secret key', array(), 'Modules.Hutko.Admin'),
+ ),
+ array(
+ 'type' => 'select',
+ 'prefix' => '',
+ 'name' => 'HUTKO_SUCCESS_STATUS_ID',
+ 'label' => $this->trans('Status after success payment', array(), 'Modules.Hutko.Admin'),
+ 'options' => array(
+ 'query' => $options,
+ 'id' => 'status_id',
+ 'name' => 'name'
+ )
+ ),
+ array(
+ 'type' => 'radio',
+ 'label' => $this->trans('Show Visa/MasterCard logo', array(), 'Modules.Hutko.Admin'),
+ 'name' => 'HUTKO_SHOW_CARDS_LOGO',
+ 'is_bool' => true,
+ 'values' => array(
+ array(
+ 'id' => 'show_cards',
+ 'value' => 1,
+ 'label' => $this->trans('Yes', array(), 'Modules.Hutko.Admin')
+ ),
+ array(
+ 'id' => 'hide_cards',
+ 'value' => 0,
+ 'label' => $this->trans('No', array(), 'Modules.Hutko.Admin')
+ )
+ ),
+ ),
+ ),
+ 'submit' => array(
+ 'title' => $this->trans('Save', array(), 'Modules.Hutko.Admin'),
+ 'class' => 'btn btn-default pull-right'
+ ),
+ ),
+ );
+ }
+
+ /**
+ * Set values for the inputs.
+ */
+ protected function getConfigFormValues()
+ {
+ return array(
+ 'HUTKO_MERCHANT' => Configuration::get('HUTKO_MERCHANT', null),
+ 'HUTKO_SECRET_KEY' => Configuration::get('HUTKO_SECRET_KEY', null),
+ 'HUTKO_SUCCESS_STATUS_ID' => Configuration::get('HUTKO_SUCCESS_STATUS_ID', null),
+ 'HUTKO_SHOW_CARDS_LOGO' => Configuration::get('HUTKO_SHOW_CARDS_LOGO', null),
+ );
+ }
+
+ /**
+ * Save form data.
+ */
+ protected function postProcess()
+ {
+ $form_values = $this->getConfigFormValues();
+ foreach (array_keys($form_values) as $key) {
+ Configuration::updateValue($key, Tools::getValue($key));
+ }
+ }
+
+ /**
+ * Validates the configuration submitted through the module's settings form.
+ *
+ * This method checks if the form has been submitted and then validates the
+ * Merchant ID and Secret Key provided by the user. It adds error messages
+ * to the `$this->postErrors` array if any of the validation rules fail.
+ */
+ private function postValidation(): void
+ {
+ // Check if the module's configuration form has been submitted.
+ if (Tools::isSubmit('submitHutkoModule')) {
+ // Retrieve the submitted Merchant ID and Secret Key.
+ $merchantId = Tools::getValue('HUTKO_MERCHANT');
+ $secretKey = Tools::getValue('HUTKO_SECRET_KEY');
+
+ // Validate Merchant ID:
+ if (empty($merchantId)) {
+ $this->postErrors[] = $this->trans('Merchant ID is required.', [], 'Modules.Hutko.Admin');
+ }
+ if (!is_numeric($merchantId)) {
+ $this->postErrors[] = $this->trans('Merchant ID must be numeric.', [], 'Modules.Hutko.Admin');
+ }
+
+ // Validate Secret Key:
+ if (empty($secretKey)) {
+ $this->postErrors[] = $this->trans('Secret key is required.', [], 'Modules.Hutko.Admin');
+ }
+ if ($secretKey != 'test' && (Tools::strlen($secretKey) < 10 || is_numeric($secretKey))) {
+ $this->postErrors[] = $this->trans('Secret key must be at least 10 characters long and cannot be entirely numeric.', [], 'Modules.Hutko.Admin');
+ }
+ }
+ }
+
+
+ /**
+ * Hook for displaying payment options on the checkout page.
+ *
+ * This hook is responsible for adding the Hutko payment option to the list
+ * of available payment methods during the checkout process. It checks if the
+ * module is active, if the necessary configuration is set, and if the cart's
+ * currency is supported before preparing the payment option.
+ *
+ * @param array $params An array of parameters passed by the hook, containing
+ * information about the current cart.
+ * @return array|false An array containing the Hutko PaymentOption object if
+ * the module is active, configured, and the currency is supported, otherwise false.
+ */
+ public function hookPaymentOptions($params)
+ {
+ // 1. Check if the module is active. If not, do not display the payment option.
+ if (!$this->active) {
+ return false;
+ }
+
+ // 2. Check if the merchant ID and secret key are configured. If not, do not display the option.
+ if (!Configuration::get("HUTKO_MERCHANT") || !Configuration::get("HUTKO_SECRET_KEY")) {
+ return false;
+ }
+
+ // 3. Check if the cart's currency is supported by the module. If not, do not display the payment option.
+ if (!$this->checkCurrency($params['cart'])) {
+ return false;
+ }
+
+ // 4. Assign template variables to be used in the payment option's additional information.
+ $this->context->smarty->assign([
+ 'hutko_logo_path' => $this->context->link->getMediaLink(__PS_BASE_URI__ . 'modules/' . $this->name . '/views/img/logo.png'),
+ 'hutko_description' => $this->trans('Pay via payment system Hutko', [], 'Modules.Hutko.Admin'),
+ ]);
+
+ // 5. Create a new PaymentOption object for the Hutko payment method.
+ $newOption = new PaymentOption();
+
+ // 6. Configure the PaymentOption object.
+ $newOption->setModuleName($this->name)
+ ->setCallToActionText($this->trans('Pay via Hutko', [], 'Modules.Hutko.Admin'))
+ ->setAction($this->context->link->getModuleLink($this->name, 'redirect', [], true))
+ ->setAdditionalInformation($this->context->smarty->fetch('module:hutko/views/templates/front/hutko.tpl'));
+
+ // 7. Optionally set a logo for the payment option if the corresponding configuration is enabled.
+ if (Configuration::get("HUTKO_SHOW_CARDS_LOGO")) {
+ $newOption->setLogo(Tools::getHttpHost(true) . $this->_path . 'views/img/hutko_logo_cards.svg');
+ }
+
+ // 8. Return an array containing the configured PaymentOption object.
+ return [$newOption];
+ }
+
+
+ /**
+ * Builds an array of input parameters required for the payment gateway.
+ *
+ * This method gathers necessary information such as order ID, merchant ID,
+ * order description, amount, currency, callback URLs, customer email,
+ * reservation data, and generates a signature for the request.
+ *
+ * @return array An associative array containing the input parameters for the
+ * payment gateway. This array includes the generated signature.
+ */
+ public function buildInputs(): array
+ {
+ // 1. Generate a unique order ID combining the cart ID and current timestamp.
+ $orderId = $this->context->cart->id . $this->order_separator . time();
+
+ // 2. Retrieve the merchant ID from the module's configuration.
+ $merchantId = Configuration::get('HUTKO_MERCHANT');
+
+ // 3. Create a description for the order.
+ $orderDescription = $this->trans('Cart pay №', [], 'Modules.Hutko.Admin') . $this->context->cart->id;
+ // 4. Calculate the order amount in the smallest currency unit.
+ $amount = round($this->context->cart->getOrderTotal() * 100);
+
+ // 5. Get the currency ISO code of the current cart.
+ $currency = $this->context->currency->iso_code;
+
+ // 6. Generate the server callback URL.
+ $serverCallbackUrl = $this->context->link->getModuleLink($this->name, 'callback', [], true);
+
+ // 7. Generate the customer redirection URL after payment.
+ $responseUrl = $this->context->link->getModuleLink($this->name, 'result', [], true);
+
+ // 8. Retrieve the customer's email address.
+ $customerEmail = $this->context->customer->email;
+
+ // 9. Build the reservation data as a base64 encoded JSON string.
+ $reservationData = $this->buildReservationData();
+
+ // 10. Construct the data array with all the collected parameters.
+ $data = [
+ 'order_id' => $orderId,
+ 'merchant_id' => $merchantId,
+ 'order_desc' => $orderDescription,
+ 'amount' => $amount,
+ 'currency' => $currency,
+ 'server_callback_url' => $serverCallbackUrl,
+ 'response_url' => $responseUrl,
+ 'sender_email' => $customerEmail,
+ 'reservation_data' => $reservationData,
+ ];
+
+ // 11. Generate the signature for the data array using the merchant's secret key.
+ $data['signature'] = $this->getSignature($data, Configuration::get('HUTKO_SECRET_KEY'));
+
+ // 12. Return the complete data array including the signature.
+ return $data;
+ }
+
+
+
+
+ /**
+ * Builds a base64 encoded JSON string containing reservation-related data.
+ *
+ * This method gathers information about the current cart, customer's delivery
+ * address, shop details, and products in the cart to create an array. This
+ * array is then encoded as a JSON string and subsequently base64 encoded
+ * for transmission or storage.
+ *
+ * @return string A base64 encoded JSON string containing the reservation data.
+ */
+ public function buildReservationData(): string
+ {
+ // 1. Retrieve the delivery address for the current cart.
+ $address = new Address((int)$this->context->cart->id_address_delivery, $this->context->language->id);
+
+ // 2. Fetch the customer's state name, if available.
+ $customerState = '';
+ if ($address->id_state) {
+ $state = new State((int) $address->id_state, $this->context->language->id);
+ $customerState = $state->name;
+ }
+
+ // 3. Construct the data array.
+ $data = [
+ "cms_name" => "Prestashop",
+ "cms_version" => _PS_VERSION_,
+ "shop_domain" => Tools::getShopDomainSsl(),
+ "path" => 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'],
+ "phonemobile" => $address->phone_mobile ?? $address->phone,
+ "customer_address" => $this->getSlug($address->address1),
+ "customer_country" => $this->getSlug($address->country),
+ "customer_state" => $this->getSlug($customerState),
+ "customer_name" => $this->getSlug($address->lastname . ' ' . $address->firstname),
+ "customer_city" => $this->getSlug($address->city),
+ "customer_zip" => $address->postcode,
+ "account" => $this->context->customer->id,
+ "uuid" => hash('sha256', _COOKIE_KEY_ . Tools::getShopDomainSsl()),
+ "products" => $this->getProducts(),
+ ];
+
+ // 4. Encode the data array as a JSON string.
+ $jsonData = json_encode($data);
+
+ // 5. Base64 encode the JSON string.
+ return base64_encode($jsonData);
+ }
+
+
+ /**
+ * Retrieves an array of product details from the current cart.
+ *
+ * This method iterates through the products in the current customer's cart
+ * using the context and extracts relevant information such as ID, name,
+ * unit price, total amount for each product (price multiplied by quantity),
+ * and the quantity itself.
+ *
+ * @return array An array where each element is an associative array containing
+ * the details of a product in the cart. The keys for each product are:
+ * - 'id': The product ID.
+ * - 'name': The name of the product.
+ * - 'price': The unit price of the product.
+ * - 'total_amount': The total price of the product in the cart (price * quantity), rounded to two decimal places.
+ * - 'quantity': The quantity of the product in the cart.
+ */
+ public function getProducts(): array
+ {
+ $products = [];
+ foreach ($this->context->cart->getProducts() as $cartProduct) {
+ $products[] = [
+ "id" => (int)$cartProduct['id_product'],
+ "name" => $cartProduct['name'],
+ "price" => (float)$cartProduct['price'],
+ "total_amount" => round((float) $cartProduct['price'] * (int)$cartProduct['quantity'], 2),
+ "quantity" => (int)$cartProduct['quantity'],
+ ];
+ }
+ return $products;
+ }
+
+
+
+ /**
+ * Validates an order based on the provided cart ID and expected amount,
+ * setting the order status to "preparation".
+ *
+ * This method serves as a convenience wrapper around the `validateOrder` method,
+ * pre-filling the order status with the configured "preparation" status.
+ *
+ * @param int $id_cart The ID of the cart associated with the order to be validated.
+ * @param float $amount The expected total amount of the order. This value will be
+ * compared against the cart's total.
+ * @return bool True if the order validation was successful, false otherwise.
+ * @see PaymentModule::validateOrder()
+ */
+ public function validateOrderFromCart(int $id_cart, float $amount, string $transaction_id = '', int $idState = 0): bool
+ {
+ if (!$idState) {
+ $idState = (int) Configuration::get('PS_OS_PREPARATION');
+ }
+ // Call the parent validateOrder method with the "preparation" status.
+ return $this->validateOrder($id_cart, $idState, $amount, $this->displayName, null, ['transaction_id' => $transaction_id], null, false, $this->context->customer->secure_key);
+ }
+
+ /**
+ * Generates a URL-friendly slug from a given text.
+ *
+ * This method transliterates non-ASCII characters to their closest ASCII equivalents,
+ * removes any characters that are not alphanumeric or spaces, trims leading/trailing
+ * spaces, optionally replaces spaces with hyphens, and optionally converts the
+ * entire string to lowercase.
+ *
+ * @param string $text The input string to convert into a slug.
+ * @param bool $removeSpaces Optional. Whether to replace spaces with hyphens (true) or keep them (false). Defaults to false.
+ * @param bool $lowerCase Optional. Whether to convert the resulting slug to lowercase (true) or keep the original casing (false). Defaults to false.
+ * @return string The generated slug.
+ */
+ public function getSlug(string $text, bool $removeSpaces = false, bool $lowerCase = false): string
+ {
+ // 1. Transliterate non-ASCII characters to ASCII.
+ $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
+
+ // 2. Remove any characters that are not alphanumeric or spaces.
+ $text = preg_replace("/[^a-zA-Z0-9 ]/", "", $text);
+
+ // 3. Trim leading and trailing spaces.
+ $text = trim($text, ' ');
+
+ // 4. Optionally replace spaces with hyphens.
+ if ($removeSpaces) {
+ $text = str_replace(' ', '-', $text);
+ }
+
+ // 5. Optionally convert the slug to lowercase.
+ if ($lowerCase) {
+ $text = strtolower($text);
+ }
+
+ // 6. Return the generated slug.
+ return $text;
+ }
+
+
+ /**
+ * Checks if the cart's currency is supported by the module.
+ *
+ * This method retrieves the currency of the provided cart and then checks if this
+ * currency is present within the list of currencies supported by the module.
+ *
+ * @param Cart $cart The cart object whose currency needs to be checked.
+ * @return bool True if the cart's currency is supported by the module, false otherwise.
+ */
+ private function checkCurrency(Cart $cart): bool
+ {
+ // 1. Get the currency object of the order from the cart.
+ $orderCurrency = new Currency((int)$cart->id_currency);
+
+ // 2. Get the list of currencies supported by this module.
+ $moduleCurrencies = $this->getCurrency((int)$cart->id_currency);
+
+ // 3. Check if the module supports any currencies.
+ if (is_array($moduleCurrencies)) {
+ // 4. Iterate through the module's supported currencies.
+ foreach ($moduleCurrencies as $moduleCurrency) {
+ // 5. If the order currency ID matches a supported currency ID, return true.
+ if ($orderCurrency->id === (int)$moduleCurrency['id_currency']) {
+ return true;
+ }
+ }
+ }
+
+ // 6. If no matching currency is found, return false.
+ return false;
+ }
+
+
+
+
+ /**
+ * Generates a signature based on the provided data and a secret password.
+ *
+ * This method filters out empty and null values from the input data, sorts the remaining
+ * data alphabetically by key, concatenates the values with a pipe delimiter, prepends
+ * the secret password, and then generates a SHA1 hash of the resulting string.
+ *
+ * @param array $data An associative array of data to be included in the signature generation.
+ * Empty strings and null values in this array will be excluded.
+ * @param string $password The secret key used to generate the signature. This should be
+ * kept confidential.
+ * @param bool $encoded Optional. Whether to return the SHA1 encoded signature (true by default)
+ * or the raw string before encoding (false).
+ * @return string The generated signature (SHA1 hash by default) or the raw string.
+ */
+ public function getSignature(array $data, string $password, bool $encoded = true): string
+ {
+ // 1. Filter out empty and null values from the data array.
+ $filteredData = array_filter($data, function ($value) {
+ return $value !== '' && $value !== null;
+ });
+
+ // 2. Sort the filtered data array alphabetically by key.
+ ksort($filteredData);
+
+ // 3. Construct the string to be hashed. Start with the password.
+ $stringToHash = $password;
+
+ // 4. Append the values from the sorted data array, separated by a pipe.
+ foreach ($filteredData as $value) {
+ $stringToHash .= '|' . $value;
+ }
+
+ // 5. Return the SHA1 hash of the string or the raw string based on the $encoded flag.
+ if ($encoded) {
+ return sha1($stringToHash);
+ } else {
+ return $stringToHash;
+ }
+ }
+
+
+ /**
+ * Validates the signature of a payment gateway response.
+ *
+ * This method verifies that the received response originates from the expected merchant
+ * and that the signature matches the calculated signature based on the response data
+ * and the merchant's secret key.
+ *
+ * @param array $response An associative array containing the payment gateway's response data.
+ * This array is expected to include keys 'merchant_id' and 'signature'.
+ * It might also contain temporary signature-related keys that will be unset
+ * during the validation process.
+ * @return bool True if the response is valid (merchant ID matches and signature is correct),
+ * false otherwise.
+ */
+ public function validateResponse(array $response): bool
+ {
+ // 1. Verify the Merchant ID
+ if (Configuration::get('HUTKO_MERCHANT') !== $response['merchant_id']) {
+ return false;
+ }
+
+ // 2. Prepare Response Data for Signature Verification
+ $responseSignature = $response['signature'];
+
+ // Unset signature-related keys that should not be part of the signature calculation.
+ // This ensures consistency with how the signature was originally generated.
+ unset($response['response_signature_string'], $response['signature']);
+
+ // 3. Calculate and Compare Signatures
+ $calculatedSignature = $this->getSignature($response, Configuration::get('HUTKO_SECRET_KEY'));
+
+ return hash_equals($calculatedSignature, $responseSignature);
+ }
+
+
+ /**
+ * Postpones the execution of a callback function until the last digit of the current second
+ * matches a specified target digit, and returns the result of the callback.
+ *
+ * @param callable $callback The callback function to execute.
+ * @param int $targetDigit An integer from 0 to 9, representing the desired last digit of the second.
+ * return the result of the callback function execution.
+ * @throws InvalidArgumentException If $targetDigit is not an integer between 0 and 9.
+ */
+ function postponeCallback(callable $callback, int $targetDigit)
+ {
+ // Validate the target digit to ensure it's within the valid range (0-9)
+ if ($targetDigit < 0 || $targetDigit > 9) {
+ throw new InvalidArgumentException("The target digit must be an integer between 0 and 9.");
+ }
+
+ // Loop indefinitely until the condition is met
+ while (true) {
+ // Get the current second as a two-digit string (e.g., '05', '12', '59')
+ $currentSecond = (int)date('s');
+
+ // Extract the last digit of the current second
+ $lastDigitOfSecond = $currentSecond % 10;
+
+ // Check if the last digit matches the target digit
+ if ($lastDigitOfSecond === $targetDigit) {
+ echo "Condition met! Current second is {$currentSecond}, last digit is {$lastDigitOfSecond}.\n";
+ // If the condition is met, execute the callback and return its result
+ return $callback(); // Capture and return the callback's result
+ } else {
+ // If the condition is not met, print the current status and wait for a short period
+ echo "Current second: {$currentSecond}, last digit: {$lastDigitOfSecond}. Still waiting...\n";
+ // Wait for 100 milliseconds (0.1 seconds) to avoid busy-waiting and reduce CPU usage
+ usleep(100000); // 100000 microseconds = 100 milliseconds
+ }
+ }
+ }
+
+
+ /**
+ * Helper method to update order status and add to history.
+ *
+ * @param int $orderId The ID of the order to update.
+ * @param int $newStateId The ID of the new order state.
+ * @param string $message A message to log with the status change.
+ * @return void
+ */
+ public function updateOrderStatus(int $orderId, int $newStateId, string $message = ''): void
+ {
+ $order = new Order($orderId);
+ // Only update if the order is loaded and the current state is different from the new state.
+ if (Validate::isLoadedObject($order) && (int)$order->getCurrentState() !== $newStateId) {
+ $history = new OrderHistory();
+ $history->id_order = $orderId;
+ $history->changeIdOrderState($newStateId, $orderId);
+ $history->addWithemail(true, ['order_name' => $orderId]);
+ // PrestaShopLogger::addLog('Hutko Callback: Order ' . $orderId . ' status changed to ' . $newStateId . '. Message: ' . $message, 1, null, 'Order', $orderId, true);
+ } else {
+ // Log if the order was not loaded or already in the target state.
+ // PrestaShopLogger::addLog('Hutko Callback: Attempted to update order ' . $orderId . ' to state ' . $newStateId . ' but order not loaded or already in target state. Message: ' . $message, 2, null, 'Order', $orderId, true);
+ }
+ }
+}
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..c4d62ed
--- /dev/null
+++ b/index.php
@@ -0,0 +1,21 @@
+1Eg;Dds&0pD#kf!)AQ<2jo%3@f}p)SE^nkv)k}MxZwlN(>;AK%juP6bm~Z
zW&~to@rb%^cxUH0!`G}byxG5^`XTJO7B3`K2=uS~@2h
zd)J;P)TwL4t@zLp9k_AC;jM@JH(k@;GfS``H~Gpu_}&?R5v~0y(t{Z$?sj
zjy1Y==@D(dH>u}qW&L0$qp6p=T`6|o-@1)|!od((xoR0jyd}HOP%tAmE1~h)(YI`K
z_CoAy{&)9IgOPAstKUcaG(jMyD6*cOg^`}#AN~Mdqy;Bv7&d5&H#ypw96O8a*Xfqj
z^DT>a#JUSzw2SyE4H5
zqkKYKO4!&z-XC#POzxw!kG&KS4t_5&BJPyy>opx9?8N#8=bpzr|D|g6vo?U=*{mMBQ$o=fKC}bS`Nr8v=mZ#
z`;E$n@GZMc>Z|$Us;gb>P!oq-YFMP6(?k7%S&7Ak`Gxsm<@v1;P(?ef0TVWZ0rD<@
z4CL1tGgBOaN>RkSP@Rd20Tgc_@jxJTtpIO4!Gp+vIul*VUK+5)XEiV=*+m0(8f^wQ
z^VTD}kqyt$h}P%KZ3yQ)2&yhHElp1K02}~7Au{mL0E(v<9T%Vh+r`BJ&pX3PFzBub
z!$Sjh#>@h$N2L*=C`FVaT;W&%*$)ZR
zBM^`I5xp2vKSQ_>{?zyOrFrhU<3dm(dJ-vsC>>Z8@wX+98<|=Bsj)+WE1BZGs|Aq#
zH%SJW^cPuwvu$T)*PWjY0o4D*{hRb3zV8YHQf6j2eJa6s$2=o_4cN~5I2S5`?1I~U
zL?JLFESiK;AfS*W1r!2^i7H+dtAIhE3948k#)Uw{`~+p>MQ7l>fEoj!0B}VzfP+Ml
z2qXjnqkwe5sw$v}c!C1f8RM+rf=0qo&Ug~hSsDEk#0eT1$V$BD&sOa~xd2ekcqAM_
zLa8Va@p!lb3gd!Nz$+_b6fkfSM%5Yb3|IaE8xN
zE_eoBAI~5Hpl~D_2S?+ODmDl#4y}w+MjwSMr!EjFbOM
zD!5>=BozcvRl!A-0HgvIsj7e{A@B-BG#W#|qKI%J;X6rxqSL7)hCiN0)NuuP1h@jy
zbC)aV;oU$T`MWLtZp57^0E8*P(F*@bn9>i2m3Df@A0DeK{Rb!Ny8=Ho89?v*7|^_c
zUa0hEGyK6BVDJCu-;Y@Qe=Y%q{=3LO;`d*={-x_5G4PLs|IMy{>H0?u{3GFiv+Mti
zF3!KMQ$#P|6yy(FmY_vB>A*#c&DrF*K4^RAKeOs?A~3??ZD>mef%fd(`7nWQrHKNA
z>jRUo
z+U(YwwcXn)n_Yw1S!5%=v!+3P|dNp@r=oQ;fWP3X?J
zF-r(hvGF~VD#T5R?Y`c1IY#ZS$t;&Ue^SRe-u
zo?>c9&a$i#DN+Xwzr8%N{gDH51*}$Lu>%s>eZ?p=EYuFnCy<@^dJCj}w72fr`hmFN
zZ4juWrc^(bpIaoOyX@Vb$-&$u>K6QBJ<{bWZ`Nk)NOE~e%2v^{Quh9>Dj~<>gDYxF
zss8#wq;soYXN1k!>pE^W<~*6GfvjBb;lFLEdKyNVEzG3f@9a4e*_`xVBnq?HmMk?{fhGW!LurL-#YECt%bR(mgWh+erm$M
z>ZFom;A-N5e#N#h5dz8$6h+%Zty(3Sf(Fd7c?y?S(=y50zd}`qAfChAfZ2Q=?n1N5
znpj~*vpofavKQTv-SXe!nIyyuwelHgR6eNT-A+0H*%;s3vwp9LOY}K+7fH7vS_ztw>=nDNuD7F5ROE4?Zx5bjfyO>2LN@4*iE5~a4~(0zlXcW$5E)Y*@LZ6=Y~8W1
zuIig)koAF&!ANCp&+Ye6>?t3v=sSn_Hs(yU`J@&b*?8t^#dO{lzj|i37-lmJ*=jh!
zzZ#(A@>rz4bnoZ^RShAR3XQ?ZdXKu_{XsX*SPM))_z<6
zc5UeWlwF>ox5^4jN)q;J>~;TK+mu&oBV!Dw(1C+7Z}jRm%R=Khl4kagB$qUE97EmAdr{uli0Bxy00AU>YuXCjxawHo5k{ZLj%#nD%SIlV;u^L~
z9d+0=Z$wsXRb|b-IhN3Eb!X4(eetz7j}~6e8y;f+npeGjw>iISq*ZQYd)AM2#YT4`
zQsD?ATXuu+uG!4}kV+6A7a>-!uh336oz*e{_rzaIyi26qLN(b#t)XHSQ4-x(zp_<+
zD&dytBOR?z!o&-2I1MF~?CCe_ABR}$FB+Saf$MID#EmQ%C%c}Am=1pf3MQ{U8IN{t
z!Yv7HTLr!TeX{OTqm-FI((oy!KI_mJ?^U}OTiV_8ap*@&)t?ksJ9+WRShhTlJKCiF
zc9#5O99c@db%^EZZEBMsIIk7maaDrz^B0a%l|f1N3bFIjIeWJ*ziVKTgB06wE%WSD
z3bKi_p+D=UM%Efd$Xn@buIbeMK*P;hPQG)($b+EmIz`kVT7!oyt*7q0e*xUeU!V?}
z9kb1RM`U45P`6?~%3F)rOvy{sk~_*0^HB;fswKd9eZvRs8SG~;W;5M;PlNxxuc;kK
z?kM+MSgXo{^uukwG`jJx{-+A}O{DDAaY&FjRrpIV?|pFV-p85cixKh=!y_pyxD{8Q
zp4@vc;tqL?ragsZZL%}niop-UbG*)iTjye70u|^tzH_zpB@#Ub&}UUk@^)fXBevjv@=;=lrOH*WL-HKE{0jH
zOuTi#BJS6NYcs)A3X|hmiIgrsF?sqyrj(SoW#15KEuZW(BU}g0HceJ!3^@glUdU^j
z2-&(p?|sQF#v1(bRc4uT9m9(k*2FwI!XH(w8DsH*X|?x8!EJg`An&>)OPwI&;_dFS
zmuIoATD%pX(IpQYm_N0&MHy7{lcc`XRD*Mxeifwk_sKJ>Jvp(*Uo3H$X>J+p-FA*A
z$Tqsna^rU7HHVDP=I#{)uixO|79UQJ2+2U%%kNQjv%f@93r2&=^bt|vrjHwtRL*-}
zmY6vg*2FiyI<$N)bj1U0K3U?xFbsPcsNcLy<=;mm>%Ppet3qcQee1@iEKAU~nmByECsN}q%&;{B)zp0bm
zdj`g%#RU}iSeZ@$`LF3^b>DRe8
z1B`C*I@Dym{5(b^g3j^m&sOH?aS9w8jgz}~YcaW{npvM~{6q<^r~lx`*or-e^4mYi
zyM9|?yFuy`?Rl<4>Ib%gIMb4fb2mit`!9uFWUebKcs>V~dQ?PP(j_`FH4HV~bS+4d
zceHsgm|xCx+4^=wz=liI5ZwBh5pPp(yEfn0G~111NpCT}?tV_AGB*ok@^kXgeR`1y
zyQP=Rb<;LrQ#VH;`}X^uKFlV>+#~1Zs<)Y}D8aLRmA6lueU`Frqa=1;;Iel=%6G!R
zWQH$iz5JMa-g#HiQ023^bb$@A^17BRpzXJ~M5=uBYdwOJRd!rPnY(Ry=$&x>PR_^<`&t@1N91WIbG5i^
z_K7TQexS}o-B$+z$BWWO5Bz4P$5Xu?UJUf=oG($#0GB@~J8leYafDtvWGhGjZ4V0+q$qwufCGJ(5)yg!vC^R+IXq@
zi*UrjP&S}dD2ogZG`^d{8HI@`g_8ESRshosu{f!g=!eJ+v{qUbW=G4;5`H&2|S
z1n8@u5_vHiw9{?t7O5Wv7_TCAUYT5T0YabNeJ1T-;){*`$b!>{TfLM9O&lXb8!*5r
zutYJ1TehWEWni8nlx5v_GfWI~Skp{$pTNTP7t41HlNV}O28(Or2n-JB^Cbs|;n`ZD
zWtQwqMkc6ZmM$acENOxGbgmM>_f!Loq;
zk(3;w#dMSN1(#Y;JSm1yOXyfkj!+zNwjarP#n~D$-LXBB_Ze22vXJf+7*ytNSFh6J
z{rVSX;JiE+rrZBZAluxC=EYc~rBXo=S58qf@8`Lf9Hk8c#_mJQV5f?SQ7h)5E%%*?
z28X8Nx1G!5F_ON)M)r5}f%Ai*Ek-0M(7JwE`lFXX1OYFq#yVlU_hc*BFO+9;Zc_4D
zRmbuS+q;=)czcLwH@dhTbm)uxrM=Bi-!3^|pA?|6n>tq=L~(!3(}d0+c{u=6w$kx`
zaiJ2~HE-G6#fLK}&4?S&WMu-5870NZyj^vkHaFkTDBzyh1)cHzi*BrFV6N(i!y{c$5Zd7krtfQA&7X9%16_D+6_kNUH
qPH(1;)FU?T{rTv>JsXN|v3LERm2Kfm76)#zAfsdE`uBC6BK`+8RverF
literal 0
HcmV?d00001
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..6232818
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,199 @@
+# Платіжний модуль Hutko PrestaShop
+
+Hutko – це платіжний сервіс, який рухає бізнес вперед. Запуск, набирання обертів, масштабування – ми подбаємо про вас усюди.
+
+Цей модуль інтегрує платіжний шлюз Hutko у ваш магазин PrestaShop, дозволяючи вашим клієнтам безпечно оплачувати свої замовлення через Hutko.
+
+## Зміст
+
+1. [Функції](#функції)
+
+2. [Встановлення](#встановлення)
+
+3. [Конфігурація](#конфігурація)
+
+4. [Використання](#використання)
+
+5. [Підтримка](#підтримка)
+
+## Функції
+
+* Безперешкодна інтеграція з платіжним шлюзом Hutko.
+
+* Безпечна обробка платежів.
+
+* Підтримка різних статусів платежів (Схвалено, Відхилено, Минув термін дії, Обробляється).
+
+* Автоматичне оновлення статусу замовлення в PrestaShop.
+
+* Надійна обробка зворотних викликів платежів для запобігання умовам гонки.
+
+## Встановлення
+
+Виконайте такі кроки, щоб встановити модуль Hutko у вашому магазині PrestaShop:
+
+1. **Завантажте модуль:** Отримайте останню версію модуля Hutko з офіційного джерела або з наданого вами пакета.
+
+2. **Завантажте в PrestaShop:**
+
+* Увійдіть до панелі адміністратора PrestaShop.
+
+* Перейдіть до **Модулі > Менеджер модулів**.
+
+* Натисніть кнопку «Завантажити модуль» (зазвичай розташована у верхньому правому куті).
+
+* Перетягніть файл модуля `.zip` в область завантаження або клацніть, щоб вибрати файл.
+
+3. **Встановіть модуль:**
+
+* Після завантаження PrestaShop автоматично виявить модуль.
+
+* Натисніть кнопку «Встановити» поруч із модулем «Hutko».
+
+* Дотримуйтесь будь-яких підказок на екрані.
+
+## Конфігурація
+
+Після успішної інсталяції необхідно налаштувати модуль, використовуючи дані вашого облікового запису Hutko:
+
+1. **Конфігурація модуля доступу:**
+
+* У панелі адміністратора PrestaShop перейдіть до **Модулі > Менеджер модулів**.
+
+* Знайдіть модуль "Hutko" та натисніть кнопку "Налаштувати".
+
+2. **Введіть необхідні облікові дані:**
+
+* **Ідентифікатор продавця:** Введіть свій унікальний ідентифікатор продавця, наданий Hutko. Це обов'язкове поле.
+
+* **Секретний ключ:** Введіть свій секретний ключ, наданий Hutko. Це обов'язкове поле, яке є критично важливим для безпечної перевірки підпису.
+
+* **Статус успішного замовлення:** (Необов'язково, якщо застосовується) Виберіть статус замовлення, який слід застосовувати до замовлень, успішно оплачених через Hutko.
+
+* **Показати логотип картки:** (Необов'язково) Увімкніть або вимкніть відображення логотипів картки на сторінці вибору способу оплати.
+
+3. **Зберегти зміни:** Натисніть кнопку "Зберегти", щоб застосувати налаштування конфігурації.
+
+**Важливо:** Без правильного налаштування **Ідентифікатора продавця** та **Секретного ключа** модуль не працюватиме належним чином і не відображатиметься як варіант оплати під час оформлення замовлення.
+
+## Використання
+
+Після налаштування варіант оплати Hutko автоматично з’явиться на сторінці оформлення замовлення для клієнтів.
+
+1. Клієнти вибирають «Оплатити через Hutko» на кроці оплати.
+
+2. Їх буде перенаправлено на сторінку оплати Hutko для завершення транзакції.
+
+3. Після успішної оплати клієнта буде перенаправлено назад на сторінку підтвердження замовлення вашого магазину PrestaShop, і статус замовлення буде оновлено відповідно.
+
+4. У разі невдалої оплати клієнта буде перенаправлено назад на сторінку замовлення з відповідним повідомленням про помилку.
+
+## Підтримка
+
+Якщо у вас виникнуть проблеми або виникнуть запитання щодо модуля Hutko PrestaShop, будь ласка, зверніться до наступного:
+
+* **Документація Hutko:** Зверніться до офіційного API та документації інтеграції Hutko для отримання детальної інформації.
+
+* **Форуми PrestaShop:** Шукайте або залишайте своє запитання на офіційних форумах PrestaShop.
+
+* **Зв’язатися з розробником:** Для отримання безпосередньої підтримки ви можете звернутися до автора модуля `panariga`.
+
+# Hutko PrestaShop Payment Module
+
+Hutko is a payment service that drives businesses forward. Launch, gain momentum, scale – we've got you covered everywhere.
+
+This module integrates the Hutko payment gateway into your PrestaShop store, allowing your customers to pay for their orders securely through Hutko.
+
+## Table of Contents
+
+1. [Features](#features)
+
+2. [Installation](#installation)
+
+3. [Configuration](#configuration)
+
+4. [Usage](#usage)
+
+5. [Support](#support)
+
+## Features
+
+* Seamless integration with the Hutko payment gateway.
+
+* Secure payment processing.
+
+* Support for various payment statuses (Approved, Declined, Expired, Processing).
+
+* Automatic order status updates in PrestaShop.
+
+* Robust handling of payment callbacks to prevent race conditions.
+
+## Installation
+
+Follow these steps to install the Hutko module on your PrestaShop store:
+
+1. **Download the Module:** Obtain the latest version of the Hutko module from the official source or your provided package.
+
+2. **Upload to PrestaShop:**
+
+ * Log in to your PrestaShop admin panel.
+
+ * Navigate to **Modules > Module Manager**.
+
+ * Click on the "Upload a module" button (usually located in the top right corner).
+
+ * Drag and drop the module's `.zip` file into the upload area, or click to select the file.
+
+3. **Install the Module:**
+
+ * Once uploaded, PrestaShop will automatically detect the module.
+
+ * Click on the "Install" button next to the "Hutko" module.
+
+ * Follow any on-screen prompts.
+
+## Configuration
+
+After successful installation, you must configure the module with your Hutko account details:
+
+1. **Access Module Configuration:**
+
+ * In your PrestaShop admin panel, go to **Modules > Module Manager**.
+
+ * Find the "Hutko" module and click on the "Configure" button.
+
+2. **Enter Required Credentials:**
+
+ * **Merchant ID:** Enter your unique Merchant ID provided by Hutko. This is a mandatory field.
+
+ * **Secret Key:** Enter your Secret Key provided by Hutko. This is a mandatory field and is crucial for secure signature validation.
+
+ * **Success Order Status:** (Optional, if applicable) Select the order status that should be applied to orders successfully paid via Hutko.
+
+ * **Show Cards Logo:** (Optional) Enable or disable the display of card logos on the payment selection page.
+
+3. **Save Changes:** Click the "Save" button to apply your configuration settings.
+
+**Important:** Without setting the correct **Merchant ID** and **Secret Key**, the module will not function correctly and will not appear as a payment option during checkout.
+
+## Usage
+
+Once configured, the Hutko payment option will automatically appear on your checkout page for customers.
+
+1. Customers select "Pay via Hutko" on the payment step of the checkout.
+
+2. They are redirected to the Hutko payment page to complete their transaction.
+
+3. Upon successful payment, the customer is redirected back to your PrestaShop store's order confirmation page, and the order status is updated accordingly.
+
+4. In case of payment failure, the customer will be redirected back to the order page with an appropriate error message.
+
+## Support
+
+If you encounter any issues or have questions regarding the Hutko PrestaShop module, please refer to the following:
+
+* **Hutko Documentation:** Consult the official Hutko API and integration documentation for detailed information.
+
+* **PrestaShop Forums:** Search or post your question on the official PrestaShop forums.
+
+* **Contact Developer:** For direct support, you can contact the module author `panariga`.
\ No newline at end of file
diff --git a/views/img/hutko_logo_cards.svg b/views/img/hutko_logo_cards.svg
new file mode 100644
index 0000000..8e428f9
--- /dev/null
+++ b/views/img/hutko_logo_cards.svg
@@ -0,0 +1,87 @@
+
+
+
+
diff --git a/views/img/index.php b/views/img/index.php
new file mode 100644
index 0000000..55c9b2c
--- /dev/null
+++ b/views/img/index.php
@@ -0,0 +1,20 @@
+1Eg;Dds&0pD#kf!)AQ<2jo%3@f}p)SE^nkv)k}MxZwlN(>;AK%juP6bm~Z
zW&~to@rb%^cxUH0!`G}byxG5^`XTJO7B3`K2=uS~@2h
zd)J;P)TwL4t@zLp9k_AC;jM@JH(k@;GfS``H~Gpu_}&?R5v~0y(t{Z$?sj
zjy1Y==@D(dH>u}qW&L0$qp6p=T`6|o-@1)|!od((xoR0jyd}HOP%tAmE1~h)(YI`K
z_CoAy{&)9IgOPAstKUcaG(jMyD6*cOg^`}#AN~Mdqy;Bv7&d5&H#ypw96O8a*Xfqj
z^DT>a#JUSzw2SyE4H5
zqkKYKO4!&z-XC#POzxw!kG&KS4t_5&BJPyy>opx9?8N#8=bpzr|D|g6vo?U=*{mMBQ$o=fKC}bS`Nr8v=mZ#
z`;E$n@GZMc>Z|$Us;gb>P!oq-YFMP6(?k7%S&7Ak`Gxsm<@v1;P(?ef0TVWZ0rD<@
z4CL1tGgBOaN>RkSP@Rd20Tgc_@jxJTtpIO4!Gp+vIul*VUK+5)XEiV=*+m0(8f^wQ
z^VTD}kqyt$h}P%KZ3yQ)2&yhHElp1K02}~7Au{mL0E(v<9T%Vh+r`BJ&pX3PFzBub
z!$Sjh#>@h$N2L*=C`FVaT;W&%*$)ZR
zBM^`I5xp2vKSQ_>{?zyOrFrhU<3dm(dJ-vsC>>Z8@wX+98<|=Bsj)+WE1BZGs|Aq#
zH%SJW^cPuwvu$T)*PWjY0o4D*{hRb3zV8YHQf6j2eJa6s$2=o_4cN~5I2S5`?1I~U
zL?JLFESiK;AfS*W1r!2^i7H+dtAIhE3948k#)Uw{`~+p>MQ7l>fEoj!0B}VzfP+Ml
z2qXjnqkwe5sw$v}c!C1f8RM+rf=0qo&Ug~hSsDEk#0eT1$V$BD&sOa~xd2ekcqAM_
zLa8Va@p!lb3gd!Nz$+_b6fkfSM%5Yb3|IaE8xN
zE_eoBAI~5Hpl~D_2S?+ODmDl#4y}w+MjwSMr!EjFbOM
zD!5>=BozcvRl!A-0HgvIsj7e{A@B-BG#W#|qKI%J;X6rxqSL7)hCiN0)NuuP1h@jy
zbC)aV;oU$T`MWLtZp57^0E8*P(F*@bn9>i2m3Df@A0DeK{Rb!Ny8=Ho89?v*7|^_c
zUa0hEGyK6BVDJCu-;Y@Qe=Y%q{=3LO;`d*={-x_5G4PLs|IMy{>H0?u{3GFiv+Mti
zF3!KMQ$#P|6yy(FmY_vB>A*#c&DrF*K4^RAKeOs?A~3??ZD>mef%fd(`7nWQrHKNA
z>jRUo
z+U(YwwcXn)n_Yw1S!5%=v!+3P|dNp@r=oQ;fWP3X?J
zF-r(hvGF~VD#T5R?Y`c1IY#ZS$t;&Ue^SRe-u
zo?>c9&a$i#DN+Xwzr8%N{gDH51*}$Lu>%s>eZ?p=EYuFnCy<@^dJCj}w72fr`hmFN
zZ4juWrc^(bpIaoOyX@Vb$-&$u>K6QBJ<{bWZ`Nk)NOE~e%2v^{Quh9>Dj~<>gDYxF
zss8#wq;soYXN1k!>pE^W<~*6GfvjBb;lFLEdKyNVEzG3f@9a4e*_`xVBnq?HmMk?{fhGW!LurL-#YECt%bR(mgWh+erm$M
z>ZFom;A-N5e#N#h5dz8$6h+%Zty(3Sf(Fd7c?y?S(=y50zd}`qAfChAfZ2Q=?n1N5
znpj~*vpofavKQTv-SXe!nIyyuwelHgR6eNT-A+0H*%;s3vwp9LOY}K+7fH7vS_ztw>=nDNuD7F5ROE4?Zx5bjfyO>2LN@4*iE5~a4~(0zlXcW$5E)Y*@LZ6=Y~8W1
zuIig)koAF&!ANCp&+Ye6>?t3v=sSn_Hs(yU`J@&b*?8t^#dO{lzj|i37-lmJ*=jh!
zzZ#(A@>rz4bnoZ^RShAR3XQ?ZdXKu_{XsX*SPM))_z<6
zc5UeWlwF>ox5^4jN)q;J>~;TK+mu&oBV!Dw(1C+7Z}jRm%R=Khl4kagB$qUE97EmAdr{uli0Bxy00AU>YuXCjxawHo5k{ZLj%#nD%SIlV;u^L~
z9d+0=Z$wsXRb|b-IhN3Eb!X4(eetz7j}~6e8y;f+npeGjw>iISq*ZQYd)AM2#YT4`
zQsD?ATXuu+uG!4}kV+6A7a>-!uh336oz*e{_rzaIyi26qLN(b#t)XHSQ4-x(zp_<+
zD&dytBOR?z!o&-2I1MF~?CCe_ABR}$FB+Saf$MID#EmQ%C%c}Am=1pf3MQ{U8IN{t
z!Yv7HTLr!TeX{OTqm-FI((oy!KI_mJ?^U}OTiV_8ap*@&)t?ksJ9+WRShhTlJKCiF
zc9#5O99c@db%^EZZEBMsIIk7maaDrz^B0a%l|f1N3bFIjIeWJ*ziVKTgB06wE%WSD
z3bKi_p+D=UM%Efd$Xn@buIbeMK*P;hPQG)($b+EmIz`kVT7!oyt*7q0e*xUeU!V?}
z9kb1RM`U45P`6?~%3F)rOvy{sk~_*0^HB;fswKd9eZvRs8SG~;W;5M;PlNxxuc;kK
z?kM+MSgXo{^uukwG`jJx{-+A}O{DDAaY&FjRrpIV?|pFV-p85cixKh=!y_pyxD{8Q
zp4@vc;tqL?ragsZZL%}niop-UbG*)iTjye70u|^tzH_zpB@#Ub&}UUk@^)fXBevjv@=;=lrOH*WL-HKE{0jH
zOuTi#BJS6NYcs)A3X|hmiIgrsF?sqyrj(SoW#15KEuZW(BU}g0HceJ!3^@glUdU^j
z2-&(p?|sQF#v1(bRc4uT9m9(k*2FwI!XH(w8DsH*X|?x8!EJg`An&>)OPwI&;_dFS
zmuIoATD%pX(IpQYm_N0&MHy7{lcc`XRD*Mxeifwk_sKJ>Jvp(*Uo3H$X>J+p-FA*A
z$Tqsna^rU7HHVDP=I#{)uixO|79UQJ2+2U%%kNQjv%f@93r2&=^bt|vrjHwtRL*-}
zmY6vg*2FiyI<$N)bj1U0K3U?xFbsPcsNcLy<=;mm>%Ppet3qcQee1@iEKAU~nmByECsN}q%&;{B)zp0bm
zdj`g%#RU}iSeZ@$`LF3^b>DRe8
z1B`C*I@Dym{5(b^g3j^m&sOH?aS9w8jgz}~YcaW{npvM~{6q<^r~lx`*or-e^4mYi
zyM9|?yFuy`?Rl<4>Ib%gIMb4fb2mit`!9uFWUebKcs>V~dQ?PP(j_`FH4HV~bS+4d
zceHsgm|xCx+4^=wz=liI5ZwBh5pPp(yEfn0G~111NpCT}?tV_AGB*ok@^kXgeR`1y
zyQP=Rb<;LrQ#VH;`}X^uKFlV>+#~1Zs<)Y}D8aLRmA6lueU`Frqa=1;;Iel=%6G!R
zWQH$iz5JMa-g#HiQ023^bb$@A^17BRpzXJ~M5=uBYdwOJRd!rPnY(Ry=$&x>PR_^<`&t@1N91WIbG5i^
z_K7TQexS}o-B$+z$BWWO5Bz4P$5Xu?UJUf=oG($#0GB@~J8leYafDtvWGhGjZ4V0+q$qwufCGJ(5)yg!vC^R+IXq@
zi*UrjP&S}dD2ogZG`^d{8HI@`g_8ESRshosu{f!g=!eJ+v{qUbW=G4;5`H&2|S
z1n8@u5_vHiw9{?t7O5Wv7_TCAUYT5T0YabNeJ1T-;){*`$b!>{TfLM9O&lXb8!*5r
zutYJ1TehWEWni8nlx5v_GfWI~Skp{$pTNTP7t41HlNV}O28(Or2n-JB^Cbs|;n`ZD
zWtQwqMkc6ZmM$acENOxGbgmM>_f!Loq;
zk(3;w#dMSN1(#Y;JSm1yOXyfkj!+zNwjarP#n~D$-LXBB_Ze22vXJf+7*ytNSFh6J
z{rVSX;JiE+rrZBZAluxC=EYc~rBXo=S58qf@8`Lf9Hk8c#_mJQV5f?SQ7h)5E%%*?
z28X8Nx1G!5F_ON)M)r5}f%Ai*Ek-0M(7JwE`lFXX1OYFq#yVlU_hc*BFO+9;Zc_4D
zRmbuS+q;=)czcLwH@dhTbm)uxrM=Bi-!3^|pA?|6n>tq=L~(!3(}d0+c{u=6w$kx`
zaiJ2~HE-G6#fLK}&4?S&WMu-5870NZyj^vkHaFkTDBzyh1)cHzi*BrFV6N(i!y{c$5Zd7krtfQA&7X9%16_D+6_kNUH
qPH(1;)FU?T{rTv>JsXN|v3LERm2Kfm76)#zAfsdE`uBC6BK`+8RverF
literal 0
HcmV?d00001
diff --git a/views/index.php b/views/index.php
new file mode 100644
index 0000000..55c9b2c
--- /dev/null
+++ b/views/index.php
@@ -0,0 +1,20 @@
+
+
+ {$hutko_description|escape:'htmlall'}
+
+
+{/block}
\ No newline at end of file
diff --git a/views/templates/index.php b/views/templates/index.php
new file mode 100644
index 0000000..55c9b2c
--- /dev/null
+++ b/views/templates/index.php
@@ -0,0 +1,20 @@
+