186 lines
9.7 KiB
PHP
186 lines
9.7 KiB
PHP
<?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');
|
|
$transaction_id = Tools::getValue('order_id'); // This is the combined cart_id|timestamp
|
|
$amountReceived = round((float)Tools::getValue('actual_amount', 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));
|
|
}
|
|
}
|
|
}
|