first commit
This commit is contained in:
214
controllers/front/callback.php
Normal file
214
controllers/front/callback.php
Normal 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 [];
|
||||
}
|
||||
}
|
||||
20
controllers/front/index.php
Normal file
20
controllers/front/index.php
Normal 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;
|
||||
47
controllers/front/redirect.php
Normal file
47
controllers/front/redirect.php
Normal 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');
|
||||
}
|
||||
}
|
||||
185
controllers/front/result.php
Normal file
185
controllers/front/result.php
Normal 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
20
controllers/index.php
Normal 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
688
hutko.php
Normal 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
21
index.php
Normal 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;
|
||||
199
readme.md
Normal file
199
readme.md
Normal 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`.
|
||||
87
views/img/hutko_logo_cards.svg
Normal file
87
views/img/hutko_logo_cards.svg
Normal 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
20
views/img/index.php
Normal 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
BIN
views/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
20
views/index.php
Normal file
20
views/index.php
Normal 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;
|
||||
14
views/templates/front/hutko.tpl
Normal file
14
views/templates/front/hutko.tpl
Normal 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>
|
||||
20
views/templates/front/index.php
Normal file
20
views/templates/front/index.php
Normal 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;
|
||||
29
views/templates/front/redirect.tpl
Normal file
29
views/templates/front/redirect.tpl
Normal 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
20
views/templates/index.php
Normal 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;
|
||||
Reference in New Issue
Block a user