Files
hutko/controllers/front/result.php
2025-05-29 10:54:49 +03:00

186 lines
9.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Hutko - Платіжний сервіс, який рухає бізнеси вперед.
*
* Запускайтесь, набирайте темп, масштабуйтесь ми підстрахуємо всюди.
*
* @author panariga
* @copyright 2025 Hutko
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
/**
* Class HutkoResultModuleFrontController
* Front Controller for handling the result of a Hutko payment.
*
* This class processes the response from the Hutko payment gateway after a customer
* has attempted a payment. It validates the incoming parameters, handles different
* payment statuses (approved, declined, processing, expired), and redirects the
* customer accordingly to the order confirmation page, order history, or back
* to the order page with relevant notifications.
*
* @property Hutko $module An instance of the Hutko module.
*/
class HutkoResultModuleFrontController extends ModuleFrontController
{
/**
* Handles the post-processing of the payment gateway response.
*
* This method retrieves payment status and order details from the request,
* performs necessary validations, and then takes action based on the
* payment status:
* - 'declined' or 'expired': Adds an error and redirects to the order page.
* - 'processing': Periodically checks for order creation (up to PHP execution timeout)
* and redirects to confirmation if found, or adds an error if not.
* - 'approved': Validates the order (if not already created) and redirects
* to the order confirmation page.
* - Any other status: Redirects to the order history or order page with errors.
*/
public function postProcess(): void
{
// Retrieve essential parameters from the request.
$orderStatus = Tools::getValue('order_status', false);
$transaction_id = Tools::getValue('order_id', false); // This is the combined cart_id|timestamp
$amountReceived = round((float)Tools::getValue('amount', 0) / 100, 2);
// Basic validation: If critical parameters are missing, redirect to home.
if (!$transaction_id || !$orderStatus || !$amountReceived) {
Tools::redirect('/');
}
// Extract cart ID from the combined order_id parameter.
// The order_id is expected to be in the format "cartID|timestamp".
$cartIdParts = explode($this->module->order_separator, $transaction_id);
$cartId = (int)$cartIdParts[0];
// Validate extracted cart ID. It must be a numeric value.
if (!is_numeric($cartId)) {
$this->errors[] = Tools::displayError($this->trans('Invalid cart ID received.', [], 'Modules.Hutko.Shop'));
$this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id));
return; // Stop execution after redirection
}
// Load the cart object.
$cart = new Cart($cartId);
// Verify that the cart belongs to the current customer to prevent unauthorized access.
if (!Validate::isLoadedObject($cart) || $cart->id_customer != $this->context->customer->id) {
$this->errors[] = Tools::displayError($this->trans('Access denied to this order.', [], 'Modules.Hutko.Shop'));
Tools::redirect('/'); // Redirect to home or a more appropriate error page
}
// Handle different payment statuses.
switch ($orderStatus) {
case 'declined':
$this->errors[] = Tools::displayError($this->trans('Your payment was declined. Please try again or use a different payment method.', [], 'Modules.Hutko.Shop'));
$this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id));
break;
case 'expired':
$this->errors[] = Tools::displayError($this->trans('Your payment has expired. Please try again.', [], 'Modules.Hutko.Shop'));
$this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id));
break;
case 'processing':
// For 'processing' status, we need to poll for order creation.
// This loop will try to find the order for a limited time to avoid
// exceeding PHP execution limits.
$maxAttempts = 10; // Max 10 attempts
$sleepTime = 5; // Sleep 5 seconds between attempts (total max 50 seconds)
$orderFound = false;
$orderId = 0;
for ($i = 0; $i < $maxAttempts; $i++) {
$orderId = Order::getIdByCartId($cart->id);
if ($orderId) {
$orderFound = true;
break; // Order found, exit loop
}
// If not found, wait for a few seconds before retrying.
sleep($sleepTime);
}
if ($orderFound) {
// Order found, redirect to confirmation page.
Tools::redirect($this->context->link->getPageLink('order-confirmation', true, $this->context->language->id, [
'id_cart' => $cart->id,
'id_module' => $this->module->id,
'id_order' => $orderId,
'key' => $this->context->customer->secure_key,
]));
} else {
// Order not found after multiple attempts, assume it's still processing or failed silently.
$this->errors[] = Tools::displayError($this->trans('Your payment is still processing. Please check your order history later.', [], 'Modules.Hutko.Shop'));
$this->redirectWithNotifications($this->context->link->getPageLink('order-history', true, $this->context->language->id));
}
break;
case 'approved':
$orderId = Order::getIdByCartId($cart->id);
// If the order doesn't exist yet, validate it.
// The postponeCallback is used here to avoid race conditions with the callback controller
// (which might be trying to validate the order on seconds ending in 8, while this
// controller tries on seconds ending in 3).
if (!$orderId) {
// Define the validation logic to be executed by postponeCallback.
// This callback will first check if the order exists, and only
// validate if it doesn't, to avoid race conditions.
$validationCallback = function () use ($cart, $amountReceived, $transaction_id) {
// Re-check if the order exists right before validation in case the callback
// controller created it in the interim while we were waiting for the second digit.
if (Order::getIdByCartId($cart->id)) {
return true; // Order already exists, no need to validate again.
}
$idState = (int)Configuration::get('HUTKO_SUCCESS_STATUS_ID');
// If order still doesn't exist, proceed with validation.
return $this->module->validateOrderFromCart((int)$cart->id, $amountReceived, $transaction_id, $idState);
};
// Postpone the execution of the validation callback until the second ends in 3.
$validationResult = $this->module->postponeCallback($validationCallback, 3);
// After the postponed callback has run, try to get the order ID again.
$orderId = Order::getIdByCartId($cart->id);
// If validation failed or order still not found, add an error.
if (!$orderId || !$validationResult) {
$this->errors[] = Tools::displayError($this->trans('Payment approved but order could not be created. Please contact support.', [], 'Modules.Hutko.Shop'));
$this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id));
break;
}
}
// If order exists (either found initially or created by validation), redirect to confirmation.
Tools::redirect($this->context->link->getPageLink('order-confirmation', true, $this->context->language->id, [
'id_cart' => $cart->id,
'id_module' => $this->module->id,
'id_order' => $orderId,
'key' => $this->context->customer->secure_key,
]));
break;
default:
// For any unexpected status, redirect to order history with a generic error.
$this->errors[] = Tools::displayError($this->trans('An unexpected payment status was received. Please check your order history.', [], 'Modules.Hutko.Shop'));
$this->redirectWithNotifications($this->context->link->getPageLink('order-history', true, $this->context->language->id));
break;
}
// This part should ideally not be reached if all cases are handled with redirects.
// However, as a fallback, if any errors were accumulated without a specific redirect,
// redirect to the order page.
if (count($this->errors)) {
$this->redirectWithNotifications($this->context->link->getPageLink('order', true, $this->context->language->id));
}
}
}