validateOrder()) { $this->sendResponse(self::FAIL_RESPONSE); } $this->initializeModule(); $this->initializeCallbackData(); } /** @throws \Exception */ public function index(): void { $this->validateCallback(); ($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 validateCallback(): void { if (empty($this->mntId) || empty($this->integrityCode)) { $this->sendResponse(self::FAIL_RESPONSE); } if ($this->mntId !== (int)$this->callbackData['MNT_ID']) { $this->sendResponse(self::FAIL_RESPONSE); } } /** * @param array|string $response * @param int $statusCode * @return void */ private function sendResponse(array|string $response, int $statusCode = 200): void { http_response_code($statusCode); if (is_string($response)) { header('Content-Type: ' . self::TEXT_CONTENT_TYPE); echo $response; } else { header('Content-Type: ' . $response['contentType']); echo $response['data']; } exit(); } /** @throws \Exception */ private function handleCheckCallback(): void { if (!$this->checkSignature($this->callbackData)) { $this->sendResponse(self::FAIL_RESPONSE); } [$resultCode, $description] = $this->determineCheckResult(); $responseData = $this->buildBaseResponseData($resultCode, $description); $products = $this->getAccountOrderProducts(); $deliveryInfo = $this->getDeliveryInfo(); $responseData['client'] = $this->getClientInfo(); $responseData['items'] = $this->getCheckItems($products, $deliveryInfo); $this->sendResponse($this->buildJsonResponse($responseData)); } /** * @return array * @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'] ?? 0)), 'MNT_CURRENCY_CODE' => $this->orderInfo['currency_code'] ?? 'RUB', 'MNT_SUBSCRIBER_ID' => $this->callbackData['MNT_SUBSCRIBER_ID'], 'MNT_TEST_MODE' => $this->callbackData['MNT_TEST_MODE'], ]; if (!$this->checkSignature($shopData)) { $this->sendResponse(self::FAIL_RESPONSE); } $isPayedOrder = $this->isOrderPaid(); if (!$isPayedOrder) { $this->load->language('extension/payanyway/payment/payanyway'); $this->addOrderHistory($this->orderId, $this->successOrderStatusId, $this->language->get('text_payment_completed'),); } $resultCode = 200; $description = $isPayedOrder ? 'Order already paid' : 'Order success paid'; $responseData = $this->buildBaseResponseData($resultCode, $description); $products = $this->getAccountOrderProducts(); $deliveryInfo = $this->getDeliveryInfo(); $payInventoryJson = $this->getPayInventoryJson($products, $deliveryInfo); $responseData['inventory'] = $payInventoryJson ?: null; $clientInfo = $this->getClientInfo(); $responseData['client'] = $clientInfo ? json_encode([$clientInfo], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null; $this->sendResponse($this->buildXmlResponse($responseData)); } /** * @param int $resultCode * @param string $description * @return array * @throws \Exception */ private function buildBaseResponseData(int $resultCode, string $description): array { $orderTotal = (float)($this->orderInfo['total'] ?? 0); $data = [ 'MNT_ID' => $this->mntId, 'MNT_TRANSACTION_ID' => $this->callbackData['MNT_TRANSACTION_ID'], 'MNT_RESULT_CODE' => $resultCode, 'MNT_DESCRIPTION' => $this->validateString($description, self::DESCRIPTION_MAX_LENGTH), 'MNT_AMOUNT' => $this->formatPrice($orderTotal), 'MNT_CURRENCY_CODE' => $this->callbackData['MNT_CURRENCY_CODE'], ]; $signatureSource = $resultCode . $this->mntId . $this->callbackData['MNT_TRANSACTION_ID'] . $this->integrityCode; $data['MNT_SIGNATURE'] = md5($signatureSource); return $data; } 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 getDeliveryInfo(): ?array { $this->load->model('account/order'); foreach ($this->model_account_order->getTotals($this->orderId) as $orderTotal) { if (($orderTotal['code'] ?? '') === 'shipping') { return $orderTotal; } } return null; } private function getClientInfo(): array { $clientInfo = []; $name = $this->getClientName(); if (null !== $name) { $clientInfo['name'] = $name; } $email = $this->getClientEmail(); if (null !== $email) { $clientInfo['email'] = $email; } $phoneNumber = $this->getClientPhone(); if (null !== $phoneNumber) { $clientInfo['phone'] = $phoneNumber; } return $clientInfo; } private function getCheckItems(array $products, ?array $deliveryInfo): array { $checkItems = array(); foreach ($products as $product) { $checkItems[] = array( 'name' => $this->validateString($product['name'] ?? '', self::ITEM_NAME_MAX_LENGTH), 'price' => $this->formatPrice((float)($product['price'] ?? 0)), 'quantity' => $this->formatQuantity((float)($product['quantity'] ?? 0)), 'vat' => $this->getVat($this->vatRate), 'measure' => self::DEFAULT_MEASURE, 'paymentMethod' => self::DEFAULT_PAYMENT_METHOD, 'paymentObject' => self::DEFAULT_PAYMENT_OBJECT, ); } if (null === $deliveryInfo) { return $checkItems; } $checkItems[] = array( 'name' => $this->validateString($deliveryInfo['title'] ?? '', self::ITEM_NAME_MAX_LENGTH), 'price' => $this->formatPrice((float)($deliveryInfo['value'] ?? 0)), 'quantity' => 1, 'vat' => $this->getVat($this->vatRate), 'measure' => self::DEFAULT_MEASURE, 'paymentMethod' => self::DEFAULT_PAYMENT_METHOD, 'paymentObject' => self::SHIPPING_PAYMENT_OBJECT, ); return $checkItems; } private function getPayInventoryJson(array $products, ?array $deliveryInfo): false|string { if (empty($products) && empty($deliveryInfo)) { return false; } $inventory = []; foreach ($products as $product) { $inventory[] = [ 'name' => $this->validateString($product['name'] ?? '', self::ITEM_NAME_MAX_LENGTH), 'price' => $this->formatPrice((float)($product['price'] ?? 0)), 'quantity' => $this->formatQuantity((float)($product['quantity'] ?? 0)), 'vatTag' => $this->vatRate, 'pm' => self::DEFAULT_PAYMENT_METHOD, 'po' => self::DEFAULT_PAYMENT_OBJECT, 'measure' => self::DEFAULT_MEASURE, ]; } if (null !== $deliveryInfo) { $inventory[] = [ 'name' => $this->validateString($deliveryInfo['title'] ?? '', self::ITEM_NAME_MAX_LENGTH), 'price' => $this->formatPrice((float)($deliveryInfo['value'] ?? 0)), 'quantity' => '1', 'vatTag' => $this->vatRate, 'pm' => self::DEFAULT_PAYMENT_METHOD, 'po' => self::SHIPPING_PAYMENT_OBJECT, 'measure' => self::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(float $quantity): string { $quantityString = number_format($quantity, 3, '.', ''); return rtrim(rtrim($quantityString, '0'), '.'); } /** * @param array $data * @return array */ public function buildJsonResponse(array $data): array { $jsonData = [ 'id' => $data['MNT_ID'], 'transactionId' => $data['MNT_TRANSACTION_ID'], 'amount' => $data['MNT_AMOUNT'], 'signature' => $data['MNT_SIGNATURE'], 'resultCode' => $data['MNT_RESULT_CODE'], 'description' => $data['MNT_DESCRIPTION'], 'externalId' => $data['MNT_TRANSACTION_ID'], 'receipt' => [ 'client' => $data['client'], 'items' => $data['items'], ], ]; $json = json_encode($jsonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); return [ 'data' => $json ?: '', 'contentType' => self::JSON_CONTENT_TYPE, ]; } /** * @param array $data * @throws \Exception */ private function buildXmlResponse(array $data): array { $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); } } $xml = $dom->saveXML(); return array( 'data' => (false !== $xml) ? $xml : self::FAIL_RESPONSE, 'contentType' => (false !== $xml) ? self::XML_CONTENT_TYPE : self::TEXT_CONTENT_TYPE, ); } public function getVat($vatRate): string { return match ($vatRate) { '1104' => 'vat0', '1108' => 'vat5', '1109' => 'vat7', '1103' => 'vat10', '7000' => 'vat20', '1113' => 'vat22', '1110' => 'vat105', '1111' => 'vat107', '1107' => 'vat110', '7001' => 'vat120', '1114' => 'vat122', default => 'none' }; } /** * @return array * @throws \Exception */ public function getAccountOrderProducts(): array { $this->load->model('account/order'); return $this->model_account_order->getProducts($this->orderId); } /** @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; } }