1369 lines
57 KiB
PHP
1369 lines
57 KiB
PHP
<?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 $redirect_url = 'https://pay.hutko.org/api/checkout/redirect/';
|
||
public $checkout_url = 'https://pay.hutko.org/api/checkout/url/';
|
||
public $refund_url = 'https://pay.hutko.org/api/reverse/order_id';
|
||
public $status_url = 'https://pay.hutko.org/api/status/order_id';
|
||
|
||
|
||
public $settingsList = [
|
||
'HUTKO_MERCHANT',
|
||
'HUTKO_SECRET_KEY',
|
||
'HUTKO_SHIPPING_INCLUDE',
|
||
'HUTKO_SHIPPING_PRODUCT_NAME',
|
||
'HUTKO_SHIPPING_PRODUCT_CODE',
|
||
'HUTKO_NEW_ORDER_STATUS_ID',
|
||
'HUTKO_SUCCESS_STATUS_ID',
|
||
'HUTKO_SHOW_CARDS_LOGO',
|
||
'HUTKO_SAVE_LOGS',
|
||
'HUTKO_INCLUDE_DISCOUNT_TO_TOTAL'
|
||
];
|
||
public $postErrors = [];
|
||
|
||
public function __construct()
|
||
{
|
||
$this->name = 'hutko';
|
||
$this->tab = 'payments_gateways';
|
||
$this->version = '1.3.0';
|
||
$this->author = 'Hutko';
|
||
$this->bootstrap = true;
|
||
parent::__construct();
|
||
$this->ps_versions_compliancy = array('min' => '1.7', 'max' => _PS_VERSION_);
|
||
//Do not translate displayName as it is used for payment identification
|
||
$this->displayName = 'Hutko Payments';
|
||
$this->description = $this->trans('Hutko is a payment platform whose main function is to provide internet acquiring.', array(), 'Modules.Hutko.Admin');
|
||
}
|
||
|
||
public function install(): bool
|
||
{
|
||
$success = parent::install()
|
||
&& $this->registerHook('paymentOptions');
|
||
|
||
// If the initial mandatory hooks failed, stop here.
|
||
if (!$success) {
|
||
return false;
|
||
}
|
||
|
||
// Now, conditionally register the order content/tab hook based on PS version.
|
||
// We only need to check if the *required* hook for this version was successfully registered.
|
||
$conditionalHookSuccess = true; // Assume success until we try to register one and it fails
|
||
|
||
// Check if PrestaShop version is 1.7.x (>= 1.7 and < 1.7.7)
|
||
if (version_compare(_PS_VERSION_, '1.7', '>=') && version_compare(_PS_VERSION_, '1.7.7', '<')) {
|
||
// Register the 1.7 hook
|
||
$conditionalHookSuccess = $this->registerHook('displayAdminOrderContentOrder');
|
||
}
|
||
// Check if PrestaShop version is 1.7.7 or 9.x (>= 1.7.7 and < 10.0)
|
||
elseif (version_compare(_PS_VERSION_, '1.7.7', '>=') && version_compare(_PS_VERSION_, '10.0', '<')) {
|
||
// Register the 8.x/9.x hook
|
||
$conditionalHookSuccess = $this->registerHook('displayAdminOrderTabContent');
|
||
}
|
||
// Note: If it's a different version (e.g., 1.6, 10.0+), neither of these specific hooks will be registered,
|
||
// and $conditionalHookSuccess remains true, which is the desired behavior based on the requirement.
|
||
|
||
// The module installation is successful only if the initial hooks passed AND the conditional hook (if attempted) passed.
|
||
return $success && $conditionalHookSuccess;
|
||
}
|
||
|
||
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() . $this->context->smarty->fetch('module:hutko/views/templates/admin/help.tpl').$this->displayLastDayLog();
|
||
}
|
||
|
||
/**
|
||
* Create the form that will be displayed in the configuration of your module.
|
||
*/
|
||
public 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.
|
||
*/
|
||
public function getConfigForm()
|
||
{
|
||
|
||
|
||
$options = [];
|
||
|
||
foreach (OrderState::getOrderStates($this->context->language->id) 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. Use 1700002 for test setup.', 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. Use "test" for test setup', 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' => 'select',
|
||
'prefix' => '<i class="icon icon-key"></i>',
|
||
'name' => 'HUTKO_NEW_ORDER_STATUS_ID',
|
||
'label' => $this->trans('Status for new orders before payment', array(), 'Modules.Hutko.Admin'),
|
||
'options' => array(
|
||
'query' => $options,
|
||
'id' => 'status_id',
|
||
'name' => 'name'
|
||
)
|
||
),
|
||
array(
|
||
'type' => 'radio',
|
||
'name' => 'HUTKO_SHIPPING_INCLUDE',
|
||
'label' => $this->trans('Include shipping cost to payment', array(), 'Modules.Hutko.Admin'),
|
||
'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')
|
||
)
|
||
),
|
||
),
|
||
array(
|
||
'type' => 'text',
|
||
'name' => 'HUTKO_SHIPPING_PRODUCT_NAME',
|
||
'label' => $this->trans('Shipping Name', array(), 'Modules.Hutko.Admin'),
|
||
'desc' => $this->trans('Name of product/service to use in fiscalization for shipping amount', array(), 'Modules.Hutko.Admin'),
|
||
|
||
),
|
||
array(
|
||
'type' => 'text',
|
||
'name' => 'HUTKO_SHIPPING_PRODUCT_CODE',
|
||
'label' => $this->trans('Shipping Code', array(), 'Modules.Hutko.Admin'),
|
||
'desc' => $this->trans('Code of product/service to use in fiscalization for shipping amount', array(), 'Modules.Hutko.Admin'),
|
||
|
||
),
|
||
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')
|
||
)
|
||
),
|
||
),
|
||
array(
|
||
'type' => 'radio',
|
||
'label' => $this->trans('Include fixed amount discounts to Payment amount calculation', array(), 'Modules.Hutko.Admin'),
|
||
'name' => 'HUTKO_INCLUDE_DISCOUNT_TO_TOTAL',
|
||
'is_bool' => true,
|
||
'values' => array(
|
||
array(
|
||
'id' => 'include_discount_to_total',
|
||
'value' => 1,
|
||
'label' => $this->trans('Yes', array(), 'Modules.Hutko.Admin')
|
||
),
|
||
array(
|
||
'id' => 'discard_discount_to_total',
|
||
'value' => 0,
|
||
'label' => $this->trans('No', array(), 'Modules.Hutko.Admin')
|
||
)
|
||
),
|
||
),
|
||
array(
|
||
'type' => 'radio',
|
||
'label' => $this->trans('Save Logs', array(), 'Modules.Hutko.Admin'),
|
||
'name' => 'HUTKO_SAVE_LOGS',
|
||
'is_bool' => true,
|
||
'values' => array(
|
||
array(
|
||
'id' => 'save_logs',
|
||
'value' => 1,
|
||
'label' => $this->trans('Yes', array(), 'Modules.Hutko.Admin')
|
||
),
|
||
array(
|
||
'id' => 'discard_logs',
|
||
'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.
|
||
*/
|
||
public function getConfigFormValues(): array
|
||
{
|
||
foreach ($this->settingsList as $settingName) {
|
||
$list[$settingName] = Configuration::get($settingName);
|
||
}
|
||
return $list;
|
||
}
|
||
|
||
/**
|
||
* Save form data.
|
||
*/
|
||
public function postProcess()
|
||
{
|
||
foreach ($this->settingsList as $settingName) {
|
||
Configuration::updateValue($settingName, Tools::getValue($settingName));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
public 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 buildPaymentRequestData(Order $order, ?float $amount, ?Currency $currency, ?Customer $customer): array
|
||
{
|
||
// 1. Generate a unique order ID combining the cart ID and current timestamp.
|
||
$orderId = $order->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('Order payment #', [], 'Modules.Hutko.Admin') . $order->reference;
|
||
// 4. Calculate the order amount in the smallest currency unit.
|
||
if (!$amount) {
|
||
if (Configuration::get('HUTKO_SHIPPING_INCLUDE') && $order->total_shipping_tax_incl > 0) {
|
||
$amount = $order->total_products_wt + $order->total_shipping_tax_incl;
|
||
} else {
|
||
$amount = $order->total_products_wt;
|
||
}
|
||
}
|
||
if (Configuration::get('HUTKO_INCLUDE_DISCOUNT_TO_TOTAL')) {
|
||
$amount -= $order->total_discounts_tax_incl;
|
||
}
|
||
|
||
|
||
|
||
$amountInt = round($amount * 100);
|
||
// 5. Get the currency ISO code of the current cart.
|
||
if (!$currency) {
|
||
$currency = new Currency($order->id_currency);
|
||
}
|
||
$currencyISO = $currency->iso_code;
|
||
|
||
// 6. Generate the server callback URL.
|
||
$serverCallbackUrl = $this->context->link->getModuleLink($this->name, 'callback', [], true);
|
||
// 7. Retrieve the customer's email address.
|
||
if (!$customer) {
|
||
$customer = new Customer($order->id_customer);
|
||
}
|
||
$customerEmail = $customer->email;
|
||
|
||
// 8. Generate the customer redirection URL after payment.
|
||
$responseUrl = $this->context->link->getModuleLink($this->name, 'return', ['hutkoPV' => $this->urlSafeEncode(json_encode([
|
||
'id_cart' => $order->id_cart,
|
||
'id_module' => $this->id,
|
||
'id_order' => $order->id,
|
||
'key' => $customer->secure_key,
|
||
]))], true);
|
||
|
||
|
||
|
||
// 9. Build the reservation data as a base64 encoded JSON string.
|
||
$reservationData = $this->buildReservationData($order);
|
||
|
||
// 10. Construct the data array with all the collected parameters.
|
||
$data = [
|
||
'order_id' => $orderId,
|
||
'merchant_id' => $merchantId,
|
||
'order_desc' => $orderDescription,
|
||
'amount' => $amountInt,
|
||
'currency' => $currencyISO,
|
||
'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);
|
||
|
||
// 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(Order $order): string
|
||
{
|
||
// 1. Retrieve the delivery address for the current cart.
|
||
$address = new Address((int)$order->id_address_delivery, $order->id_lang);
|
||
|
||
// 2. Fetch the customer's state name, if available.
|
||
$customerState = '';
|
||
if ($address->id_state) {
|
||
$state = new State((int) $address->id_state, $order->id_lang);
|
||
$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" => empty($address->phone_mobile) ? $address->phone : $address->phone_mobile,
|
||
"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" => $order->id_customer,
|
||
"uuid" => hash('sha256', _COOKIE_KEY_ . Tools::getShopDomainSsl()),
|
||
"products" => $this->getProducts($order),
|
||
];
|
||
|
||
|
||
|
||
|
||
return base64_encode(json_encode($data));
|
||
}
|
||
|
||
|
||
/**
|
||
* 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(Order $order): array
|
||
{
|
||
$products = [];
|
||
foreach ($order->getProductsDetail() as $productDetail) {
|
||
$products[] = [
|
||
"id" => $productDetail['product_id'] . '_' . $productDetail['product_attribute_id'] . '_' . $productDetail['id_customization'],
|
||
"name" => $productDetail['product_name'],
|
||
"price" => round((float)$productDetail['unit_price_tax_incl'], 2),
|
||
"total_amount" => round((float) $productDetail['total_price_tax_incl'], 2),
|
||
"quantity" => (int)$productDetail['product_quantity'],
|
||
];
|
||
}
|
||
|
||
if (Configuration::get('HUTKO_SHIPPING_INCLUDE') && $order->total_shipping_tax_incl > 0) {
|
||
$products[] = [
|
||
"id" => Configuration::get('HUTKO_SHIPPING_PRODUCT_CODE', null, null, null, '0_0_1'),
|
||
"name" => Configuration::get('HUTKO_SHIPPING_PRODUCT_NAME', null, null, null, 'Service Fee'),
|
||
"price" => round((float)$order->total_shipping_tax_incl, 2),
|
||
"total_amount" => round((float) $order->total_shipping_tax_incl, 2),
|
||
"quantity" => 1,
|
||
];
|
||
}
|
||
return $products;
|
||
}
|
||
|
||
|
||
|
||
|
||
/**
|
||
* 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, bool $encoded = true): string
|
||
{
|
||
$password = Configuration::get('HUTKO_SECRET_KEY');
|
||
if (!$password || empty($password)) {
|
||
throw new PrestaShopException('Merchant secret not set');
|
||
}
|
||
// 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 ((string)Configuration::get('HUTKO_MERCHANT') != (string)$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);
|
||
|
||
return hash_equals($calculatedSignature, $responseSignature);
|
||
}
|
||
|
||
|
||
|
||
|
||
/**
|
||
* 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): 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();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Hook implementation for PrestaShop 1.7.x to display content in the admin order page.
|
||
*
|
||
* This hook is typically used to add content *below* the main order details
|
||
* but before the tabbed section. It's often used for specific sections
|
||
* rather than entire tabs in 1.7. However, in this case, it's likely
|
||
* being used as a fallback or alternative for displaying the payment content
|
||
* depending on the module's design or compatibility needs for 1.7.
|
||
*
|
||
* @param array $params Contains context information, including the 'order' object.
|
||
* @return string The HTML content to be displayed in the admin order page.
|
||
*/
|
||
public function hookdisplayAdminOrderContentOrder(array $params)
|
||
{
|
||
// Delegate the actual content generation to a shared function
|
||
// to avoid code duplication.
|
||
return $this->displayAdminOrderContent($params);
|
||
}
|
||
|
||
|
||
/**
|
||
* Hook implementation for PrestaShop 8.x and 9.x to display content
|
||
* within a specific tab on the admin order page.
|
||
*
|
||
* This hook is the standard way in newer PS versions to add a custom tab
|
||
* and populate its content on the order detail page.
|
||
*
|
||
* @param array $params Contains context information, including the 'order' object.
|
||
* @return string The HTML content to be displayed within the module's custom tab.
|
||
*/
|
||
public function hookdisplayAdminOrderTabContent(array $params)
|
||
{
|
||
$params['order'] = new Order((int) $params['id_order']);
|
||
// Delegate the actual content generation to a shared function
|
||
// to avoid code duplication.
|
||
return $this->displayAdminOrderContent($params);
|
||
}
|
||
|
||
|
||
/**
|
||
* Common function to display content related to Hutko payments on the admin order page.
|
||
*
|
||
* This function handles the logic for processing potential form submissions (like refunds
|
||
* or status updates) and preparing data to be displayed in a template.
|
||
*
|
||
* It is called by different hooks depending on the PrestaShop version
|
||
* (displayAdminOrderContentOrder for 1.7.x, displayAdminOrderTabContent for 8.x/9.x).
|
||
*
|
||
* @param array $params Contains context information from the hook, typically including the 'order' object.
|
||
* @return string The rendered HTML content from the template.
|
||
*/
|
||
public function displayAdminOrderContent(array $params): string
|
||
{
|
||
if (!Configuration::get('HUTKO_MERCHANT') || empty(Configuration::get('HUTKO_MERCHANT') || !Configuration::get('HUTKO_SECRET_KEY') || empty(Configuration::get('HUTKO_SECRET_KEY')))) {
|
||
return '';
|
||
}
|
||
|
||
// Ensure the 'order' object is present in the parameters
|
||
if (!isset($params['order']) || !$params['order'] instanceof Order) {
|
||
// Log an error or return an empty string if the order object is missing
|
||
// depending on how critical it is. Returning empty string is safer
|
||
// to avoid crashing the admin page.
|
||
PrestaShopLogger::addLog(
|
||
'Hutko Module: Order object missing in displayAdminOrderContent hook.',
|
||
1, // Error level
|
||
null,
|
||
'Module',
|
||
(int)$this->id
|
||
);
|
||
return '';
|
||
}
|
||
|
||
// Check if a refund form has been submitted
|
||
if (Tools::isSubmit('hutkoRefundsubmit')) {
|
||
// Process the refund logic.
|
||
$this->processRefundForm();
|
||
}
|
||
|
||
// Check payment status
|
||
if (Tools::getValue('hutkoOrderPaymentStatus')) {
|
||
|
||
// Process the requested order status check.
|
||
$this->renderOrderPaymentStatus(Tools::getValue('hutkoOrderPaymentStatus'));
|
||
}
|
||
|
||
// Get the Order object from the parameters
|
||
$order = $params['order'];
|
||
|
||
// Fetch all OrderPayment records associated with this order
|
||
// that were processed specifically by this payment module (based on display name)
|
||
// and have a transaction ID matching a specific pattern (order ID prefix).
|
||
// The transaction_id pattern suggests it's linked to the order ID for easy lookup.
|
||
$hutkoPayments = new PrestaShopCollection('OrderPayment');
|
||
$hutkoPayments->where('order_reference', '=', $order->reference); // Filter by order reference
|
||
$hutkoPayments->where('payment_method', '=', $this->displayName); // Filter by this module's payment method name
|
||
// Filter by transaction ID pattern: Starts with the order ID followed by the configured separator.
|
||
// This assumes transaction IDs generated by this module follow this format.
|
||
$hutkoPayments->where('transaction_id', 'like', '' . $order->id . $this->order_separator . '%');
|
||
|
||
// Assign data to Smarty to be used in the template
|
||
$this->context->smarty->assign([
|
||
// Pass the fetched Hutko payment records to the template as an array
|
||
'hutkoPayments' => $hutkoPayments,
|
||
// Pass the order ID to the template
|
||
'id_order' => $order->id,
|
||
'currency' => new Currency($order->id_currency),
|
||
'hutkoOrderPaymentStatus' => $this->context->session->get('hutkoOrderPaymentStatus'),
|
||
]);
|
||
// Render the template located at 'views/templates/admin/order_payment_refund.tpl'
|
||
// This template will display the fetched payment information and potentially refund/status forms.
|
||
return $this->display(__FILE__, 'views/templates/admin/order_payment_refund.tpl');
|
||
}
|
||
public function renderOrderPaymentStatus(string $order_id)
|
||
{
|
||
|
||
$data = $this->getOrderPaymentStatus($order_id);
|
||
|
||
$this->context->session->set('hutkoOrderPaymentStatus', $this->displayArrayInNotification($data['response']));
|
||
Tools::redirectAdmin($this->context->controller->currentIndex);
|
||
}
|
||
public function getOrderPaymentStatus(string $order_id): array
|
||
{
|
||
$data = [
|
||
'order_id' => $order_id,
|
||
'merchant_id' => Configuration::get('HUTKO_MERCHANT'),
|
||
'version' => '1.0',
|
||
];
|
||
$data['signature'] = $this->getSignature($data);
|
||
|
||
return $this->sendAPICall($this->status_url, $data);
|
||
}
|
||
|
||
public function processRefundForm()
|
||
{
|
||
$orderPaymentId = (int) Tools::getValue('orderPaymentId');
|
||
$amount = (float) Tools::getValue('refund_amount');
|
||
$comment = mb_substr(Tools::getValue('comment', ''), 0, 1024);
|
||
$id_order = (int) Tools::getValue('id_order');
|
||
$result = $this->processRefund($orderPaymentId, $id_order, $amount, $comment);
|
||
$link = $this->context->link->getAdminLink('AdminOrders', true, [], ['id_order' => $id_order, 'vieworder' => true]);
|
||
if ($result->error) {
|
||
$this->context->controller->errors[] = $result->description;
|
||
}
|
||
if ($result->success) {
|
||
$this->context->controller->informations[] = $result->description;
|
||
}
|
||
Tools::redirectAdmin($link);
|
||
}
|
||
|
||
/**
|
||
* Processes a payment refund via the Hutko gateway and updates PrestaShop order.
|
||
*
|
||
* This method initiates a refund request to the Hutko payment gateway for a specific
|
||
* order payment. Upon successful refund from Hutko, it creates an OrderSlip (if partial),
|
||
* updates the order history, and logs the action.
|
||
*
|
||
* @param int $orderPaymentId The ID of the OrderPayment record to refund.
|
||
* @param int $id_order The ID of the Order to refund.
|
||
* @param float $amount The amount to refund.
|
||
* @param string $comment A comment or reason for the refund.
|
||
* @return stdClass Result description.
|
||
* @throws Exception If the OrderPayment is not found, invalid, or refund fails.
|
||
*/
|
||
public function processRefund(int $orderPaymentId, int $id_order, float $amount, string $comment = ''): stdClass
|
||
{
|
||
$result = new stdClass();
|
||
$result->error = false;
|
||
|
||
$order = new Order($id_order);
|
||
$orderPayment = new OrderPayment($orderPaymentId);
|
||
$currency = new Currency($orderPayment->id_currency);
|
||
if (!Validate::isLoadedObject($orderPayment)) {
|
||
PrestaShopLogger::addLog(
|
||
'Hutko Refund: OrderPayment object not found for ID: ' . $orderPaymentId,
|
||
3, // Error
|
||
null,
|
||
'OrderPayment',
|
||
$orderPaymentId,
|
||
true
|
||
);
|
||
throw new Exception($this->trans('Order payment not found.', [], 'Modules.Hutko.Admin'));
|
||
}
|
||
|
||
// Validate the transaction_id format and extract cart ID.
|
||
// Assuming transaction_id is in the format "orderID#timestamp"
|
||
$transactionIdParts = explode($this->order_separator, $orderPayment->transaction_id);
|
||
$cartId = (int)$transactionIdParts[0];
|
||
|
||
if (!$cartId) {
|
||
PrestaShopLogger::addLog(
|
||
'Hutko Refund: Invalid transaction ID format for OrderPayment ID: ' . $orderPaymentId . ' Transaction ID: ' . $orderPayment->transaction_id,
|
||
3, // Error
|
||
null,
|
||
'OrderPayment',
|
||
$orderPaymentId,
|
||
true
|
||
);
|
||
throw new Exception($this->trans('Invalid transaction ID format.', [], 'Modules.Hutko.Admin'));
|
||
}
|
||
|
||
|
||
$response = $this->refundAPICall($orderPayment->transaction_id, $amount, $currency->iso_code, $comment);
|
||
if ($response['response']['reverse_status'] === 'approved' && $response['response']['response_status'] === 'success') {
|
||
$result->success = true;
|
||
$result->description = $this->trans('Refund success.', [], 'Modules.Hutko.Admin');
|
||
} else {
|
||
PrestaShopLogger::addLog(
|
||
'Hutko Refund: refund failure response: ' . json_encode($response),
|
||
3, // Info
|
||
null,
|
||
'OrderPayment',
|
||
$orderPaymentId,
|
||
true
|
||
);
|
||
$this->addOrderMessage($order, $this->trans('Refund Failed. Please check actual amount in Hutko account page.', [], 'Modules.Hutko.Admin'));
|
||
$result->error = true;
|
||
$result->description = $response['response']['error_message'];
|
||
return $result;
|
||
}
|
||
$amountFloat = round((int)$response['response']['reversal_amount'] / 100, 2);
|
||
|
||
$this->addOrderMessage($order, $this->trans('Refund success.', [], 'Modules.Hutko.Admin') . ' ' . $currency->iso_code . $amountFloat . '. [' . $comment . ']');
|
||
$order->addOrderPayment($amountFloat, $this->displayName, $orderPayment->transaction_id . '_refund', $currency);
|
||
$order->setCurrentState((int)Configuration::get('PS_OS_REFUND'));
|
||
|
||
|
||
return $result;
|
||
}
|
||
|
||
public function addOrderMessage(Order $order, string $message)
|
||
{
|
||
try {
|
||
$customer = new Customer($order->id_customer);
|
||
|
||
$thread = CustomerThread::getIdCustomerThreadByEmailAndIdOrder($customer->email, $order->id);
|
||
|
||
if (is_int((int) $thread) && $thread != 0) {
|
||
$thread = (int) $thread;
|
||
} else {
|
||
$customer_thread = new CustomerThread();
|
||
$customer_thread->id_contact = 0;
|
||
$customer_thread->id_customer = (int) $order->id_customer;
|
||
$customer_thread->id_shop = (int) $order->id_shop;
|
||
$customer_thread->id_order = (int) $order->id;
|
||
$customer_thread->id_lang = (int) $this->context->language->id;
|
||
$customer_thread->email = $customer->email;
|
||
$customer_thread->status = 'open';
|
||
$customer_thread->token = Tools::passwdGen(12);
|
||
$customer_thread->add();
|
||
$thread = $customer_thread->id;
|
||
}
|
||
|
||
|
||
|
||
$customer_message = new CustomerMessage();
|
||
$customer_message->id_customer_thread = $thread;
|
||
$customer_message->id_employee = 1;
|
||
$customer_message->message = $message;
|
||
$customer_message->private = 1;
|
||
if (false === $customer_message->validateField('message', $customer_message->message)) {
|
||
throw new Exception('Invalid reply message');
|
||
}
|
||
|
||
if (false === $customer_message->add()) {
|
||
throw new Exception('Failed to add customer message');
|
||
}
|
||
} catch (Throwable $e) {
|
||
PrestaShopLogger::addLog('failed add message to order. ' . $e->getMessage() . ' | ' . $message, 3);
|
||
}
|
||
}
|
||
|
||
|
||
public function addRefundPayment(Order $order, OrderPayment $purchaseOrderPayment, float $amount)
|
||
|
||
{
|
||
$order_payment = new OrderPayment();
|
||
$order_payment->order_reference = $order->reference;
|
||
$order_payment->id_currency = $purchaseOrderPayment->id_currency;
|
||
$order_payment->payment_method = $purchaseOrderPayment->payment_method;
|
||
$order_payment->transaction_id = $purchaseOrderPayment->transaction_id . $this->order_separator . 'refund';
|
||
$order_payment->amount = $amount;
|
||
$order_payment->id_employee = 0;
|
||
$order_payment->add();
|
||
$order->total_paid_real = $order->total_paid_real - $amount;
|
||
$order->update();
|
||
}
|
||
/**
|
||
* Initiates a refund (reverse) request via Hutko API.
|
||
*
|
||
* @param string $order_id The gateway's order ID to refund.
|
||
* @param float $amount The amount to refund (in base units, e.g., 100.50).
|
||
* @param string $currency The currency code (e.g., 'UAH').
|
||
* @param string $comment Optional comment for the refund.
|
||
* @return array Decoded API response array. Returns an error structure on failure.
|
||
*/
|
||
public function refundAPICall(string $order_id, float $amount, string $currencyISO, string $comment = ''): array
|
||
{
|
||
// 1. Prepare the data payload
|
||
$data = [
|
||
'order_id' => $order_id,
|
||
// Assuming Configuration::get is available to fetch the merchant ID
|
||
'merchant_id' => Configuration::get('HUTKO_MERCHANT'),
|
||
'version' => '1.0',
|
||
// Amount should be in minor units (cents, kopecks) and converted to string as per API example
|
||
'amount' => round($amount * 100),
|
||
'currency' => $currencyISO,
|
||
];
|
||
|
||
if (!empty($comment)) {
|
||
$data['comment'] = $comment;
|
||
}
|
||
|
||
// 2. Calculate the signature based on the data array *before* wrapping in 'request'
|
||
$data['signature'] = $this->getSignature($data);
|
||
return $this->sendAPICall($this->refund_url, $data);
|
||
}
|
||
|
||
/**
|
||
* Initiates a request via Hutko API.
|
||
*
|
||
* @param string $url The gateway's url.
|
||
* @param array $data The data.
|
||
* @return array Decoded API response array. Returns an error structure on failure.
|
||
*/
|
||
public function sendAPICall(string $url, array $data, int $timeout = 60): array
|
||
{
|
||
if (Configuration::get('HUTKO_SAVE_LOGS')) {
|
||
$this->log($url . ' <= ' . json_encode($data));
|
||
}
|
||
|
||
// Wrap the prepared data inside the 'request' key as required by the API
|
||
$requestPayload = ['request' => $data];
|
||
|
||
// Convert the payload to JSON string
|
||
$jsonPayload = json_encode($requestPayload);
|
||
|
||
if ($jsonPayload === false) {
|
||
// Handle JSON encoding error
|
||
return [
|
||
'response' => [
|
||
'response_status' => 'failure',
|
||
'error_message' => 'Failed to encode request data to JSON: ' . json_last_error_msg(),
|
||
'error_code' => 'JSON_ENCODE_ERROR'
|
||
]
|
||
];
|
||
}
|
||
|
||
// Initialize CURL
|
||
$ch = curl_init();
|
||
|
||
// 4. Set CURL options
|
||
curl_setopt($ch, CURLOPT_URL, $url);
|
||
curl_setopt($ch, CURLOPT_POST, true); // Use POST method
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonPayload); // Set the JSON body
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response as a string
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||
'Content-Type: application/json',
|
||
'Content-Length: ' . strlen($jsonPayload), // Good practice
|
||
]);
|
||
|
||
// Recommended for production: Verify SSL certificate
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // Verify hostname against certificate
|
||
|
||
|
||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // Timeout in seconds
|
||
|
||
// Execute the CURL request
|
||
$response = curl_exec($ch);
|
||
|
||
// Check for CURL errors
|
||
$curl_error = curl_error($ch);
|
||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
|
||
if ($curl_error) {
|
||
// Log the error or handle it appropriately
|
||
curl_close($ch);
|
||
return [
|
||
'response' => [
|
||
'response_status' => 'failure',
|
||
'error_message' => 'CURL Error: ' . $curl_error,
|
||
'error_code' => 'CURL_' . curl_errno($ch),
|
||
'http_code' => $http_code // Include http code for context
|
||
]
|
||
];
|
||
}
|
||
|
||
// Close CURL handle
|
||
curl_close($ch);
|
||
if (Configuration::get('HUTKO_SAVE_LOGS')) {
|
||
$this->log($url . ' => ' . $response);
|
||
}
|
||
// Process the response
|
||
// Decode the JSON response into a PHP array
|
||
$responseData = json_decode($response, true);
|
||
|
||
// Check if JSON decoding failed
|
||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||
// Log the error or handle it appropriately
|
||
return [
|
||
'response' => [
|
||
'response_status' => 'failure',
|
||
'error_message' => 'Invalid JSON response from API: ' . json_last_error_msg(),
|
||
'error_code' => 'JSON_DECODE_ERROR',
|
||
'http_code' => $http_code,
|
||
'raw_response' => $response // Include raw response for debugging
|
||
]
|
||
];
|
||
}
|
||
return $responseData;
|
||
}
|
||
|
||
|
||
/**
|
||
* Displays an array's contents in a PrestaShop notification box.
|
||
*
|
||
* This function is intended for debugging or displaying API responses/structured data
|
||
* in the PrestaShop back office notifications.
|
||
*
|
||
* @param array $data The array to display.
|
||
*/
|
||
protected function displayArrayInNotification(array $data): string
|
||
{
|
||
if (isset($data['response_signature_string'])) {
|
||
unset($data['response_signature_string']);
|
||
}
|
||
if (isset($data['additional_info'])) {
|
||
$data['additional_info_decoded'] = json_decode($data['additional_info'], true);
|
||
if (isset($data['additional_info_decoded']['reservation_data'])) {
|
||
$data['additional_info_decoded']['reservation_data_decoded'] = json_decode($data['additional_info_decoded']['reservation_data'], true);
|
||
unset($data['additional_info_decoded']['reservation_data']);
|
||
}
|
||
unset($data['additional_info']);
|
||
}
|
||
$retStr = '<ul>';
|
||
if (is_array($data)) {
|
||
foreach ($data as $key => $val) {
|
||
if (is_array($val)) {
|
||
$retStr .= '<li>' . $key . ' => ' . $this->displayArrayInNotification($val) . '</li>';
|
||
} else {
|
||
$retStr .= '<li>' . $key . ' => ' . $val . '</li>';
|
||
}
|
||
}
|
||
}
|
||
$retStr .= '</ul>';
|
||
return $retStr;
|
||
}
|
||
|
||
|
||
/**
|
||
* Check if the module uses the new translation system.
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function isUsingNewTranslationSystem()
|
||
{
|
||
return true;
|
||
}
|
||
|
||
|
||
public function log($data)
|
||
{
|
||
if (!is_string($data)) {
|
||
$data = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||
}
|
||
$logdirectory = _PS_ROOT_DIR_ . '/var/modules/' . $this->name . '/logs/' . date("Y") . '/' . date("m") . '/' . date("d") . '/';
|
||
if (!is_dir($logdirectory)) {
|
||
mkdir($logdirectory, 0750, true);
|
||
}
|
||
$logger = new \FileLogger(0); //0 == debug level, logDebug() won’t work without this.
|
||
$logger->setFilename($logdirectory . 'dayly.log');
|
||
$logger->logInfo($data);
|
||
}
|
||
public function displayLastDayLog()
|
||
{
|
||
$baseLogDir = _PS_ROOT_DIR_ . '/var/modules/' . $this->name . '/logs/';
|
||
$daysToCheck = 300; // How many recent days to check for logs
|
||
|
||
$latestLogFile = false;
|
||
$logDate = null;
|
||
|
||
// Find the latest log file by iterating backward from today
|
||
for ($i = 0; $i < $daysToCheck; $i++) {
|
||
// Use \DateTime for clarity as we might be in a different namespace
|
||
$date = new \DateTime("-$i days");
|
||
$year = $date->format("Y");
|
||
$month = $date->format("m");
|
||
$day = $date->format("d");
|
||
|
||
$potentialDir = $baseLogDir . $year . '/' . $month . '/' . $day . '/';
|
||
$potentialFile = $potentialDir . 'dayly.log';
|
||
|
||
if (is_dir($potentialDir) && file_exists($potentialFile)) {
|
||
$latestLogFile = $potentialFile;
|
||
$logDate = $date; // Store the date of the found log
|
||
break; // Found the latest one, stop searching
|
||
}
|
||
}
|
||
|
||
// Prepare HTML output
|
||
$html = '<div class="panel">';
|
||
// Use translation function for the heading
|
||
$html .= '<div class="panel-heading"><i class="icon-align-left"></i> ' . $this->trans('Hutko Module Logs', [], 'Modules.Hutko.Admin') . '</div>';
|
||
$html .= '<div class="panel-body">';
|
||
|
||
if ($latestLogFile) {
|
||
// Add a heading indicating the date of the log file
|
||
$html .= '<h4>' . sprintf($this->trans('Log file for %s', [], 'Modules.Hutko.Admin'), $logDate->format("Y-m-d")) . '</h4>';
|
||
|
||
// Read the file line by line into an array
|
||
// FILE_IGNORE_NEW_LINES: removes the trailing newline character from each element
|
||
// FILE_SKIP_EMPTY_LINES: skips empty lines
|
||
$lines = file($latestLogFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||
|
||
if ($lines !== false) { // Check if file() succeeded
|
||
if (empty($lines)) {
|
||
$html .= '<p>' . $this->trans('The log file is empty.', [], 'Modules.Hutko.Admin') . '</p>';
|
||
} else {
|
||
// Output each line wrapped in a <p> tag
|
||
foreach ($lines as $line) {
|
||
// Use htmlspecialchars to prevent HTML injection and display characters correctly
|
||
// Add word-wrap style for long lines
|
||
$html .= '<p style="word-wrap: break-word;">' . htmlspecialchars($line, ENT_QUOTES, 'UTF-8') . '</p>';
|
||
}
|
||
}
|
||
} else {
|
||
// Error reading the file
|
||
$html .= '<div class="alert alert-warning">' . $this->trans('Could not read the log file:', [], 'Modules.Hutko.Admin') . ' ' . $latestLogFile . '</div>';
|
||
}
|
||
} else {
|
||
// No log file found in the specified range
|
||
$html .= '<div class="alert alert-info">' . sprintf($this->trans('No log file found in the last %d days.', [], 'Modules.Hutko.Admin'), $daysToCheck) . '</div>';
|
||
}
|
||
|
||
$html .= '</div>'; // End panel-body
|
||
$html .= '</div>'; // End panel
|
||
|
||
return $html;
|
||
}
|
||
|
||
/**
|
||
* URL-safe encodes a string using a Base64-like approach.
|
||
* Replaces '+' with '-', '/' with '_', and removes trailing '=' padding.
|
||
* Useful for encoding arbitrary binary data into a URL-friendly format
|
||
* that can be safely passed in URLs (e.g., within query parameters
|
||
* or path segments without needing standard percent encoding for + and /).
|
||
*
|
||
* @param string $string The string or binary data to encode.
|
||
* @return string The URL-safe encoded string.
|
||
*/
|
||
public function urlSafeEncode($string)
|
||
{
|
||
// Standard Base64 encode
|
||
$encoded = base64_encode($string);
|
||
|
||
// Replace '+' with '-' and '/' with '_'
|
||
// This avoids characters that have special meaning in URLs
|
||
$encoded = str_replace(['+', '/'], ['-', '_'], $encoded);
|
||
|
||
// Remove trailing '=' padding
|
||
// Padding is not necessary for decoding if the length is known or can be inferred.
|
||
// Removing it makes the string shorter and avoids another problematic character (=).
|
||
$encoded = rtrim($encoded, '=');
|
||
|
||
return $encoded;
|
||
}
|
||
|
||
/**
|
||
* Decodes a URL-safe encoded string back to its original form.
|
||
* Replaces '-' with '+', '_' with '/', and adds back trailing '=' padding.
|
||
*
|
||
* @param string $string The URL-safe encoded string.
|
||
* @return string|false The decoded string, or false on failure (if the input is not valid base64 after adjustments).
|
||
*/
|
||
public function urlSafeDecode($string)
|
||
{
|
||
// Replace '-' with '+' and '_' with '/'
|
||
$decoded = str_replace(['-', '_'], ['+', '/'], $string);
|
||
|
||
// Add back trailing '=' padding.
|
||
// Base64 strings (before decoding) must have a length that is a multiple of 4.
|
||
// We calculate how many characters short we are from a multiple of 4
|
||
// and add the corresponding number of '=' characters back.
|
||
$padding = strlen($decoded) % 4;
|
||
if ($padding > 0) {
|
||
$decoded .= str_repeat('=', 4 - $padding);
|
||
}
|
||
|
||
// Standard Base64 decode
|
||
// base64_decode returns the original data or FALSE on failure.
|
||
return base64_decode($decoded);
|
||
}
|
||
}
|