getRequestBody(); // If request body is empty, log and exit. if (empty($requestBody)) { PrestaShopLogger::addLog('Hutko Callback: Empty request body received.', 2, null, 'Cart', null, true); exit('Empty request'); } // 2. Validate the request signature and required fields. // Ensure all expected fields are present before proceeding with validation. $requiredFields = ['order_id', 'amount', 'order_status', 'signature', 'merchant_id']; foreach ($requiredFields as $field) { if (!isset($requestBody[$field])) { PrestaShopLogger::addLog('Hutko Callback: Missing required field in request: ' . $field, 2, null, 'Cart', null, true); exit('Missing parameter: ' . $field); } } // Assuming validateResponse returns true on success, or a string error message on failure. $isSignatureValid = $this->module->validateResponse($requestBody); if ($isSignatureValid !== true) { PrestaShopLogger::addLog('Hutko Callback: Invalid signature. Error: ' . $isSignatureValid, 2, null, 'Cart', null, true); exit('Invalid signature'); } // 3. Extract cart ID and load the cart. // The order_id is expected to be in the format "cartID|timestamp". $transaction_id = $requestBody['order_id']; $orderIdParamParts = explode($this->module->order_separator, $transaction_id); $cartId = (int)$orderIdParamParts[0]; // Ensure it's an integer $cart = new Cart($cartId); // Validate cart object. if (!Validate::isLoadedObject($cart)) { PrestaShopLogger::addLog('Hutko Callback: Cart not found for ID: ' . $cartId, 3, null, 'Cart', $cartId, true); exit('Cart not found'); } // 4. Determine the amount received from the callback. $amountReceived = round((float)$requestBody['amount'] / 100, 2); // 5. Check if the order already exists for this cart. $orderId = Order::getIdByCartId($cart->id); $orderExists = (bool)$orderId; // 6. If the order doesn't exist, attempt to validate it using postponeCallback. // This handles the scenario where the callback arrives before the customer returns to the site. if (!$orderExists) { // The callback function will check for order existence again right before validation // to handle potential race conditions. $validationCallback = function () use ($cart, $amountReceived, $transaction_id) { // Re-check if the order exists right before validation in case the result controller // created it in the interim while we were waiting for the second digit. if (Order::getIdByCartId($cart->id)) { return true; // Order already exists, no need to validate again. } // If order still doesn't exist, proceed with validation. $idState = (int)Configuration::get('HUTKO_SUCCESS_STATUS_ID'); return $this->module->validateOrderFromCart((int)$cart->id, $amountReceived, $transaction_id, $idState); }; // Postpone validation to seconds ending in 8 to avoid collision with result controller (ending in 3). $validationResult = $this->module->postponeCallback($validationCallback, 8); // Re-fetch order ID after potential validation. $orderId = Order::getIdByCartId($cart->id); if (!$orderId || !$validationResult) { PrestaShopLogger::addLog('Hutko Callback: Order validation failed for cart ID: ' . $cart->id, 2, null, 'Cart', $cart->id, true); exit('Order validation failed'); } } // If we reached here, an order should exist. Load it. $order = new Order($orderId); if (!Validate::isLoadedObject($order)) { PrestaShopLogger::addLog('Hutko Callback: Order could not be loaded for ID: ' . $orderId, 3, null, 'Order', $orderId, true); exit('Order not found after validation'); } // 7. Handle payment status from the callback. $orderStatusCallback = $requestBody['order_status']; $currentOrderState = (int)$order->getCurrentState(); switch ($orderStatusCallback) { case 'approved': $expectedState = (int)Configuration::get('HUTKO_SUCCESS_STATUS_ID'); // Only change state if it's not already the success state or "Payment accepted". // "Payment accepted" (PS_OS_PAYMENT) might be set by validateOrderFromCart. if ($currentOrderState !== $expectedState && $currentOrderState !== (int)Configuration::get('PS_OS_PAYMENT')) { $this->module->updateOrderStatus($orderId, $expectedState, 'Payment approved by Hutko.'); } exit('OK'); break; case 'declined': $expectedState = (int)Configuration::get('PS_OS_ERROR'); // Only change state if it's not already the error state. if ($currentOrderState !== $expectedState) { $this->module->updateOrderStatus($orderId, $expectedState, 'Payment ' . $orderStatusCallback . ' by Hutko.'); } exit('Order ' . $orderStatusCallback); break; case 'expired': $expectedState = (int)Configuration::get('PS_OS_ERROR'); // Only change state if it's not already the error state. if ($currentOrderState !== $expectedState) { $this->module->updateOrderStatus($orderId, $expectedState, 'Payment ' . $orderStatusCallback . ' by Hutko.'); } exit('Order ' . $orderStatusCallback); break; case 'processing': // If the order is still processing, we might want to update its status // to a specific 'processing' state if available, or just acknowledge. // For now, if it's not already in a success/error state, set it to 'processing'. $processingState = (int)Configuration::get('PS_OS_PAYMENT'); // Or a custom 'processing' state if ($currentOrderState !== $processingState && $currentOrderState !== (int)Configuration::get('HUTKO_SUCCESS_STATUS_ID') && $currentOrderState !== (int)Configuration::get('PS_OS_ERROR')) { $this->module->updateOrderStatus($orderId, $processingState, 'Payment processing by Hutko.'); } exit('Processing'); break; default: // Log unexpected status and exit with an error. PrestaShopLogger::addLog('Hutko Callback: Unexpected order status received: ' . $orderStatusCallback . ' for order ID: ' . $orderId, 3, null, 'Order', $orderId, true); exit('Unexpected status'); break; } } catch (Exception $e) { // Log any uncaught exceptions and exit with the error message. PrestaShopLogger::addLog('Hutko Callback Error: ' . $e->getMessage(), 3, null, 'HutkoCallbackModuleFrontController', null, true); exit($e->getMessage()); } } /** * Helper method to parse the request body from POST or raw input. * * @return array The parsed request body. */ private function getRequestBody(): array { // Prioritize $_POST for form data. if (!empty($_POST)) { return $_POST; } // Fallback to raw input for JSON payloads, common for callbacks. $jsonBody = json_decode(Tools::file_get_contents("php://input"), true); if (is_array($jsonBody)) { return $jsonBody; } return []; } }