From 4233c1507e3dee39b042ca7cec493e5cf28b92a3 Mon Sep 17 00:00:00 2001 From: O K Date: Sun, 1 Jun 2025 16:38:06 +0300 Subject: [PATCH] added refund logic --- controllers/front/callback.php | 108 +---- controllers/front/redirect.php | 34 +- controllers/front/result.php | 185 -------- hutko.php | 411 +++++++++++------- .../templates/admin/order_payment_refund.tpl | 41 +- views/templates/front/redirect.tpl | 23 +- 6 files changed, 327 insertions(+), 475 deletions(-) delete mode 100644 controllers/front/result.php diff --git a/controllers/front/callback.php b/controllers/front/callback.php index f299b4e..23670f7 100644 --- a/controllers/front/callback.php +++ b/controllers/front/callback.php @@ -29,24 +29,7 @@ if (!defined('_PS_VERSION_')) { */ 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') { @@ -61,72 +44,18 @@ class HutkoCallbackModuleFrontController extends ModuleFrontController 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'); } + // PrestaShopLogger::addLog('Hutko Callback: ' . json_encode($requestBody), 1); + - // 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, true); - }; - - // 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'); - } - } + $orderId = (int)$orderIdParamParts[0]; // Ensure it's an integer // If we reached here, an order should exist. Load it. $order = new Order($orderId); @@ -141,11 +70,17 @@ class HutkoCallbackModuleFrontController extends ModuleFrontController 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.'); + // Only success state if no refunds was done. + if ($requestBody['response_status'] == 'success' && $requestBody['reversal_amount'] == '0') { + $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->addPayment($requestBody, $order); + $order->setCurrentState($expectedState); + } + } else { + PrestaShopLogger::addLog('Hutko Callback: Unhandled response_status: ' . $requestBody['response_status']); } exit('OK'); break; @@ -154,7 +89,7 @@ class HutkoCallbackModuleFrontController extends ModuleFrontController $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.'); + $order->setCurrentState($expectedState); } exit('Order ' . $orderStatusCallback); break; @@ -162,7 +97,7 @@ class HutkoCallbackModuleFrontController extends ModuleFrontController $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.'); + $order->setCurrentState($expectedState); } exit('Order ' . $orderStatusCallback); break; @@ -173,7 +108,7 @@ class HutkoCallbackModuleFrontController extends ModuleFrontController // 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.'); + $order->setCurrentState($processingState); } exit('Processing'); break; @@ -198,13 +133,8 @@ class HutkoCallbackModuleFrontController extends ModuleFrontController */ 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); + $jsonBody = json_decode(file_get_contents("php://input"), true); if (is_array($jsonBody)) { return $jsonBody; } diff --git a/controllers/front/redirect.php b/controllers/front/redirect.php index a49fba1..0a757f5 100644 --- a/controllers/front/redirect.php +++ b/controllers/front/redirect.php @@ -32,16 +32,32 @@ class HutkoRedirectModuleFrontController extends ModuleFrontController */ 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'); + // Call the parent class's initContent method to perform default initializations. + parent::initContent(); + $idState = (int) Configuration::get('HUTKO_NEW_ORDER_STATUS_ID', null, null, null, Configuration::get('PS_OS_PREPARATION')); + $validationResult = $this->module->validateOrder($this->context->cart->id, $idState, 0, $this->module->displayName); + + if ($validationResult) { + $order = new Order((int)$this->module->currentOrder); + $requestData = $this->module->buildPaymentRequestData($order, null, null, null); + $responseData = $this->module->sendAPICall($this->module->checkout_url, $requestData); + if (isset($responseData['response']['response_status']) && $responseData['response']['response_status'] === 'success') { + $orderPayment = new OrderPayment(); + $orderPayment->order_reference = $order->reference; + $orderPayment->id_currency = $order->id_currency; + $orderPayment->amount = 0; + $orderPayment->payment_method = $this->module->displayName; + $orderPayment->transaction_id = $requestData['order_id']; + $orderPayment->card_holder = $responseData['response']['checkout_url']; + $orderPayment->add(); + Tools::redirect($responseData['response']['checkout_url']); + return; + } + $this->context->smarty->assign([ + 'hutko_response' => $responseData['response'], // The URL of the Hutko payment gateway. + ]); + } } } diff --git a/controllers/front/result.php b/controllers/front/result.php deleted file mode 100644 index 20dc940..0000000 --- a/controllers/front/result.php +++ /dev/null @@ -1,185 +0,0 @@ -module->order_separator, $transaction_id); - $cartId = (int)$cartIdParts[0]; - - // Validate extracted cart ID. It must be a numeric value. - if (!is_numeric($cartId)) { - $this->errors[] = Tools::displayError($this->trans('Invalid cart ID received.', [], 'Modules.Hutko.Shop')); - $this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id)); - return; // Stop execution after redirection - } - - // Load the cart object. - $cart = new Cart($cartId); - - // Verify that the cart belongs to the current customer to prevent unauthorized access. - if (!Validate::isLoadedObject($cart) || $cart->id_customer != $this->context->customer->id) { - $this->errors[] = Tools::displayError($this->trans('Access denied to this order.', [], 'Modules.Hutko.Shop')); - Tools::redirect('/'); // Redirect to home or a more appropriate error page - } - - // Handle different payment statuses. - switch ($orderStatus) { - case 'declined': - $this->errors[] = Tools::displayError($this->trans('Your payment was declined. Please try again or use a different payment method.', [], 'Modules.Hutko.Shop')); - $this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id)); - break; - - case 'expired': - $this->errors[] = Tools::displayError($this->trans('Your payment has expired. Please try again.', [], 'Modules.Hutko.Shop')); - $this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id)); - break; - - case 'processing': - // For 'processing' status, we need to poll for order creation. - // This loop will try to find the order for a limited time to avoid - // exceeding PHP execution limits. - $maxAttempts = 10; // Max 10 attempts - $sleepTime = 5; // Sleep 5 seconds between attempts (total max 50 seconds) - $orderFound = false; - $orderId = 0; - - for ($i = 0; $i < $maxAttempts; $i++) { - $orderId = Order::getIdByCartId($cart->id); - if ($orderId) { - $orderFound = true; - break; // Order found, exit loop - } - // If not found, wait for a few seconds before retrying. - sleep($sleepTime); - } - - if ($orderFound) { - // Order found, redirect to confirmation page. - Tools::redirect($this->context->link->getPageLink('order-confirmation', true, $this->context->language->id, [ - 'id_cart' => $cart->id, - 'id_module' => $this->module->id, - 'id_order' => $orderId, - 'key' => $this->context->customer->secure_key, - ])); - } else { - // Order not found after multiple attempts, assume it's still processing or failed silently. - $this->errors[] = Tools::displayError($this->trans('Your payment is still processing. Please check your order history later.', [], 'Modules.Hutko.Shop')); - $this->redirectWithNotifications($this->context->link->getPageLink('order-history', true, $this->context->language->id)); - } - break; - - case 'approved': - $orderId = Order::getIdByCartId($cart->id); - - // If the order doesn't exist yet, validate it. - // The postponeCallback is used here to avoid race conditions with the callback controller - // (which might be trying to validate the order on seconds ending in 8, while this - // controller tries on seconds ending in 3). - if (!$orderId) { - // Define the validation logic to be executed by postponeCallback. - // This callback will first check if the order exists, and only - // validate if it doesn't, to avoid race conditions. - $validationCallback = function () use ($cart, $amountReceived, $transaction_id) { - // Re-check if the order exists right before validation in case the callback - // controller created it in the interim while we were waiting for the second digit. - if (Order::getIdByCartId($cart->id)) { - return true; // Order already exists, no need to validate again. - } - $idState = (int)Configuration::get('HUTKO_SUCCESS_STATUS_ID'); - // If order still doesn't exist, proceed with validation. - return $this->module->validateOrderFromCart((int)$cart->id, $amountReceived, $transaction_id, $idState); - }; - - // Postpone the execution of the validation callback until the second ends in 3. - $validationResult = $this->module->postponeCallback($validationCallback, 3); - - // After the postponed callback has run, try to get the order ID again. - $orderId = Order::getIdByCartId($cart->id); - - // If validation failed or order still not found, add an error. - if (!$orderId || !$validationResult) { - $this->errors[] = Tools::displayError($this->trans('Payment approved but order could not be created. Please contact support.', [], 'Modules.Hutko.Shop')); - $this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id)); - break; - } - } - - // If order exists (either found initially or created by validation), redirect to confirmation. - Tools::redirect($this->context->link->getPageLink('order-confirmation', true, $this->context->language->id, [ - 'id_cart' => $cart->id, - 'id_module' => $this->module->id, - 'id_order' => $orderId, - 'key' => $this->context->customer->secure_key, - ])); - break; - - default: - // For any unexpected status, redirect to order history with a generic error. - $this->errors[] = Tools::displayError($this->trans('An unexpected payment status was received. Please check your order history.', [], 'Modules.Hutko.Shop')); - $this->redirectWithNotifications($this->context->link->getPageLink('order-history', true, $this->context->language->id)); - break; - } - - // This part should ideally not be reached if all cases are handled with redirects. - // However, as a fallback, if any errors were accumulated without a specific redirect, - // redirect to the order page. - if (count($this->errors)) { - $this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id)); - } - } -} diff --git a/hutko.php b/hutko.php index e317d17..9ea8408 100644 --- a/hutko.php +++ b/hutko.php @@ -21,26 +21,29 @@ class Hutko extends PaymentModule { public $order_separator = '#'; - - public $checkout_url = 'https://pay.hutko.org/api/checkout/redirect/'; + 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'; - private $settingsList = [ + public $settingsList = [ 'HUTKO_MERCHANT', 'HUTKO_SECRET_KEY', - 'HUTKO_BACK_REF', + 'HUTKO_SHIPPING_INCLUDE', + 'HUTKO_SHIPPING_PRODUCT_NAME', + 'HUTKO_SHIPPING_PRODUCT_CODE', + 'HUTKO_NEW_ORDER_STATUS_ID', 'HUTKO_SUCCESS_STATUS_ID', 'HUTKO_SHOW_CARDS_LOGO' ]; - private $postErrors = []; + public $postErrors = []; public function __construct() { $this->name = 'hutko'; $this->tab = 'payments_gateways'; - $this->version = '1.1.1'; + $this->version = '1.2.0'; $this->author = 'Hutko'; $this->bootstrap = true; parent::__construct(); @@ -50,12 +53,36 @@ class Hutko extends PaymentModule $this->description = $this->trans('Hutko is a payment platform whose main function is to provide internet acquiring.', array(), 'Modules.Hutko.Admin'); } - public function install() + public function install(): bool { - return parent::install() + $success = parent::install() && $this->registerHook('paymentOptions') - && $this->registerHook('displayAdminOrderContentOrder') && $this->registerHook('actionAdminControllerSetMedia'); + + // 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 < 8.0) + if (version_compare(_PS_VERSION_, '1.7', '>=') && version_compare(_PS_VERSION_, '8.0', '<')) { + // Register the 1.7 hook + $conditionalHookSuccess = $this->registerHook('displayAdminOrderContentOrder'); + } + // Check if PrestaShop version is 8.x or 9.x (>= 8.0 and < 10.0) + elseif (version_compare(_PS_VERSION_, '8.0', '>=') && 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() @@ -98,7 +125,7 @@ class Hutko extends PaymentModule /** * Create the form that will be displayed in the configuration of your module. */ - protected function renderForm() + public function renderForm() { $helper = new HelperForm(); @@ -126,13 +153,13 @@ class Hutko extends PaymentModule /** * Create the structure of your form. */ - protected function getConfigForm() + public function getConfigForm() { - global $cookie; + $options = []; - foreach (OrderState::getOrderStates($cookie->id_lang) as $state) { // getting all Prestashop statuses + 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]]"]; } @@ -149,16 +176,16 @@ class Hutko extends PaymentModule 'col' => 4, 'type' => 'text', 'prefix' => '', - 'desc' => $this->trans('Enter a merchant id', array(), 'Modules.Hutko.Admin'), + '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'), + 'label' => $this->trans('Merchant ID.', array(), 'Modules.Hutko.Admin'), ), array( 'col' => 4, 'type' => 'text', 'prefix' => '', 'name' => 'HUTKO_SECRET_KEY', - 'desc' => $this->trans('Enter a secret key', array(), 'Modules.Hutko.Admin'), + '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( @@ -172,6 +199,49 @@ class Hutko extends PaymentModule 'name' => 'name' ) ), + array( + 'type' => 'select', + 'prefix' => '', + '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'), @@ -191,6 +261,7 @@ class Hutko extends PaymentModule ), ), ), + 'submit' => array( 'title' => $this->trans('Save', array(), 'Modules.Hutko.Admin'), 'class' => 'btn btn-default pull-right' @@ -202,25 +273,21 @@ class Hutko extends PaymentModule /** * Set values for the inputs. */ - protected function getConfigFormValues() + public function getConfigFormValues(): array { - 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), - ); + foreach ($this->settingsList as $settingName) { + $list[$settingName] = Configuration::get($settingName); + } + return $list; } /** * Save form data. */ - protected function postProcess() + public function postProcess() { - - $form_values = $this->getConfigFormValues(); - foreach (array_keys($form_values) as $key) { - Configuration::updateValue($key, Tools::getValue($key)); + foreach ($this->settingsList as $settingName) { + Configuration::updateValue($settingName, Tools::getValue($settingName)); } } @@ -231,7 +298,7 @@ class Hutko extends PaymentModule * 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 + public function postValidation(): void { // Check if the module's configuration form has been submitted. if (Tools::isSubmit('submitHutkoModule')) { @@ -323,41 +390,55 @@ class Hutko extends PaymentModule * @return array An associative array containing the input parameters for the * payment gateway. This array includes the generated signature. */ - public function buildInputs(): array + 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 = $this->context->cart->id . $this->order_separator . time(); + $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('Cart pay №', [], 'Modules.Hutko.Admin') . $this->context->cart->id; + $orderDescription = $this->trans('Order payment #', [], 'Modules.Hutko.Admin') . $order->reference; // 4. Calculate the order amount in the smallest currency unit. - $amount = round($this->context->cart->getOrderTotal(true, CART::ONLY_PRODUCTS) * 100); - + if (!$amount) { + $amount = $order->total_products; + } + $amountInt = round($amount * 100); // 5. Get the currency ISO code of the current cart. - $currency = $this->context->currency->iso_code; + 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->getPageLink('order-confirmation', true, $order->id_lang, [ + 'id_cart' => $order->id_cart, + 'id_module' => $this->id, + 'id_order' => $order->id, + 'key' => $customer->secure_key, + ]); - // 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(); + $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' => $amount, - 'currency' => $currency, + 'amount' => $amountInt, + 'currency' => $currencyISO, 'server_callback_url' => $serverCallbackUrl, 'response_url' => $responseUrl, 'sender_email' => $customerEmail, @@ -384,15 +465,15 @@ class Hutko extends PaymentModule * * @return string A base64 encoded JSON string containing the reservation data. */ - public function buildReservationData(): string + public function buildReservationData(Order $order): string { // 1. Retrieve the delivery address for the current cart. - $address = new Address((int)$this->context->cart->id_address_delivery, $this->context->language->id); + $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, $this->context->language->id); + $state = new State((int) $address->id_state, $order->id_lang); $customerState = $state->name; } @@ -409,16 +490,15 @@ class Hutko extends PaymentModule "customer_name" => $this->getSlug($address->lastname . ' ' . $address->firstname), "customer_city" => $this->getSlug($address->city), "customer_zip" => $address->postcode, - "account" => $this->context->customer->id, + "account" => $order->id_customer, "uuid" => hash('sha256', _COOKIE_KEY_ . Tools::getShopDomainSsl()), - "products" => $this->getProducts(), + "products" => $this->getProducts($order), ]; - // 4. Encode the data array as a JSON string. - $jsonData = json_encode($data); - // 5. Base64 encode the JSON string. - return base64_encode($jsonData); + + + return base64_encode(json_encode($data)); } @@ -438,16 +518,26 @@ class Hutko extends PaymentModule * - '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 + public function getProducts(Order $order): array { $products = []; - foreach ($this->context->cart->getProducts() as $cartProduct) { + foreach ($order->getProductsDetail() as $productDetail) { $products[] = [ - "id" => (int)$cartProduct['id_product'], - "name" => $cartProduct['name'], - "price" => round((float)$cartProduct['price_with_reduction'], 2), - "total_amount" => round((float) $cartProduct['price'] * (int)$cartProduct['quantity'], 2), - "quantity" => (int)$cartProduct['quantity'], + "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; @@ -455,31 +545,6 @@ class Hutko extends PaymentModule - /** - * 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 $fromCallBack = false): bool - { - if (!$idState) { - $idState = (int) Configuration::get('PS_OS_PREPARATION'); - } - if ($fromCallBack) { - $this->context->cart = new Cart($id_cart); - $this->context->customer = new Customer($this->context->cart->id_customer); - } - // 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. @@ -618,7 +683,7 @@ class Hutko extends PaymentModule public function validateResponse(array $response): bool { // 1. Verify the Merchant ID - if (Configuration::get('HUTKO_MERCHANT') !== $response['merchant_id']) { + if ((string)Configuration::get('HUTKO_MERCHANT') != (string)$response['merchant_id']) { return false; } @@ -636,43 +701,6 @@ class Hutko extends PaymentModule } - /** - * 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 - } - } - } /** @@ -687,49 +715,131 @@ class Hutko extends PaymentModule { $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) { + if (Validate::isLoadedObject($order) && (int)$order->getCurrentState() != $newStateId) { $history = new OrderHistory(); $history->id_order = $orderId; $history->changeIdOrderState($newStateId, $orderId); $history->addWithemail(); } } + public function addPayment(array $callback, Order $order): void + { + $callbackAmount = $callback['actual_amount'] ?? $callback['amount']; + $amountFloat = round($callbackAmount / 100, 2); + + $order->addOrderPayment($amountFloat, $this->displayName, $callback['order_id'], $order->id_currency); + } /** - * Hook to display content in the admin order page tabs. - * This will add a new tab for "Hutko Payments" or similar. + * Hook implementation for PrestaShop 1.7.x to display content in the admin order page. * - * @param array $params Contains Order 'order' - * @return string + * 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): string + 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 + { + // Check if a refund form has been submitted if (Tools::isSubmit('hutkoRefundsubmit')) { + // Process the refund logic. $this->processRefundForm(); } - if (Tools::getValue('hutkoOrderStatus')) { - $this->processOrderStatus(Tools::getValue('hutkoOrderStatus')); - } - // This hook is used to render the content of the new tab on the order page. - // We will fetch the payments for this order and pass them to the template. + // Check payment status + if (Tools::getValue('hutkoOrderPaymentStatus')) { + // Process the requested order status check. + $this->processOrderPaymentStatus(Tools::getValue('hutkoOrderPaymentStatus')); + } + + // 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 ''; + } + + // Get the Order object from the parameters $order = $params['order']; - - // Fetch payments made via Hutko for this 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); - $hutkoPayments->where('payment_method', '=', $this->displayName); + $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([ - 'hutkoPayments' => $hutkoPayments->getAll(), + // 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), ]); + // 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 processOrderStatus(string $order_id): void + + public function processOrderPaymentStatus(string $order_id): void { $data = [ 'order_id' => $order_id, @@ -741,32 +851,22 @@ class Hutko extends PaymentModule $response = $this->sendAPICall($this->status_url, $data); $this->context->controller->informations[] = $this->displayArrayInNotification($response['response']); } - /** - * Hook to set media (JS/CSS) for admin controllers. - * Used to load our custom JavaScript for the refund modal. - * - * @param array $params - * @return void - */ - public function hookActionAdminControllerSetMedia(array $params): void - { - // Only load our JS on the AdminOrders controller page - if ($this->context->controller->controller_name === 'AdminOrders') { - } - } + public function processRefundForm() { $orderPaymentId = (int) Tools::getValue('orderPaymentId'); $amount = (float) Tools::getValue('refund_amount'); $comment = mb_substr(Tools::getValue('orderPaymentId', ''), 0, 1024); - $orderId = (int) Tools::getValue('id_order'); - $result = $this->processRefund($orderPaymentId, $orderId, $amount, $comment); + $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); } /** @@ -836,9 +936,6 @@ class Hutko extends PaymentModule $order = new Order($orderId); - // Add a note to the order history. - $this->updateOrderStatus($order->id, (int)Configuration::get('PS_OS_REFUND')); - // Add a private message to the order for tracking. $order->addOrderPayment( -$amount, // Negative amount for refund @@ -846,6 +943,8 @@ class Hutko extends PaymentModule $orderPayment->transaction_id ); + $order->setCurrentState((int)Configuration::get('PS_OS_REFUND'), $this->context->employee->id); + PrestaShopLogger::addLog( 'Hutko Refund: Successfully processed refund for Order: ' . $orderId . ', Amount: ' . $amount . ', Comment: ' . $comment, 1, // Info diff --git a/views/templates/admin/order_payment_refund.tpl b/views/templates/admin/order_payment_refund.tpl index b6c669a..51e70e9 100644 --- a/views/templates/admin/order_payment_refund.tpl +++ b/views/templates/admin/order_payment_refund.tpl @@ -28,31 +28,29 @@ {foreach from=$hutkoPayments item='payment'} -
- - {$payment->transaction_id|escape:'htmlall':'UTF-8'} - {displayPrice price=Tools::ps_round($payment->amount, 2) currency=$currency->id|floatval} +
+ + {$payment->transaction_id} + {displayPrice price=Tools::ps_round($payment->amount, 2) currency=$currency->id} {$payment->date_add|date_format:'%Y-%m-%d %H:%M:%S'} {if $payment->amount > 0} {/if} - -