first commit

This commit is contained in:
O K
2025-05-29 10:54:49 +03:00
commit cae15fb881
17 changed files with 1604 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
/**
* Class HutkoCallbackModuleFrontController
*
* This front controller handles the asynchronous callback notifications from the Hutko payment gateway.
* It is responsible for validating the payment response, updating the order status in PrestaShop,
* and handling various payment statuses (approved, declined, expired, processing).
* It also incorporates logic to mitigate race conditions with the customer's return to the result page.
*
* @property \Hutko $module An instance of the Hutko module.
*/
class HutkoCallbackModuleFrontController extends ModuleFrontController
{
/**
* Handles the post-processing of the payment gateway callback.
*
* This method is the entry point for Hutko's server-to-server notifications.
* It performs the following steps:
* 1. Parses the incoming request body (from POST or raw input).
* 2. Validates the integrity of the request using the module's signature validation.
* 3. Extracts the cart ID and loads the corresponding cart.
* 4. Checks if the order already exists for the cart. If not, it attempts to validate
* and create the order, using `postponeCallback` to manage potential race conditions.
* 5. Based on the `order_status` received in the callback, it updates the PrestaShop
* order's status (e.g., to success, error, or processing).
* 6. Logs all significant events and errors using PrestaShopLogger.
* 7. Exits with a simple string response ('OK' or an error message) as expected by
* payment gateways.
*
* @return void
*/
public function postProcess(): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit;
}
try {
// 1. Parse the incoming request body.
$requestBody = $this->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 [];
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

View File

@@ -0,0 +1,47 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
/**
* Class HutkoRedirectModuleFrontController
*
* @property \Hutko $module
*/
class HutkoRedirectModuleFrontController extends ModuleFrontController
{
/**
* Initializes the content of the redirect page for the Hutko payment gateway.
*
* This method is responsible for preparing the necessary data and assigning
* it to the Smarty template that handles the redirection to the Hutko payment
* service. It calls the parent class's `initContent` method first and then
* assigns the Hutko checkout URL and the payment input parameters to the template.
*/
public function initContent()
{
// Call the parent class's initContent method to perform default initializations.
parent::initContent();
// Assign Smarty variables to be used in the redirect template.
$this->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');
}
}

View File

@@ -0,0 +1,185 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
/**
* Class HutkoResultModuleFrontController
* Front Controller for handling the result of a Hutko payment.
*
* This class processes the response from the Hutko payment gateway after a customer
* has attempted a payment. It validates the incoming parameters, handles different
* payment statuses (approved, declined, processing, expired), and redirects the
* customer accordingly to the order confirmation page, order history, or back
* to the order page with relevant notifications.
*
* @property Hutko $module An instance of the Hutko module.
*/
class HutkoResultModuleFrontController extends ModuleFrontController
{
/**
* Handles the post-processing of the payment gateway response.
*
* This method retrieves payment status and order details from the request,
* performs necessary validations, and then takes action based on the
* payment status:
* - 'declined' or 'expired': Adds an error and redirects to the order page.
* - 'processing': Periodically checks for order creation (up to PHP execution timeout)
* and redirects to confirmation if found, or adds an error if not.
* - 'approved': Validates the order (if not already created) and redirects
* to the order confirmation page.
* - Any other status: Redirects to the order history or order page with errors.
*/
public function postProcess(): void
{
// Retrieve essential parameters from the request.
$orderStatus = Tools::getValue('order_status', false);
$transaction_id = Tools::getValue('order_id', false); // This is the combined cart_id|timestamp
$amountReceived = round((float)Tools::getValue('amount', 0) / 100, 2);
// Basic validation: If critical parameters are missing, redirect to home.
if (!$transaction_id || !$orderStatus || !$amountReceived) {
Tools::redirect('/');
}
// Extract cart ID from the combined order_id parameter.
// The order_id is expected to be in the format "cartID|timestamp".
$cartIdParts = explode($this->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));
}
}
}

20
controllers/index.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

688
hutko.php Normal file
View File

@@ -0,0 +1,688 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
use PrestaShop\PrestaShop\Core\Payment\PaymentOption;
if (!defined('_PS_VERSION_')) {
exit;
}
class Hutko extends PaymentModule
{
public $order_separator = '#';
public $checkout_url = 'https://pay.hutko.org/api/checkout/redirect/';
private $settingsList = [
'HUTKO_MERCHANT',
'HUTKO_SECRET_KEY',
'HUTKO_BACK_REF',
'HUTKO_SUCCESS_STATUS_ID',
'HUTKO_SHOW_CARDS_LOGO'
];
private $postErrors = [];
public function __construct()
{
$this->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' => '<i class="icon icon-user"></i>',
'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' => '<i class="icon icon-key"></i>',
'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' => '<i class="icon icon-key"></i>',
'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);
}
}
}

21
index.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

199
readme.md Normal file
View File

@@ -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`.

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="80"
height="18"
viewBox="0 0 211.66667 52.916666"
version="1.1"
id="svg470"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="drawing.svg">
<defs
id="defs464" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.49497475"
inkscape:cx="103.0113"
inkscape:cy="287.73146"
inkscape:document-units="mm"
inkscape:current-layer="g396"
showgrid="false"
units="px"
inkscape:window-width="1848"
inkscape:window-height="1016"
inkscape:window-x="72"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata467">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-244.08334)">
<g
id="g396"
transform="matrix(0.05940458,0,0,-0.05940458,-41.5994,95.616925)">
<path
d="m 1841.8697,-2678.1649 c 54.2854,0 98.151,-12.1225 125.7888,-23.0618 l -19.0531,-117.2041 -12.6901,5.7593 c -25.3678,10.9878 -58.8616,21.9269 -104.4658,20.7919 -55.3723,0 -80.1845,-23.6775 -80.7884,-46.7876 0,-25.3678 30.0406,-42.1147 79.086,-66.9753 80.8004,-38.6736 118.2907,-85.9778 117.7232,-147.764 -1.1229,-112.5772 -96.9193,-185.3626 -244.0796,-185.3626 -62.9065,0.6132 -123.5188,13.9238 -156.4451,28.9176 l 19.6205,121.7789 18.4977,-8.6306 c 45.6043,-20.2388 75.5965,-28.8706 131.5842,-28.8706 40.3763,0 83.6741,16.7492 84.2419,53.1132 0,23.681 -18.4494,40.9809 -72.735,67.5442 -53.054,25.9836 -124.0381,69.2816 -122.9031,147.2444 0.6037,105.6492 98.1389,179.5071 236.6179,179.5071 z m -522.8242,-552.4916 h 140.2295 l 87.7188,542.6874 h -140.2174 z m 835.0624,192.2487 c 11.5549,31.1707 55.976,151.8182 55.976,151.8182 -0.2294,-0.4709 1.6057,4.6245 4.3467,12.2192 4.0086,11.1807 9.9974,27.7344 14.1026,39.7602 l 9.8041,-46.7874 c 0,0 26.5512,-129.8952 32.3105,-157.0102 z m 173.0954,350.4387 h -108.4864 c -33.4335,0 -58.8617,-9.8163 -73.2903,-45.049 l -208.328,-497.6384 h 147.1481 c 0,0 24.2449,66.9791 29.4367,81.4065 h 180.0746 c 4.0569,-19.0545 16.7468,-81.4065 16.7468,-81.4065 h 129.8458 z m -1125.362,0 -137.3438,-370.0594 -14.9986,75.0387 -0.046,0.1461 0.046,-0.1932 -49.0418,249.4513 c -8.06544,34.6651 -32.87803,44.4814 -63.48851,45.6165 h -225.6411 l -2.30617,-10.9998 c 55.04619,-14.0543 104.23399,-34.3269 147.38952,-59.5619 l 124.99204,-471.5547 H 1129.686 l 220.4505,542.1164 z"
style="fill:#0b589f;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.20741808"
id="path424"
inkscape:connector-curvature="0" />
<path
d="m 3341.2415,-3293.9846 h 417.0313 v 686.526 h -417.0313 z"
style="fill:#ff5f00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20741808"
id="path426"
inkscape:connector-curvature="0" />
<path
d="m 3384.2136,-2950.776 c -0.047,66.1304 14.8027,131.3551 43.4789,190.8326 28.628,59.4291 70.3685,111.5412 121.9252,152.3882 -63.8603,50.6029 -140.6039,82.0683 -221.3923,90.7859 -80.8366,8.7778 -162.4339,-5.6145 -235.543,-41.4024 -73.1092,-35.8001 -134.6996,-91.5948 -177.8164,-161.0698 -43.105,-69.4265 -65.9734,-149.7076 -65.9734,-231.5827 0,-81.9245 22.8684,-162.2034 65.9734,-231.6298 43.1168,-69.4266 104.7072,-125.2227 177.8164,-161.0588 73.1091,-35.8024 154.7064,-50.1452 235.543,-41.4145 80.7884,8.7297 157.532,40.2312 221.3923,90.834 -51.605,40.7976 -93.2972,92.9714 -121.9734,152.4368 -28.6761,59.4798 -43.5273,124.7503 -43.4307,190.8805 z"
style="fill:#eb001b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20741808"
id="path428"
inkscape:connector-curvature="0" />
<path
d="m 4209.0733,-3221.3087 v 14.0555 h 5.9889 v 2.9195 h -14.2959 v -2.9195 h 5.6627 v -14.0555 z m 27.7826,0 v 16.975 h -4.3466 l -5.047,-12.1213 -5.047,12.1213 h -4.3347 v -16.975 h 3.1151 v 12.8202 l 4.721,-11.0323 h 3.2481 l 4.6726,11.0323 v -12.8202 z"
style="fill:#f79e1b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20741808"
id="path430"
inkscape:connector-curvature="0" />
<path
d="m 4250.4877,-2950.776 c 0,-81.9256 -22.8806,-162.2045 -66.0338,-231.631 -43.1046,-69.4265 -104.7556,-125.2697 -177.8647,-161.0576 -73.0971,-35.7515 -154.6944,-50.1464 -235.4827,-41.3636 -80.8487,8.7151 -157.58,40.2263 -221.4406,90.8812 51.5447,40.8445 93.237,92.9578 121.9132,152.4365 28.6761,59.4279 43.5878,124.6999 43.5878,190.7829 0,66.1183 -14.9117,131.355 -43.5878,190.8325 -28.6762,59.4653 -70.3685,111.5896 -121.9132,152.4365 63.8606,50.6513 140.5919,82.1045 221.4406,90.8823 80.7883,8.7297 162.3856,-5.6146 235.4827,-41.4143 73.1091,-35.8 134.7601,-91.5949 177.8647,-161.0213 43.1532,-69.4749 66.0338,-149.6957 66.0338,-231.6191 z"
style="fill:#f79e1b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20741808"
id="path432"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

20
views/img/index.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

BIN
views/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

20
views/index.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

View File

@@ -0,0 +1,14 @@
{*
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*}
<p class="payment_module">
<img src="{$hutko_logo_path}" width="150" height="50" alt="Hutko logo"/><br/>
{$hutko_description|escape:'htmlall'}
</p>

View File

@@ -0,0 +1,20 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

View File

@@ -0,0 +1,29 @@
{*
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*}
{extends "$layout"}
{block name="content"}
{l s='You will be redirected to the website in a few seconds.' d='Modules.Hutko.Shop'}
<div class="form-group">
<form id="hutko_redirect" method="POST" action="{$hutko_url}" accept-charset="utf-8">
{foreach from=$hutko_inputs item=item key=key name=name}
<input type="hidden" name="{$key|escape:'htmlall'}" value="{$item|escape:'htmlall'}" />
{/foreach}
<button class="btn btn-primary"
type="submit">{l s='Go to payment (if auto redirect doesn`t work)' d='Modules.Hutko.Shop'}</button>
</form>
<div>
<script>
document.getElementById("hutko_redirect").submit();
</script>
{/block}

20
views/templates/index.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;