*/ private $orderInfo; /** @throws \Exception */ public function __construct($registry) { parent::__construct($registry); $this->initializeModule(); $this->orderId = $this->getOrderId(); $this->orderInfo = $this->getOrderInfo(); } /** * @return string * @throws \Exception */ public function index() { $this->load->language('extension/payment/payanyway'); $this->document->setTitle($this->language->get('heading_title')); $data['action'] = (self::PAYMENT_SERVER_PROD === $this->server) ? self::PAYMENT_URL_PROD : self::PAYMENT_URL_DEMO; $orderTotal = (float)(isset($this->orderInfo['total']) ? $this->orderInfo['total'] : 0); $data['MNT_ID'] = $this->mntId; $data['MNT_TRANSACTION_ID'] = $this->createTransactionId(); $data['MNT_AMOUNT'] = $this->formatPrice($orderTotal); $data['MNT_CURRENCY_CODE'] = $this->currency; $data['MNT_TEST_MODE'] = '0'; $data['MNT_DESCRIPTION'] = $this->getDescription(); $data['MNT_SUBSCRIBER_ID'] = $this->getSubscriberId(); $signature = md5( $data['MNT_ID'] . $data['MNT_TRANSACTION_ID'] . $data['MNT_AMOUNT'] . $data['MNT_CURRENCY_CODE'] . $data['MNT_SUBSCRIBER_ID'] . $data['MNT_TEST_MODE'] . $this->integrityCode ); $data['MNT_SIGNATURE'] = $signature; $data['MNT_SUCCESS_URL'] = $this->url->link('extension/payment/payanyway/success', '', true); $data['MNT_FAIL_URL'] = $this->url->link('extension/payment/payanyway/fail', '', true); $data['MNT_RETURN_URL'] = $this->url->link('checkout/checkout', '', true); $data['moneta_locale'] = $this->language->get('text_locale'); $data['MNT_CMS'] = $this->cmsModuleVersion; $data['btn_confirm'] = $this->language->get('text_paw_pay'); $data['confirm_url'] = $this->url->link('extension/payment/payanyway/confirm', '', true); return $this->load->view('extension/payment/payanyway', $data); } /** * @return void * @throws \Exception */ public function confirm() { $json = array(); $this->load->language('extension/payment/payanyway'); $this->addOrderHistory($this->pendingOrderStatusId, $this->language->get('text_order_confirmed')); $json['success'] = true; $this->response->addHeader('Content-Type: application/json'); $this->response->setOutput(json_encode($json)); } /** * @return void * @throws \Exception */ public function success() { $this->load->language('extension/payment/payanyway'); $orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null; if ((null !== $orderStatusId) && ($orderStatusId !== $this->successOrderStatusId)) { $this->addOrderHistory($this->pendingOrderStatusId, $this->language->get('text_payment_processing')); } $this->response->redirect($this->url->link('checkout/success', '', true)); } /** * @return void * @throws \Exception */ public function fail() { $this->load->language('extension/payment/payanyway'); $this->addOrderHistory($this->errorOrderStatusId, $this->language->get('text_payment_error')); $this->response->redirect($this->url->link('checkout/failure', '', true)); } /** @return int|null */ private function getOrderId() { $orderId = $this->getOrderIdFromTransactionId(); if (null !== $orderId) { return $orderId; } return $this->getOrderIdFromSession(); } /** * @return false|array * @throws \Exception */ private function getOrderInfo() { if (null === $this->orderId) { return array(); } $this->load->model('checkout/order'); return $this->model_checkout_order->getOrder($this->orderId); } /** @return string */ private function createTransactionId() { $date = date('YmdHis'); return $this->orderId . self::TRANSACTION_ID_STRING_DELIMITER . $date; } /** * @param float $price * @return string */ private function formatPrice($price) { return number_format($price, 2, '.', ''); } /** @return string */ private function getDescription() { $orderId = isset($this->orderInfo['order_id']) ? $this->orderInfo['order_id'] : null; $description = "Оплата заказа" . (!empty($orderId) ? " №{$orderId}" : ''); $clientName = $this->getClientName(); if (null !== $clientName) { $description .= " от {$clientName}"; } $comment = isset($this->orderInfo['comment']) ? $this->orderInfo['comment'] : ''; if ('' !== $comment) { $description .= ': ' . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8'); } return $this->validateString($description, self::DESCRIPTION_MAX_LENGTH); } /** * @param string $value * @param int $maxLength * @return string */ private function validateString($value, $maxLength) { $maxLength = max(0, $maxLength); $value = $this->sanitizeString($value); if (mb_strlen($value, 'UTF-8') <= $maxLength) { return $value; } $trimLength = max(0, $maxLength - 3); return mb_substr($value, 0, $trimLength, 'UTF-8') . ($maxLength > 3 ? '...' : ''); } /** * @param string $value * @return string */ private function sanitizeString($value) { $decoded = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $cleaned = preg_replace('/[^\p{L}\p{N}\s.,()_№+-]/u', '', $decoded); $cleaned = str_replace(['&', '/', '\\', ';', '%', '#', '"', "'"], '', $cleaned); return trim(preg_replace('/\s+/', ' ', $cleaned)); } /** @return string */ private function getSubscriberId() { $email = $this->getClientEmail(); if (null !== $email) { return $email; } $phone = $this->getClientPhone(); return (null !== $phone) ? $phone : ''; } /** * @param int $orderStatusId * @param string $comment * @return void * @throws \Exception */ private function addOrderHistory($orderStatusId, $comment) { $this->load->model('checkout/order'); $this->model_checkout_order->addOrderHistory($this->orderId, $orderStatusId, $comment); } /** * @return void * @throws \Exception */ public function callback() { if (null === $this->orderId) { $this->sendResponse('FAIL'); } if (false === $this->orderInfo) { $this->sendResponse('FAIL'); } $requestData = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET; $callbackData = $this->getCallbackData($requestData); (isset($callbackData['MNT_COMMAND'])) && ('CHECK' === $callbackData['MNT_COMMAND']) ? $this->handleCheckCallback($callbackData) : $this->handlePayCallback($callbackData); } /** * @param array $requestData * @return array */ private function getCallbackData(array $requestData) { return array( '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'), ); } /** @return int|null */ private function getOrderIdFromTransactionId() { if (!isset($_REQUEST['MNT_TRANSACTION_ID'])) { return null; } $transactionIdData = explode(self::TRANSACTION_ID_STRING_DELIMITER, $_REQUEST['MNT_TRANSACTION_ID'], 2); return isset($transactionIdData[0]) ? (int)$transactionIdData[0] : null; } /** @return int|null */ private function getOrderIdFromSession() { return isset($this->session->data['order_id']) ? (int)$this->session->data['order_id'] : null; } /** * @param string $response * @param int $statusCode * @return void */ private function sendResponse($response, $statusCode = 200) { http_response_code($statusCode); if (strpos($response, ' $data * @return bool */ private function checkSignature(array $data, $callbackSignature) { $baseFields = array( '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, function ($carry, $field) use ($data) { return $carry . $data[$field]; }, '') . $this->integrityCode; return hash_equals(md5($signatureString), $callbackSignature); } /** * @param array $callbackData * @return void * @throws \Exception */ private function handleCheckCallback(array $callbackData) { if (!$this->checkSignature($callbackData, $callbackData['MNT_SIGNATURE'])) { $this->sendResponse('FAIL'); } list($resultCode, $description) = $this->determineCheckResult($callbackData); $responseData = $this->buildResponseData($callbackData, $resultCode, $description); $this->sendResponse($this->buildXMLResponse($responseData)); } /** * @param array $callbackData * @return void * @throws \Exception */ private function handlePayCallback(array $callbackData) { $orderTotal = (float)(isset($this->orderInfo['total']) ? $this->orderInfo['total'] : 0); $currencyCode = isset($this->orderInfo['currency_code']) ? $this->orderInfo['currency_code'] : 'RUB'; $shopData = array( 'MNT_ID' => $this->mntId, 'MNT_TRANSACTION_ID' => $callbackData['MNT_TRANSACTION_ID'], 'MNT_OPERATION_ID' => $callbackData['MNT_OPERATION_ID'], 'MNT_AMOUNT' => $this->formatPrice($orderTotal), 'MNT_CURRENCY_CODE' => $currencyCode, 'MNT_SUBSCRIBER_ID' => $callbackData['MNT_SUBSCRIBER_ID'], 'MNT_TEST_MODE' => $callbackData['MNT_TEST_MODE'], ); if (!$this->checkSignature($shopData, $callbackData['MNT_SIGNATURE'])) { $this->sendResponse('FAIL'); } $isPayedOrder = $this->isOrderPaid(); if (!$isPayedOrder) { $this->load->language('extension/payment/payanyway'); $this->addOrderHistory($this->successOrderStatusId, $this->language->get('text_payment_completed')); } $resultCode = 200; $description = $isPayedOrder ? 'Order already paid' : 'Order success paid'; $responseData = $this->buildResponseData($callbackData, $resultCode, $description); $this->sendResponse($this->buildXMLResponse($responseData)); } /** * @param array $callbackData * @param int $resultCode * @param string $description * @return array * @throws \Exception */ private function buildResponseData(array $callbackData, $resultCode, $description) { $orderTotal = (float)(isset($this->orderInfo['total']) ? $this->orderInfo['total'] : 0); $data = array( 'MNT_ID' => $this->mntId, 'MNT_TRANSACTION_ID' => $callbackData['MNT_TRANSACTION_ID'], 'MNT_RESULT_CODE' => $resultCode, 'MNT_DESCRIPTION' => $description, 'MNT_AMOUNT' => $this->formatPrice($orderTotal), 'MNT_CURRENCY_CODE' => $callbackData['MNT_CURRENCY_CODE'], ); $signatureSource = $resultCode . $this->mntId . $callbackData['MNT_TRANSACTION_ID'] . $this->integrityCode; $data['MNT_SIGNATURE'] = md5($signatureSource); $this->load->model('account/order'); $products = $this->model_account_order->getOrderProducts($this->orderId); $deliveryPrice = $this->getDeliveryPrice(); $data['inventory'] = $this->getInventoryJson($products) ?: null; $data['client'] = $this->getClientInfoJson() ?: null; $data['sno'] = null; $data['delivery'] = (null !== $deliveryPrice) ? $this->formatPrice($deliveryPrice) : null; return $data; } /** * @param array $callbackData * @return array * @throws \Exception */ private function determineCheckResult(array $callbackData) { $orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null; if (null === $orderStatusId) { return array(500, 'Order status not set'); } $orderStatus = $this->getOrderStatusById($orderStatusId); if (empty($callbackData['MNT_AMOUNT'])) { return array(100, "Order status is '{$orderStatus}'"); } if ($this->successOrderStatusId === $orderStatusId) { return array(200, 'Order Paid'); } if (in_array($orderStatusId, array(7, 9, 16), true)) { return array(500, "Order status is '{$orderStatus}'"); } return array(402, 'Order created, but not paid'); } /** @return bool */ private function isOrderPaid() { if (!isset($this->orderInfo['order_status_id'])) { return false; } return $this->successOrderStatusId === (int)$this->orderInfo['order_status_id']; } /** * @return float|null * @throws \Exception */ private function getDeliveryPrice() { $this->load->model('account/order'); foreach ($this->model_account_order->getOrderTotals($this->orderId) as $orderTotal) { if (isset($orderTotal['code']) && ($orderTotal['code'] === 'shipping')) { $deliveryPrice = isset($orderTotal['value']) ? (float)$orderTotal['value'] : 0; return ($deliveryPrice !== 0.0) ? $deliveryPrice : null; } } return null; } /** * @param array $products * @return false|string */ private function getInventoryJson(array $products) { if (empty($products)) { return false; } $inventory = array(); foreach ($products as $product) { $name = isset($product['name']) ? $product['name'] : ''; $price = isset($product['price']) ? (float)$product['price'] : 0; $quantity = isset($product['quantity']) ? (float)$product['quantity'] : 0; $inventory[] = array( 'name' => $this->validateString($name, self::INVENTORY_ITEM_NAME_MAX_LENGTH), 'price' => $this->formatPrice($price), 'quantity' => $this->formatQuantity($quantity), '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); } /** @return false|string */ private function getClientInfoJson() { $clientInfo = array(); $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; } if (empty($clientInfo)) { return false; } return json_encode(array($clientInfo), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } /** * @param int $orderStatusId * @return string * @throws \Exception */ public function getOrderStatusById($orderStatusId) { $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; } /** * @param float $quantity * @return string */ private function formatQuantity($quantity) { $quantityString = number_format($quantity, 3, '.', ''); return rtrim(rtrim($quantityString, '0'), '.'); } /** * @param array $data * @return false|string * @return bool|string * @throws \Exception */ private function buildXMLResponse(array $data) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = false; $root = $dom->createElement('MNT_RESPONSE'); $dom->appendChild($root); $requiredFields = array( '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 = array( 'INVENTORY' => isset($data['inventory']) ? $data['inventory'] : null, 'CLIENT' => isset($data['client']) ? $data['client'] : null, 'SNO' => isset($data['sno']) ? $data['sno'] : null, 'DELIVERY' => isset($data['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 (false !== $xml) ? $xml : 'FAIL'; } /** * @return non-empty-string|null */ private function getClientName() { $firstname = isset($this->orderInfo['firstname']) ? $this->orderInfo['firstname'] : ''; $lastname = isset($this->orderInfo['lastname']) ? $this->orderInfo['lastname'] : ''; $name = trim($firstname . ' ' . $lastname); return ('' !== $name) ? $this->validateString($name, self::CLIENT_NAME_MAX_LENGTH) : null; } /** * @return non-empty-string|null */ private function getClientEmail() { $email = isset($this->orderInfo['email']) ? $this->orderInfo['email'] : ''; $email = trim((string)$email); return ('' !== $email) ? filter_var($email, FILTER_SANITIZE_EMAIL) : null; } /** * @return non-empty-string|null */ private function getClientPhone() { $telephone = isset($this->orderInfo['telephone']) ? $this->orderInfo['telephone'] : ''; $digits = preg_replace('/\D/', '', trim((string)$telephone)); return ('' !== $digits) ? '+' . $digits : null; } /** * @param array $params * @param string $key * @param string $default * @return string */ private function getOptionalStringWithDefault(array $params, $key, $default = '') { $optionalString = $this->getOptionalString($params, $key); if (null !== $optionalString) { return $optionalString; } return $default; } /** * @param array $params * @param string $key * @return string|null */ private function getOptionalString(array $params, $key) { $value = isset($params[$key]) ? $params[$key] : null; if (!is_string($value) || trim($value) === '') { return null; } return $value; } /** * @param array $params * @param string $key * @return string * @throws \InvalidArgumentException */ private function getRequiredString(array $params, $key) { 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; } private function initializeModule() { $this->mntId = (int)$this->config->get('payment_payanyway_account'); $this->integrityCode = (string)$this->config->get('payment_payanyway_integrity_code'); $this->server = (string)$this->config->get('payment_payanyway_server'); $this->vatRate = (string)$this->config->get('payment_payanyway_vat_rate'); $this->currency = $this->config->get('payment_payanyway_currency'); $this->pendingOrderStatusId = (int)$this->config->get('payment_payanyway_pending_order_status_id'); $this->successOrderStatusId = (int)$this->config->get('payment_payanyway_success_order_status_id'); $this->errorOrderStatusId = (int)$this->config->get('payment_payanyway_error_order_status_id'); $this->cmsModuleVersion = (string)$this->config->get('payment_payanyway_cms_module_version'); } }