validateOrder()) { $this->sendResponse('FAIL'); } $this->initializeModule(); $this->initializeCallbackData(); } /** @throws \Exception */ public function index(): void { ($this->callbackData['MNT_COMMAND'] === 'CHECK') ? $this->handleCheckCallback() : $this->handlePayCallback(); } private function initializeCallbackData(): void { $requestData = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET; $this->callbackData = [ 'MNT_COMMAND' => $this->getOptionalStringWithDefault($requestData, 'MNT_COMMAND'), 'MNT_ID' => $this->getRequiredString($requestData, 'MNT_ID'), 'MNT_TRANSACTION_ID' => $this->getRequiredString($requestData, 'MNT_TRANSACTION_ID'), 'MNT_OPERATION_ID' => $this->getOptionalStringWithDefault($requestData, 'MNT_OPERATION_ID'), 'MNT_AMOUNT' => $this->getOptionalStringWithDefault($requestData, 'MNT_AMOUNT'), 'MNT_CURRENCY_CODE' => $this->getRequiredString($requestData, 'MNT_CURRENCY_CODE'), 'MNT_SUBSCRIBER_ID' => $this->getOptionalStringWithDefault($requestData, 'MNT_SUBSCRIBER_ID'), 'MNT_TEST_MODE' => $this->getRequiredString($requestData, 'MNT_TEST_MODE'), 'MNT_SIGNATURE' => $this->getRequiredString($requestData, 'MNT_SIGNATURE'), ]; } private function sendResponse(string $response, int $statusCode = 200): void { http_response_code($statusCode); $isXml = str_starts_with($response, 'checkSignature($this->callbackData)) { $this->sendResponse('FAIL'); } $this->load->model('account/order'); $products = $this->model_account_order->getProducts($this->orderId); $deliveryPrice = $this->getDeliveryPrice(); $xmlData = [ 'MNT_ID' => $this->mntId, 'MNT_TRANSACTION_ID' => $this->callbackData['MNT_TRANSACTION_ID'], 'MNT_AMOUNT' => $this->formatPrice((float)($this->orderInfo['total'] ?? 0)), 'MNT_CURRENCY_CODE' => $this->callbackData['MNT_CURRENCY_CODE'], 'inventory' => $this->getInventoryJson($products) ?: null, 'client' => $this->callbackData['MNT_SUBSCRIBER_ID'], 'sno' => null, 'delivery' => (null !== $deliveryPrice) ? $this->formatPrice($deliveryPrice) : null, ]; [$xmlData['MNT_RESULT_CODE'], $xmlData['MNT_DESCRIPTION']] = $this->determineCheckResult(); $xmlData['MNT_SIGNATURE'] = md5( $xmlData['MNT_RESULT_CODE'] . $xmlData['MNT_ID'] . $xmlData['MNT_TRANSACTION_ID'] . $this->integrityCode, ); $this->sendResponse($this->buildXMLResponse($xmlData)); } /** @throws \Exception */ private function determineCheckResult(): array { $orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null; if (null === $orderStatusId) { return [500, "Order status not set"]; } $orderStatus = $this->getOrderStatusById($orderStatusId); if (empty($this->callbackData['MNT_AMOUNT'])) { return [100, "Order status is '{$orderStatus}'"]; } if ($this->successOrderStatusId === $orderStatusId) { return [200, 'Order Paid']; } if (in_array($orderStatusId, self::CANCEL_ORDER_STATUS_IDS, true)) { return [500, "Order status is '{$orderStatus}'"]; } return [402, 'Order created, but not paid']; } /** @throws \Exception */ private function handlePayCallback(): void { $shopData = [ 'MNT_ID' => $this->mntId, 'MNT_TRANSACTION_ID' => $this->callbackData['MNT_TRANSACTION_ID'], 'MNT_OPERATION_ID' => $this->callbackData['MNT_OPERATION_ID'], 'MNT_AMOUNT' => $this->formatPrice((float)$this->orderInfo['total']), 'MNT_CURRENCY_CODE' => $this->orderInfo['currency_code'], 'MNT_SUBSCRIBER_ID' => $this->callbackData['MNT_SUBSCRIBER_ID'], 'MNT_TEST_MODE' => $this->callbackData['MNT_TEST_MODE'], ]; if (!$this->checkSignature($shopData)) { $this->sendResponse('FAIL'); } $isPayedOrder = $this->isOrderPaid(); $resultCode = 200; $this->load->model('account/order'); $products = $this->model_account_order->getProducts($this->orderId); $deliveryPrice = $this->getDeliveryPrice(); $xmlData = [ 'MNT_ID' => $this->mntId, 'MNT_TRANSACTION_ID' => $this->callbackData['MNT_TRANSACTION_ID'], 'MNT_RESULT_CODE' => $resultCode, 'MNT_SIGNATURE' => md5($resultCode . $this->mntId . $this->callbackData['MNT_TRANSACTION_ID'] . $this->integrityCode), 'MNT_AMOUNT' => $this->callbackData['MNT_AMOUNT'], 'MNT_CURRENCY_CODE' => $this->callbackData['MNT_CURRENCY_CODE'], 'MNT_DESCRIPTION' => $isPayedOrder ? 'Order already paid' : 'Order success paid', 'inventory' => $this->getInventoryJson($products), 'client' => $this->callbackData['MNT_SUBSCRIBER_ID'], 'sno' => null, 'delivery' => $deliveryPrice !== null ? $this->formatPrice($deliveryPrice) : null, ]; if (!$isPayedOrder) { $this->load->language('extension/payanyway/payment/payanyway'); $this->addOrderHistory($this->orderId, $this->successOrderStatusId, $this->language->get('text_payment_completed'),); } $this->sendResponse($this->buildXMLResponse($xmlData)); } private function isOrderPaid(): bool { return $this->successOrderStatusId === (int)$this->orderInfo['order_status_id']; } /** @param array $data */ private function checkSignature(array $data): bool { $baseFields = [ 'MNT_ID', 'MNT_TRANSACTION_ID', 'MNT_OPERATION_ID', 'MNT_AMOUNT', 'MNT_CURRENCY_CODE', 'MNT_SUBSCRIBER_ID', 'MNT_TEST_MODE', ]; $fields = isset($data['MNT_COMMAND']) && ('CHECK' === $data['MNT_COMMAND']) ? array_merge(['MNT_COMMAND'], $baseFields) : $baseFields; $signatureString = array_reduce($fields, static function ($carry, $field) use ($data) { return $carry . $data[$field]; }, '') . $this->integrityCode; return hash_equals(md5($signatureString), $this->callbackData['MNT_SIGNATURE']); } private function getDeliveryPrice(): ?float { $this->load->model('account/order'); foreach ($this->model_account_order->getTotals($this->orderId) as $orderTotal) { if (($orderTotal['code'] ?? '') === 'shipping') { $deliveryPrice = (float)($orderTotal['value'] ?? 0); return $deliveryPrice !== 0.0 ? $deliveryPrice : null; } } return null; } /** * @param array $products * @return false|string */ private function getInventoryJson(array $products): false|string { $inventory = []; foreach ($products as $product) { $inventory[] = [ 'name' => $this->validateString($product['name'] ?? '', self::INVENTORY_ITEM_NAME_MAX_LENGTH), 'price' => $this->formatPrice((float)($product['price'] ?? 0)), 'quantity' => $this->formatQuantity((string)($product['quantity'] ?? '0')), 'vatTag' => $this->vatRate, 'pm' => self::INVENTORY_ITEM_DEFAULT_PAYMENT_METHOD, 'po' => self::INVENTORY_ITEM_DEFAULT_PAYMENT_OBJECT, 'measure' => self::INVENTORY_ITEM_DEFAULT_MEASURE, ]; } return json_encode($inventory, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } /** * @param int $orderStatusId * @return string * @throws \Exception */ public function getOrderStatusById(int $orderStatusId): string { $this->load->model('localisation/order_status'); /**@var array $orderStatuses */ $orderStatuses = $this->model_localisation_order_status->getOrderStatuses(); $orderStatus = 'unknown'; foreach ($orderStatuses as $orderStatus) { $statusId = isset($orderStatus['order_status_id']) ? (int)$orderStatus['order_status_id'] : null; if ($statusId === $orderStatusId) { $orderStatus = $orderStatus['name']; break; } } return $orderStatus; } private function formatQuantity(string $quantity): string { $quantity = str_replace(',', '.', $quantity); if (!str_contains($quantity, '.') && ctype_digit($quantity)) { return (string)(int)$quantity; } return number_format((float)$quantity, 3, '.', ''); } /** * @param array $data * @throws \Exception */ private function buildXMLResponse(array $data): false|string { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = false; $root = $dom->createElement('MNT_RESPONSE'); $dom->appendChild($root); $requiredFields = [ 'MNT_ID', 'MNT_TRANSACTION_ID', 'MNT_RESULT_CODE', 'MNT_DESCRIPTION', 'MNT_AMOUNT', 'MNT_CURRENCY_CODE', 'MNT_SIGNATURE', ]; foreach ($requiredFields as $field) { if (isset($data[$field]) && ($data[$field] !== '')) { $root->appendChild($dom->createElement($field, (string)$data[$field])); } } $attributes = $dom->createElement('MNT_ATTRIBUTES'); $root->appendChild($attributes); $attributeMap = [ 'INVENTORY' => $data['inventory'] ?? null, 'CLIENT' => $data['client'] ?? null, 'SNO' => $data['sno'] ?? null, 'DELIVERY' => $data['delivery'] ?? null, ]; foreach ($attributeMap as $key => $value) { if (!empty($value)) { $attrElement = $dom->createElement('ATTRIBUTE'); $attrElement->appendChild($dom->createElement('KEY', $key)); $attrElement->appendChild($dom->createElement('VALUE', (string)$value)); $attributes->appendChild($attrElement); } } return $dom->saveXML(); } /** @param array $params */ private function getOptionalStringWithDefault(array $params, string $key, string $default = ''): string { return $this->getOptionalString($params, $key) ?? $default; } /** @param array $params */ private function getOptionalString(array $params, string $key): ?string { $value = $params[$key] ?? null; if (!is_string($value) || trim($value) === '') { return null; } return $value; } /** * @param array $params * @throws \InvalidArgumentException */ private function getRequiredString(array $params, string $key): string { if (!isset($params[$key])) { throw new \InvalidArgumentException('Missing required field: ' . $key); } $value = $params[$key]; if (!is_string($value)) { throw new \InvalidArgumentException( sprintf('Field "%s" must be a string, %s given', $key, gettype($value)), ); } if ('' === $value) { throw new \InvalidArgumentException(sprintf('Field "%s" must be a non-empty string', $key)); } return $value; } }