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^17BRp&#zXJ~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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + 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^17BRp&#zXJ~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 logo
+ {$hutko_description|escape:'htmlall'} +

diff --git a/views/templates/front/index.php b/views/templates/front/index.php new file mode 100644 index 0000000..55c9b2c --- /dev/null +++ b/views/templates/front/index.php @@ -0,0 +1,20 @@ + +
+ + {foreach from=$hutko_inputs item=item key=key name=name} + + {/foreach} + +
+
+ +{/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 @@ +