diff --git a/README.md b/README.md index 3776fdc..40d4560 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@   -> **📦 Версия модуля: 1.0.3** +> **📦 Версия модуля: 1.0.4** > **ℹ️ Совместимость версий OpenCart:** > 3.0.0.0, 3.0.1.1, 3.0.1.2, 3.0.2.0, 3.0.3.0, 3.0.3.1, 3.0.3.2, 3.0.3.3, 3.0.3.4, 3.0.3.5, 3.0.3.6, 3.0.3.7, 3.0.3.8, 3.0.3.9, 3.0.4.0, 3.0.4.1, 3.0.5.0 @@ -29,11 +29,11 @@ │ │ ├── view/image/payment/payanyway.png │ │ └── view/template/extension/payment/payanyway.twig │ └── catalog -│ ├── controller/extension/payment/payanyway.php +│ ├── controller/extension/payment/payanyway.php — форма оплаты, Check URL и Pay URL │ ├── language/{en-gb,ru-ru}/extension/payment/payanyway.php │ ├── model/extension/payment/payanyway.php │ └── view/theme/default/template/extension/payment/payanyway.twig -├── install.xml +├── install.xml — OCMOD: приём Check/Pay URL методом POST (правка роутера витрины) └── README.md ``` @@ -48,20 +48,31 @@ - **«Редактировать»** — для перехода к настройкам 4. ✏️ Заполните соответствующие поля в настройках способа оплаты: -| Поле | Описание | -|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------| -| **Включить модуль оплаты** | Включите или выключите модуль оплаты | -| **Номер счёта** | Ван номер расширенного счёта в системе [MONETA.RU](https://moneta.ru/) | -| **Код проверки целостности данных** | Код проверки целостности данных, указанный в настройках расширенного счёта в системе [MONETA.RU](https://moneta.ru/) | -| **Сервер платежей** | `Боевой` / `Тестовый` | -| **НДС** | Выберите из списка | -| **Валюта платежа** | `RUB` | -| **Статусы заказа** | Выберите соответствующие статусы заказа из списка | -| **Обрабатываемый** | Укажите статус заказа, который выставляется при его создании | -| **Успешный** | Укажите статус заказа, который выставляется при успешной оплате | -| **Неуспешный** | Укажите статус заказа, который выставляется при ошибке оплаты | -| **Регион отображения** | Географическая зона, в которой будет отображаться способ оплаты | -| **Порядок сортировки** | Определяет порядок отображения платежного метода в списке методов оплаты при оформлении заказа. Чем меньше число, тем выше позиция в списке | +| Поле | Описание | +|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Включить модуль оплаты** | Включите или выключите модуль оплаты | +| **Номер счёта** | Ваш номер расширенного счёта в системе [MONETA.RU](https://moneta.ru/), только цифры | +| **Код проверки целостности данных** | Код проверки целостности данных, указанный в настройках расширенного счёта в системе [MONETA.RU](https://moneta.ru/). Сохраняется ровно так, как введён, в том числе с символами `&`, `"`, `<`, `>` | +| **Сервер платежей** | `Боевой` / `Демо` (демо-площадка MONETA.RU) | +| **Тестовый режим** | Имитация оплаты без реального списания средств. Должен совпадать с настройкой счёта в MONETA.RU: уведомление в другом режиме модуль отклоняет | +| **Фискализация чеков** | Включайте, только если к счёту подключён кассовый сервис [kassa.payanyway.ru](https://kassa.payanyway.ru/). Включена — на проверочный запрос модуль отвечает чеком для кассы (без кассы MONETA.RU вернёт ошибку `-700`). Выключена — в операцию уходят только позиции заказа и контакты покупателя. По умолчанию включена | +| **Email для чеков** | Обязателен при фискализации. Подставляется в чек, только если у покупателя нет ни e-mail, ни телефона (ФИО покупателя в чеке остаётся) | +| **НДС** | Ставка НДС всех позиций чека | +| **Признак способа расчёта** | Для товаров, доставки и сборов. Обычно `Полный расчёт`. Проданные подарочные сертификаты всегда идут авансом | +| **Признак предмета расчёта** | Для товаров. Доставка и сборы всегда идут услугой, подарочные сертификаты — платежом | +| **Валюта платежа** | `RUB` | +| **Статусы заказа** | Выберите соответствующие статусы заказа из списка | +| **Обрабатываемый** | Укажите статус заказа, который выставляется при его создании | +| **Успешный** | Укажите статус заказа, который выставляется при успешной оплате | +| **Неуспешный** | Укажите статус заказа, который выставляется при ошибке оплаты | +| **Отменённые** | Заказ в этих статусах не оплачивается: на проверочный запрос модуль отвечает отказом. По умолчанию `Canceled`, `Canceled Reversal`, `Voided` | +| **URL для уведомлений** | Адрес Check URL и Pay URL для настроек счёта MONETA.RU, кнопка «Скопировать» | +| **Регион отображения** | Географическая зона, в которой будет отображаться способ оплаты | +| **Порядок сортировки** | Определяет порядок отображения платежного метода в списке методов оплаты при оформлении заказа. Чем меньше число, тем выше позиция в списке | + +Сумма позиций чека всегда равна сумме оплаты: налоги OpenCart входят в цену товаров, скидки (купоны, подарочные сертификаты, бонусные баллы) распределяются по товарам, доставка и сборы идут отдельными позициями. В истории заказа при оплате сохраняется номер операции MONETA.RU: «Заказ оплачен. Номер операции: N». + +> ⚠️ **После обновления модуля** нажмите **«Модули / Расширения» → «Модификации» → «Обновить»**, затем откройте настройки PayAnyWay, проверьте новые поля (фискализация после обновления включена) и нажмите **«Сохранить»**. Пока при включённой фискализации не задан «Email для чеков», над формой настроек показывается предупреждение. --- @@ -76,7 +87,7 @@ | **Тестовый режим** | `Нет` | | **Check URL** | `https://your_site_name/index.php?route=extension/payment/payanyway/callback` | | **Pay URL** | `https://your_site_name/index.php?route=extension/payment/payanyway/callback` | -| **HTTP method** | `GET` | +| **HTTP method\*** | `GET` или `POST` (требуется настройка) | | **Можно переопределять настройки в URL** | `Да` | | **Подпись формы оплаты обязательна** | `Да` | | **Код проверки целостности данных** | `ваш_код` (произвольный набор символов) | @@ -87,6 +98,18 @@ > ⚠️ **Важно!** Для кириллического домена PayURL и CheckURL должны быть указаны в кодировке [Punycode](https://2ip.ru/punycode/). +### \* 📨 Уведомления методом POST + +При **HTTP method** = `POST` MONETA.RU переносит параметры из адреса Check URL / Pay URL в тело запроса, в том числе `route=extension/payment/payanyway/callback`. Штатный OpenCart 3 ищет маршрут только в адресе и без доработки отвечает главной страницей, а платёжная форма показывает «Код ошибки: 302». + +Модуль решает это модификацией OCMOD из `install.xml`: маршрут колбэка (и только его) берётся из тела запроса. Чтобы модификация заработала: + +1. Устанавливайте модуль только через **«Модули / Расширения» → «Установка расширений»** (архив `payanyway.ocmod.zip`). При заливке файлов по FTP модификация не регистрируется. +2. После установки или обновления модуля откройте **«Модули / Расширения» → «Модификации»** (Extensions → Modifications) и нажмите синюю кнопку **«Обновить»** вверху справа. Повторяйте это после включения, выключения или удаления любой модификации и после кнопки «Очистить». +3. Откройте настройки PayAnyWay. Если модификация не применена, над формой будет жёлтое предупреждение. Причину смотрите на вкладке «Лог» в «Модификациях»: строка `NOT FOUND` у файла `catalog/controller/startup/router.php` означает, что роутер уже изменён другой модификацией. + +Если модификацию применить не удаётся, выберите в настройках счёта **HTTP method** = `GET`: при GET модификация не нужна. + --- ## 📚 Полезные ресурсы OpenCart diff --git a/install.xml b/install.xml index de05886..93021a5 100644 --- a/install.xml +++ b/install.xml @@ -2,7 +2,26 @@ PayAnyWay payanyway - 1.0.3 + 1.0.4 PayAnyWay https://payanyway.ru/ + + + + request->get['route']) && $this->request->get['route'] != 'startup/router') {]]> + request->get['route']) && isset($this->request->post['route']) && ($this->request->post['route'] === 'extension/payment/payanyway/callback')) { + $this->request->get['route'] = $this->request->post['route']; + } +]]> + + diff --git a/upload/admin/controller/extension/payment/payanyway.php b/upload/admin/controller/extension/payment/payanyway.php index 50092de..f0d8209 100755 --- a/upload/admin/controller/extension/payment/payanyway.php +++ b/upload/admin/controller/extension/payment/payanyway.php @@ -9,9 +9,13 @@ class ControllerExtensionPaymentPayanyway extends Controller { const CMS_NAME = 'OpenCart'; const MODULE_NAME = 'payanyway'; - const MODULE_VERSION = '1.0.3'; + const MODULE_VERSION = '1.0.4'; const MODULE_DEFAULT_SORT_ORDER = 0; + /** Маркер вставки OCMOD из install.xml в роутере витрины (приём Check/Pay URL методом POST). */ + const OCMOD_ROUTER_MARKER = 'payanyway-post-route'; + const OCMOD_ROUTER_FILE = 'catalog/controller/startup/router.php'; + private $supportedCurrencies = array('RUB'); private $availableVatRates = array( 'vat_none' => '1105', @@ -26,6 +30,47 @@ class ControllerExtensionPaymentPayanyway extends Controller { 'vat122' => '1114' ); + /** Признак способа расчёта (kassaspecification.pdf, pm). */ + private $paymentMethods = array( + 'full_prepayment', + 'prepayment', + 'advance', + 'full_payment', + 'partial_payment', + 'credit', + 'credit_payment' + ); + + /** Признак предмета расчёта (kassaspecification.pdf, po). */ + private $paymentObjects = array( + 'commodity', + 'excise', + 'job', + 'service', + 'gambling_bet', + 'gambling_prize', + 'lottery', + 'lottery_prize', + 'intellectual_activity', + 'payment', + 'agent_commission', + 'composite', + 'another', + 'property_right', + 'sales_tax', + 'resort_fee', + 'award', + 'non_operating', + 'insurance_premium' + ); + + /** Canceled, Canceled Reversal, Voided штатной установки OpenCart. */ + private $defaultCanceledOrderStatusIds = array(7, 9, 16); + + const RECEIPT_EMAIL_MAX_LENGTH = 64; + const DEFAULT_PAYMENT_METHOD = 'full_payment'; + const DEFAULT_PAYMENT_OBJECT = 'commodity'; + private $error = array(); /** @@ -47,6 +92,15 @@ class ControllerExtensionPaymentPayanyway extends Controller { $data = array(); $this->applyValidationErrors($data); + $data['warning_ocmod'] = $this->isRouterModificationApplied() + ? '' + : sprintf( + $this->language->get('warning_ocmod_router'), + $this->url->link('marketplace/modification', 'user_token=' . $this->session->data['user_token'], true) + ); + $data['warning_receipt_email'] = ($this->isFiscalizationEnabled() && ('' === (string)$this->config->get('payment_payanyway_receipt_email'))) + ? $this->language->get('warning_receipt_email') + : ''; $data['breadcrumbs'] = $this->getBreadcrumbs(); $data['action'] = $this->url->link('extension/payment/payanyway', 'user_token=' . $this->session->data['user_token'], true); @@ -56,6 +110,13 @@ class ControllerExtensionPaymentPayanyway extends Controller { $data['payment_payanyway_account'] = $this->retrieveValue('payment_payanyway_account'); $data['payment_payanyway_integrity_code'] = $this->getIntegrityCode('payment_payanyway_integrity_code'); $data['payment_payanyway_server'] = $this->retrieveValue('payment_payanyway_server'); + $data['payment_payanyway_test_mode'] = (string)$this->retrieveValue('payment_payanyway_test_mode'); + $data['payment_payanyway_fiscalization'] = $this->isFiscalizationEnabled() ? '1' : '0'; + $data['payment_payanyway_receipt_email'] = htmlspecialchars((string)$this->retrieveRawValue('payment_payanyway_receipt_email'), ENT_QUOTES, 'UTF-8'); + $data['payment_payanyway_payment_methods'] = $this->getOptions($this->paymentMethods, 'entry_payment_method_'); + $data['payment_payanyway_payment_method'] = $this->retrieveValueOrDefault('payment_payanyway_payment_method', self::DEFAULT_PAYMENT_METHOD); + $data['payment_payanyway_payment_objects'] = $this->getOptions($this->paymentObjects, 'entry_payment_object_'); + $data['payment_payanyway_payment_object'] = $this->retrieveValueOrDefault('payment_payanyway_payment_object', self::DEFAULT_PAYMENT_OBJECT); $data['payment_payanyway_vat_rates'] = $this->getModuleVatRates(); $data['payment_payanyway_vat_rate'] = $this->retrieveValue('payment_payanyway_vat_rate'); $data['payment_payanyway_currencies'] = $this->getModuleCurrencies(); @@ -64,10 +125,11 @@ class ControllerExtensionPaymentPayanyway extends Controller { $data['payment_payanyway_pending_order_status_id'] = $this->retrieveValue('payment_payanyway_pending_order_status_id'); $data['payment_payanyway_success_order_status_id'] = $this->retrieveValue('payment_payanyway_success_order_status_id'); $data['payment_payanyway_error_order_status_id'] = $this->retrieveValue('payment_payanyway_error_order_status_id'); + $data['payment_payanyway_canceled_order_status_ids'] = $this->getCanceledOrderStatusIds(); $data['geo_zones'] = $this->getAvailableGeoZones(); $data['payment_payanyway_geo_zone_id'] = $this->retrieveValue('payment_payanyway_geo_zone_id'); $data['payment_payanyway_sort_order'] = $this->retrieveValue('payment_payanyway_sort_order'); - $data['payment_payanyway_cms_module_version'] = $this->getCmsModuleVersion(); + $data['callback_url'] = HTTPS_CATALOG . 'index.php?route=extension/payment/payanyway/callback'; $data['header'] = $this->load->controller('common/header'); $data['column_left'] = $this->load->controller('common/column_left'); @@ -76,6 +138,39 @@ class ControllerExtensionPaymentPayanyway extends Controller { $this->response->setOutput($this->load->view('extension/payment/payanyway', $data)); } + /** + * @return bool + */ + private function isFiscalizationEnabled() { + $value = $this->retrieveValue('payment_payanyway_fiscalization'); + + return (null === $value) ? true : (bool)$value; + } + + /** + * @return int[] + */ + private function getCanceledOrderStatusIds() { + $value = $this->retrieveValue('payment_payanyway_canceled_order_status_ids'); + if (null === $value) { + return $this->defaultCanceledOrderStatusIds; + } + + return array_map('intval', (array)$value); + } + + /** + * @return bool + */ + private function isRouterModificationApplied() { + $file = DIR_MODIFICATION . self::OCMOD_ROUTER_FILE; + if (!is_file($file)) { + return false; + } + + return false !== strpos((string)file_get_contents($file), self::OCMOD_ROUTER_MARKER); + } + /** * @return void * @throws \Exception @@ -86,8 +181,14 @@ class ControllerExtensionPaymentPayanyway extends Controller { 'payment_payanyway_account' => '', 'payment_payanyway_integrity_code' => '', 'payment_payanyway_server' => 'prod', + 'payment_payanyway_test_mode' => 0, + 'payment_payanyway_fiscalization' => 1, + 'payment_payanyway_receipt_email' => '', + 'payment_payanyway_payment_method' => self::DEFAULT_PAYMENT_METHOD, + 'payment_payanyway_payment_object' => self::DEFAULT_PAYMENT_OBJECT, + 'payment_payanyway_canceled_order_status_ids' => $this->defaultCanceledOrderStatusIds, 'payment_payanyway_sort_order' => 0, - 'payment_payanyway_cms_module_version' => $this->getCmsModuleVersion(), + 'payment_payanyway_cms_name' => $this->getCmsName(), ); $orderStatuses = $this->getAvailableOrderStatuses(); @@ -139,14 +240,45 @@ class ControllerExtensionPaymentPayanyway extends Controller { } if (!isset($request->post['payment_payanyway_account']) || + !preg_match('/^[0-9]{1,20}$/', (string)$request->post['payment_payanyway_account']) || ((int)$request->post['payment_payanyway_account'] <= 0) ) { $this->error['account'] = $this->language->get('error_account'); } - if (empty($request->post['payment_payanyway_integrity_code'])) { + // OpenCart экранирует весь POST (htmlspecialchars в system/library/request.php): код с & " < > + // сохранился бы экранированным, и подписи не сошлись бы с MONETA.RU. + $integrityCode = $this->decodePostedString($request, 'payment_payanyway_integrity_code'); + if ('' === $integrityCode) { $this->error['integrity_code'] = $this->language->get('error_integrity_code'); } + $request->post['payment_payanyway_integrity_code'] = $integrityCode; + + $fiscalization = !empty($request->post['payment_payanyway_fiscalization']); + $request->post['payment_payanyway_fiscalization'] = $fiscalization ? 1 : 0; + $request->post['payment_payanyway_test_mode'] = empty($request->post['payment_payanyway_test_mode']) ? 0 : 1; + + $receiptEmail = $this->decodePostedString($request, 'payment_payanyway_receipt_email'); + if (($fiscalization || ('' !== $receiptEmail)) && !$this->isValidReceiptEmail($receiptEmail)) { + $this->error['receipt_email'] = $this->language->get('error_receipt_email'); + } + $request->post['payment_payanyway_receipt_email'] = $receiptEmail; + + if (!isset($request->post['payment_payanyway_payment_method']) || + !in_array($request->post['payment_payanyway_payment_method'], $this->paymentMethods, true) + ) { + $request->post['payment_payanyway_payment_method'] = self::DEFAULT_PAYMENT_METHOD; + } + if (!isset($request->post['payment_payanyway_payment_object']) || + !in_array($request->post['payment_payanyway_payment_object'], $this->paymentObjects, true) + ) { + $request->post['payment_payanyway_payment_object'] = self::DEFAULT_PAYMENT_OBJECT; + } + + $canceled = isset($request->post['payment_payanyway_canceled_order_status_ids']) + ? (array)$request->post['payment_payanyway_canceled_order_status_ids'] + : array(); + $request->post['payment_payanyway_canceled_order_status_ids'] = array_values(array_unique(array_map('intval', $canceled))); if (empty($request->post['payment_payanyway_pending_order_status_id'])) { $this->error['pending_order_status'] = $this->language->get('error_order_status'); @@ -162,7 +294,7 @@ class ControllerExtensionPaymentPayanyway extends Controller { ? (int)$request->post['payment_payanyway_sort_order'] : self::MODULE_DEFAULT_SORT_ORDER; $request->post['payment_payanyway_sort_order'] = $sortOrder; - $request->post['payment_payanyway_cms_module_version'] = $this->getCmsModuleVersion(); + $request->post['payment_payanyway_cms_name'] = $this->getCmsName(); return empty($this->error); } @@ -175,6 +307,7 @@ class ControllerExtensionPaymentPayanyway extends Controller { $data['error_warning'] = isset($this->error['warning']) ? $this->error['warning'] : ''; $data['error_account'] = isset($this->error['account']) ? $this->error['account'] : ''; $data['error_integrity_code'] = isset($this->error['integrity_code']) ? $this->error['integrity_code'] : ''; + $data['error_receipt_email'] = isset($this->error['receipt_email']) ? $this->error['receipt_email'] : ''; $data['error_pending_order_status'] = isset($this->error['pending_order_status']) ? $this->error['pending_order_status'] : ''; $data['error_success_order_status'] = isset($this->error['success_order_status']) ? $this->error['success_order_status'] : ''; $data['error_error_order_status'] = isset($this->error['error_order_status']) ? $this->error['error_order_status'] : ''; @@ -203,12 +336,31 @@ class ControllerExtensionPaymentPayanyway extends Controller { * @return string */ private function getIntegrityCode($key) { - $value = $this->retrieveValue($key); - if (null === $value) { - return ''; + return htmlspecialchars((string)$this->retrieveRawValue($key), ENT_QUOTES, 'UTF-8'); + } + + /** + * @param string $email + * @return bool + */ + private function isValidReceiptEmail($email) { + return ('' !== $email) + && (strlen($email) <= self::RECEIPT_EMAIL_MAX_LENGTH) + && (false !== filter_var($email, FILTER_VALIDATE_EMAIL)); + } + + /** + * @param string[] $values + * @param string $labelPrefix + * @return array + */ + private function getOptions(array $values, $labelPrefix) { + $result = array(); + foreach ($values as $value) { + $result[$value] = $this->language->get($labelPrefix . $value); } - return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + return $result; } /** @return array */ @@ -280,22 +432,15 @@ class ControllerExtensionPaymentPayanyway extends Controller { return $this->model_localisation_geo_zone->getGeoZones(); } - /** @return string */ - private function getCmsModuleVersion() { + /** + * @return string + */ + private function getCmsName() { $this->load->language('common/footer'); $textFooter = $this->language->get('text_footer'); $textFooter = is_string($textFooter) ? $textFooter : ''; - $cmsName = $this->detectCmsByPatterns($textFooter); - $cmsVersion = defined('VERSION') ? constant('VERSION') : '.unknown'; - - return sprintf('%s v%s|PHP %s|%s v%s', - $cmsName, - $cmsVersion, - PHP_VERSION, - self::MODULE_NAME, - self::MODULE_VERSION - ); + return $this->detectCmsByPatterns($textFooter); } private function detectCmsByPatterns($textFooter) { @@ -331,4 +476,41 @@ class ControllerExtensionPaymentPayanyway extends Controller { return $this->config->get($name); } + + /** + * Строка с формы без экранирования OpenCart: htmlspecialchars_decode обращает ENT_COMPAT + * из system/library/request.php. + * + * @param \Request $request + * @param string $name + * @return string + */ + private function decodePostedString($request, $name) { + $value = isset($request->post[$name]) && is_string($request->post[$name]) ? $request->post[$name] : ''; + + return trim(htmlspecialchars_decode($value, ENT_COMPAT)); + } + + /** + * @param string $name + * @return string + */ + private function retrieveRawValue($name) { + if (isset($this->request->post[$name]) && is_string($this->request->post[$name])) { + return $this->request->post[$name]; + } + + return (string)$this->config->get($name); + } + + /** + * @param string $name + * @param string $default + * @return string + */ + private function retrieveValueOrDefault($name, $default) { + $value = (string)$this->retrieveValue($name); + + return ('' !== $value) ? $value : $default; + } } diff --git a/upload/admin/language/en-gb/extension/payment/payanyway.php b/upload/admin/language/en-gb/extension/payment/payanyway.php index 7bc0459..bac6073 100755 --- a/upload/admin/language/en-gb/extension/payment/payanyway.php +++ b/upload/admin/language/en-gb/extension/payment/payanyway.php @@ -44,6 +44,41 @@ $_['entry_error_order_status_label'] = 'Error'; $_['entry_geo_zone_label'] = 'Area of display'; $_['entry_sort_order_label'] = 'Sort Order'; +$_['entry_test_mode_label'] = 'Test mode'; +$_['entry_fiscalization_label'] = 'Receipt fiscalization'; +$_['entry_receipt_email_label'] = 'Email for receipts'; +$_['entry_payment_method_label'] = 'Payment method'; +$_['entry_payment_object_label'] = 'Payment object'; +$_['entry_canceled_order_status_label'] = 'Canceled'; +$_['entry_callback_url_label'] = 'Notification URL'; + +$_['entry_payment_method_full_prepayment'] = 'Full prepayment'; +$_['entry_payment_method_prepayment'] = 'Partial prepayment'; +$_['entry_payment_method_advance'] = 'Advance'; +$_['entry_payment_method_full_payment'] = 'Full payment'; +$_['entry_payment_method_partial_payment'] = 'Partial payment and credit'; +$_['entry_payment_method_credit'] = 'Credit'; +$_['entry_payment_method_credit_payment'] = 'Credit payment'; +$_['entry_payment_object_commodity'] = 'Commodity'; +$_['entry_payment_object_excise'] = 'Excise commodity'; +$_['entry_payment_object_job'] = 'Job'; +$_['entry_payment_object_service'] = 'Service'; +$_['entry_payment_object_gambling_bet'] = 'Gambling bet'; +$_['entry_payment_object_gambling_prize'] = 'Gambling prize'; +$_['entry_payment_object_lottery'] = 'Lottery ticket'; +$_['entry_payment_object_lottery_prize'] = 'Lottery prize'; +$_['entry_payment_object_intellectual_activity'] = 'Intellectual activity'; +$_['entry_payment_object_payment'] = 'Payment (advance, deposit, prepayment, credit)'; +$_['entry_payment_object_agent_commission'] = 'Agent commission'; +$_['entry_payment_object_composite'] = 'Composite'; +$_['entry_payment_object_another'] = 'Another'; +$_['entry_payment_object_property_right'] = 'Property right'; +$_['entry_payment_object_sales_tax'] = 'Sales tax'; +$_['entry_payment_object_resort_fee'] = 'Resort fee'; +$_['entry_payment_object_award'] = 'Contribution, penalty, fine, reward, bonus'; +$_['entry_payment_object_non_operating'] = 'Non-operating income'; +$_['entry_payment_object_insurance_premium'] = 'Insurance premium'; + // Help $_['help_pending_order_status'] = 'Specify the order status that is set when the order is created'; $_['help_success_order_status'] = 'Specify the order status that is set upon successful payment'; @@ -51,8 +86,24 @@ $_['help_error_order_status'] = 'Specify the order status that is set upon pay $_['help_geo_zone'] = 'Geographic area where the payment method will be displayed'; $_['help_sort_order'] = 'Determines the display order of the payment method. The smaller the number, the higher the position in the list'; +$_['help_test_mode'] = 'Simulates payments without charging. Must match the account setting in MONETA.RU'; +$_['help_fiscalization'] = 'Enable only if the kassa.payanyway.ru cash register service is connected to the account'; +$_['help_receipt_email'] = 'Required with fiscalization. Used in the receipt only when the customer has neither e-mail nor phone: the cash register does not accept a receipt without a contact, so it is sent to this address (the customer name stays in the receipt)'; +$_['help_canceled_order_status'] = 'Orders in these statuses are not paid: the module rejects the check request'; +$_['help_callback_url'] = 'Set this address as Check URL and Pay URL in the MONETA.RU account settings. It must be reachable from the internet'; + +// Button +$_['button_integrity_code_toggle'] = 'Show or hide'; +$_['button_copy'] = 'Copy'; +$_['text_copied'] = 'Copied'; + +// Warning +$_['warning_ocmod_router'] = 'The PayAnyWay router modification is not applied: Check URL and Pay URL notifications sent by POST will not reach the module. Click Extensions → Modifications → Refresh and check the modification log. Not needed if the account uses the GET method.'; +$_['warning_receipt_email'] = 'Receipt fiscalization is enabled but "Email for receipts" is empty: a customer without e-mail and phone will not get a receipt. Set the address below or disable fiscalization if no cash register service is connected.'; + // Error $_['error_permission'] = 'Warning: You do not have permission to modify payment PayAnyWay!'; -$_['error_account'] = 'Account Number is required and must be greater than 0'; +$_['error_account'] = 'Account number is required and must contain digits only'; $_['error_integrity_code'] = 'Account Integrity Code Required'; $_['error_order_status'] = 'Select order status'; +$_['error_receipt_email'] = 'Enter a valid e-mail no longer than 64 characters'; diff --git a/upload/admin/language/ru-ru/extension/payment/payanyway.php b/upload/admin/language/ru-ru/extension/payment/payanyway.php index 6b2e120..312ec3b 100755 --- a/upload/admin/language/ru-ru/extension/payment/payanyway.php +++ b/upload/admin/language/ru-ru/extension/payment/payanyway.php @@ -17,7 +17,7 @@ $_['entry_integrity_code_label'] = 'Код проверки целостн $_['entry_integrity_code_description'] = 'Код проверки целостности данных, указанный в настройках расширенного счёта в системе MONETA.RU'; $_['entry_server_label'] = 'Сервер платежей'; $_['entry_server_prod_label'] = 'Боевой'; -$_['entry_server_demo_label'] = 'Тестовый'; +$_['entry_server_demo_label'] = 'Демо'; $_['entry_vat_rate_label'] = 'НДС'; $_['entry_vat_none_label'] = 'Без НДС'; @@ -44,6 +44,41 @@ $_['entry_error_order_status_label'] = 'Неуспешный'; $_['entry_geo_zone_label'] = 'Регион отображения'; $_['entry_sort_order_label'] = 'Порядок сортировки'; +$_['entry_test_mode_label'] = 'Тестовый режим'; +$_['entry_fiscalization_label'] = 'Фискализация чеков'; +$_['entry_receipt_email_label'] = 'Email для чеков'; +$_['entry_payment_method_label'] = 'Признак способа расчёта'; +$_['entry_payment_object_label'] = 'Признак предмета расчёта'; +$_['entry_canceled_order_status_label'] = 'Отменённые'; +$_['entry_callback_url_label'] = 'URL для уведомлений'; + +$_['entry_payment_method_full_prepayment'] = 'Полная предоплата'; +$_['entry_payment_method_prepayment'] = 'Частичная предоплата'; +$_['entry_payment_method_advance'] = 'Аванс'; +$_['entry_payment_method_full_payment'] = 'Полный расчёт'; +$_['entry_payment_method_partial_payment'] = 'Частичный расчёт и кредит'; +$_['entry_payment_method_credit'] = 'Передача в кредит'; +$_['entry_payment_method_credit_payment'] = 'Оплата кредита'; +$_['entry_payment_object_commodity'] = 'Товар'; +$_['entry_payment_object_excise'] = 'Подакцизный товар'; +$_['entry_payment_object_job'] = 'Работа'; +$_['entry_payment_object_service'] = 'Услуга'; +$_['entry_payment_object_gambling_bet'] = 'Ставка азартной игры'; +$_['entry_payment_object_gambling_prize'] = 'Выигрыш азартной игры'; +$_['entry_payment_object_lottery'] = 'Лотерейный билет'; +$_['entry_payment_object_lottery_prize'] = 'Выигрыш лотереи'; +$_['entry_payment_object_intellectual_activity'] = 'Результаты интеллектуальной деятельности'; +$_['entry_payment_object_payment'] = 'Платёж (аванс, задаток, предоплата, кредит)'; +$_['entry_payment_object_agent_commission'] = 'Агентское вознаграждение'; +$_['entry_payment_object_composite'] = 'Составной предмет расчёта'; +$_['entry_payment_object_another'] = 'Иной предмет расчёта'; +$_['entry_payment_object_property_right'] = 'Имущественное право'; +$_['entry_payment_object_sales_tax'] = 'Торговый сбор'; +$_['entry_payment_object_resort_fee'] = 'Курортный сбор'; +$_['entry_payment_object_award'] = 'Взнос в счёт оплаты, пени, штраф, вознаграждение, бонус'; +$_['entry_payment_object_non_operating'] = 'Внереализационный доход'; +$_['entry_payment_object_insurance_premium'] = 'Страховой сбор'; + // Help $_['help_pending_order_status'] = 'Укажите статус заказа, который выставляется при его создании'; $_['help_success_order_status'] = 'Укажите статус заказа, который выставляется при успешной оплате'; @@ -51,8 +86,24 @@ $_['help_error_order_status'] = 'Укажите статус заказа, к $_['help_geo_zone'] = 'Географическая зона, в которой будет отображаться способ оплаты'; $_['help_sort_order'] = 'Определяет порядок отображения платежного метода в списке методов оплаты при оформлении заказа. Чем меньше число, тем выше позиция в списке'; +$_['help_test_mode'] = 'Имитация оплаты без реального списания средств. Режим должен совпадать с настройкой счёта в MONETA.RU'; +$_['help_fiscalization'] = 'Включайте, только если к счёту подключён кассовый сервис kassa.payanyway.ru'; +$_['help_receipt_email'] = 'Обязателен при фискализации. Подставляется в чек, только если у покупателя нет ни e-mail, ни телефона: без контакта касса чек не примет, поэтому он уйдёт на этот адрес (ФИО покупателя в чеке остаётся)'; +$_['help_canceled_order_status'] = 'Заказ в этих статусах не оплачивается: на проверочный запрос модуль отвечает отказом'; +$_['help_callback_url'] = 'Укажите этот адрес в настройках счёта MONETA.RU как Check URL и Pay URL. Он должен быть доступен из интернета'; + +// Button +$_['button_integrity_code_toggle'] = 'Показать или скрыть'; +$_['button_copy'] = 'Скопировать'; +$_['text_copied'] = 'Скопировано'; + +// Warning +$_['warning_ocmod_router'] = 'Модификация PayAnyWay для роутера не применена: уведомления Check URL и Pay URL методом POST не дойдут до модуля. Нажмите Расширения → Модификации → Обновить и проверьте лог модификаций. При методе GET в настройках счёта модификация не нужна.'; +$_['warning_receipt_email'] = 'Фискализация чеков включена, а «Email для чеков» не задан: покупателю без e-mail и телефона чек не будет пробит. Укажите адрес в настройках ниже или выключите фискализацию, если кассовый сервис не подключён.'; + // Error $_['error_permission'] = 'Недостаточно прав для изменения настроек способа оплаты PayAnyWay'; -$_['error_account'] = 'Номер счёта обязателен'; +$_['error_account'] = 'Номер счёта обязателен и состоит только из цифр'; $_['error_integrity_code'] = 'Код проверки целостности данных обязателен'; $_['error_order_status'] = 'Выберите статус заказа'; +$_['error_receipt_email'] = 'Укажите корректный e-mail не длиннее 64 символов'; diff --git a/upload/admin/view/template/extension/payment/payanyway.twig b/upload/admin/view/template/extension/payment/payanyway.twig index 1725f98..2228c13 100755 --- a/upload/admin/view/template/extension/payment/payanyway.twig +++ b/upload/admin/view/template/extension/payment/payanyway.twig @@ -16,6 +16,12 @@ + {% if warning_ocmod %} + {{ warning_ocmod }} + {% endif %} + {% if warning_receipt_email %} + {{ warning_receipt_email }} + {% endif %} {% if error_warning %} {{ error_warning }} × @@ -64,9 +70,15 @@ {{ entry_integrity_code_label }} - + + + + + + {% if error_integrity_code %} {{ error_integrity_code }} {% endif %} @@ -84,18 +96,87 @@ - {# НДС #} + {# Тестовый режим #} - {{ entry_vat_rate_label }} - - - {% for vat_id, label in payment_payanyway_vat_rates %} - - {{ label }} - - {% endfor %} - + + + + + {{ entry_test_mode_label }} + + {{ help_test_mode }} + + + + {# Фискализация чеков #} + + + + + + {{ entry_fiscalization_label }} + + {{ help_fiscalization }} + + + + {# Поля чека — только при фискализации. Скрываются, а не блокируются: заблокированное + поле браузер не отправляет, и значение потерялось бы. #} + + {# Email для чеков #} + + {{ entry_receipt_email_label }} + + + {{ help_receipt_email }} + {% if error_receipt_email %} + {{ error_receipt_email }} + {% endif %} + + + + {# НДС #} + + {{ entry_vat_rate_label }} + + + {% for vat_id, label in payment_payanyway_vat_rates %} + + {{ label }} + + {% endfor %} + + + + + {# Признак способа расчёта #} + + {{ entry_payment_method_label }} + + + {% for value, label in payment_payanyway_payment_methods %} + {{ label }} + {% endfor %} + + + + + {# Признак предмета расчёта #} + + {{ entry_payment_object_label }} + + + {% for value, label in payment_payanyway_payment_objects %} + {{ label }} + {% endfor %} + + @@ -192,6 +273,28 @@ {% endif %} + + {# canceled #} + + + {{ entry_canceled_order_status_label }} + + + + {% for order_status in order_statuses %} + + + + {{ order_status.name }} + + + {% endfor %} + + + @@ -218,6 +321,20 @@ + {# URL для уведомлений #} + + {{ entry_callback_url_label }} + + + + + {{ button_copy }} + + + {{ help_callback_url }} + + + {# sort-order #} @@ -234,4 +351,34 @@ + + {{ footer }} diff --git a/upload/catalog/controller/extension/payment/payanyway.php b/upload/catalog/controller/extension/payment/payanyway.php index 69145b6..da3f1c1 100755 --- a/upload/catalog/controller/extension/payment/payanyway.php +++ b/upload/catalog/controller/extension/payment/payanyway.php @@ -6,6 +6,10 @@ * @property ModelLocalisationOrderStatus $model_localisation_order_status */ class ControllerExtensionPaymentPayanyway extends Controller { + const MODULE_NAME = 'payanyway'; + const MODULE_VERSION = '1.0.4'; + const CMS_NAME = 'OpenCart'; + const PAYMENT_SERVER_PROD = 'prod'; const PAYMENT_URL_PROD = 'https://www.payanyway.ru/assistant.htm'; const PAYMENT_URL_DEMO = 'https://demo.moneta.ru/assistant.htm'; @@ -15,16 +19,28 @@ class ControllerExtensionPaymentPayanyway extends Controller { const JSON_CONTENT_TYPE = 'application/json'; const TEXT_CONTENT_TYPE = 'text/plain; charset=UTF-8'; - const TRANSACTION_ID_STRING_DELIMITER = '|'; + const TRANSACTION_ID_DELIMITER = '_'; + const LEGACY_TRANSACTION_ID_DELIMITER = '|'; + const TRANSACTION_ID_PATTERN = '/^([1-9][0-9]*)[%s]([0-9]{14})([A-Z0-9+-]{1,6})?$/'; const DESCRIPTION_MAX_LENGTH = 500; const CLIENT_NAME_MAX_LENGTH = 256; + const CLIENT_EMAIL_MAX_LENGTH = 64; + const CLIENT_PHONE_MAX_LENGTH = 19; + const CHECK_CLIENT_PHONE_MAX_LENGTH = 13; const ITEM_NAME_MAX_LENGTH = 128; const DEFAULT_PAYMENT_METHOD = 'full_payment'; const DEFAULT_PAYMENT_OBJECT = 'commodity'; - const SHIPPING_PAYMENT_OBJECT = 'service'; + const SERVICE_PAYMENT_OBJECT = 'service'; + const VOUCHER_PAYMENT_METHOD = 'advance'; + const VOUCHER_PAYMENT_OBJECT = 'payment'; + const PAYMENT_ITEM_NAME = 'Оплата заказа №'; const DEFAULT_MEASURE = 'unit'; + const DEFAULT_CANCELED_ORDER_STATUS_IDS = '7,9,16'; + + const JSON_NUMBER_PREFIX = '__payanyway_number:'; + /** @var int */ private $mntId; @@ -49,8 +65,26 @@ class ControllerExtensionPaymentPayanyway extends Controller { /** @var int */ private $errorOrderStatusId; + /** @var bool */ + private $testMode; + + /** @var bool */ + private $fiscalization; + + /** @var string «Email для чеков»: подставляется, если у покупателя нет ни email, ни телефона */ + private $receiptEmail; + /** @var string */ - private $cmsModuleVersion; + private $paymentMethod; + + /** @var string */ + private $paymentObject; + + /** @var int[] */ + private $canceledOrderStatusIds; + + /** @var string */ + private $cmsName; /** @var int|null */ private $orderId; @@ -64,7 +98,7 @@ class ControllerExtensionPaymentPayanyway extends Controller { $this->initializeModule(); - $this->orderId = $this->getOrderId(); + $this->orderId = $this->getOrderIdFromSession(); $this->orderInfo = $this->getOrderInfo(); } @@ -78,17 +112,34 @@ class ControllerExtensionPaymentPayanyway extends Controller { $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_AMOUNT'] = $this->getOrderAmount(); $data['MNT_CURRENCY_CODE'] = $this->currency; - $data['MNT_TEST_MODE'] = '0'; + $data['MNT_TEST_MODE'] = $this->testMode ? '1' : '0'; $data['MNT_DESCRIPTION'] = $this->getDescription(); $data['MNT_SUBSCRIBER_ID'] = $this->getSubscriberId(); - $signature = md5( + $data['MNT_SIGNATURE'] = $this->buildFormSignature($data); + $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->getCms(); + + $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); + } + + /** + * @param array $data + * @return string + */ + private function buildFormSignature(array $data) { + return md5( $data['MNT_ID'] . $data['MNT_TRANSACTION_ID'] . $data['MNT_AMOUNT'] . @@ -97,18 +148,6 @@ class ControllerExtensionPaymentPayanyway extends Controller { $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); } /** @@ -119,9 +158,18 @@ class ControllerExtensionPaymentPayanyway extends Controller { $json = array(); $this->load->language('extension/payment/payanyway'); - $this->addOrderHistory($this->pendingOrderStatusId, $this->language->get('text_order_confirmed')); - $json['success'] = true; + $orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null; + $paymentCode = isset($this->orderInfo['payment_code']) ? $this->orderInfo['payment_code'] : ''; + if (self::MODULE_NAME !== $paymentCode) { + $json['success'] = false; + } else { + if ((null !== $orderStatusId) && ($orderStatusId !== $this->successOrderStatusId)) { + $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)); @@ -148,19 +196,13 @@ class ControllerExtensionPaymentPayanyway extends Controller { */ 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; + $orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null; + if ((null !== $orderStatusId) && ($orderStatusId !== $this->successOrderStatusId)) { + $this->addOrderHistory($this->errorOrderStatusId, $this->language->get('text_payment_error')); } - return $this->getOrderIdFromSession(); + $this->response->redirect($this->url->link('checkout/failure', '', true)); } /** @@ -178,8 +220,7 @@ class ControllerExtensionPaymentPayanyway extends Controller { /** @return string */ private function createTransactionId() { - $date = date('YmdHis'); - return $this->orderId . self::TRANSACTION_ID_STRING_DELIMITER . $date; + return $this->orderId . self::TRANSACTION_ID_DELIMITER . date('YmdHisT'); } /** @@ -190,14 +231,37 @@ class ControllerExtensionPaymentPayanyway extends Controller { return number_format($price, 2, '.', ''); } + /** + * @return string + */ + private function getOrderAmount() { + return $this->formatPrice($this->toKopecks(isset($this->orderInfo['total']) ? $this->orderInfo['total'] : 0) / 100); + } + + /** + * @return string + */ + private function getCms() { + return sprintf('%s v%s_PHP %s_%s v%s', + $this->cmsName, + defined('VERSION') ? constant('VERSION') : 'unknown', + PHP_VERSION, + self::MODULE_NAME, + self::MODULE_VERSION + ); + } + /** @return string */ private function getDescription() { $orderId = isset($this->orderInfo['order_id']) ? $this->orderInfo['order_id'] : null; - $description = "Оплата заказа" . (!empty($orderId) ? " №{$orderId}" : ''); + $description = $this->language->get('text_description'); + if (!empty($orderId)) { + $description .= ' ' . sprintf($this->language->get('text_description_number'), $orderId); + } $clientName = $this->getClientName(); if (null !== $clientName) { - $description .= " от {$clientName}"; + $description .= ' ' . sprintf($this->language->get('text_description_customer'), $clientName); } $comment = isset($this->orderInfo['comment']) ? $this->orderInfo['comment'] : ''; @@ -231,7 +295,7 @@ class ControllerExtensionPaymentPayanyway extends Controller { */ 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 = preg_replace('/[^\p{L}\p{N}\s:.,()_№+-]/u', '', $decoded); $cleaned = str_replace(['&', '/', '\\', ';', '%', '#', '"', "'"], '', $cleaned); return trim(preg_replace('/\s+/', ' ', $cleaned)); @@ -265,14 +329,32 @@ class ControllerExtensionPaymentPayanyway extends Controller { * @throws \Exception */ public function callback() { - $requestData = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET; - $callback = $this->getCallbackData($requestData); + try { + $callback = $this->getCallbackData($this->getCallbackRequestData()); + } catch (\InvalidArgumentException $exception) { + $this->log->write('PayAnyWay callback: ' . $exception->getMessage()); + $this->sendResponse(self::FAIL_RESPONSE); + } $this->validateCallback($callback); - (isset($callback['MNT_COMMAND'])) && ('CHECK' === $callback['MNT_COMMAND']) - ? $this->handleCheckCallback($callback) - : $this->handlePayCallback($callback); + $isCheck = 'CHECK' === $callback['MNT_COMMAND']; + if ($isCheck && !$this->checkSignature($callback, $callback['MNT_SIGNATURE'])) { + $this->sendResponse(self::FAIL_RESPONSE); + } + + $this->orderId = $this->parseOrderIdFromTransactionId($callback['MNT_TRANSACTION_ID']); + if (null === $this->orderId) { + $this->log->write('PayAnyWay callback: MNT_TRANSACTION_ID has unexpected format'); + $this->sendResponse(self::FAIL_RESPONSE); + } + + $this->orderInfo = $this->getOrderInfo(); + if (empty($this->orderInfo)) { + $this->sendResponse($isCheck ? $this->buildUnknownOrderResponse($callback) : self::FAIL_RESPONSE); + } + + $isCheck ? $this->handleCheckCallback($callback) : $this->handlePayCallback($callback); } /** @@ -293,14 +375,28 @@ class ControllerExtensionPaymentPayanyway extends Controller { ); } - /** @return int|null */ - private function getOrderIdFromTransactionId() { - if (!isset($_REQUEST['MNT_TRANSACTION_ID'])) { + /** + * @return array + */ + private function getCallbackRequestData() { + return array_merge($_GET, $_POST); + } + + /** + * @param mixed $transactionId + * @return int|null + */ + private function parseOrderIdFromTransactionId($transactionId) { + if (!is_string($transactionId)) { return null; } - $transactionIdData = explode(self::TRANSACTION_ID_STRING_DELIMITER, $_REQUEST['MNT_TRANSACTION_ID'], 2); - return isset($transactionIdData[0]) ? (int)$transactionIdData[0] : null; + $delimiters = preg_quote(self::TRANSACTION_ID_DELIMITER . self::LEGACY_TRANSACTION_ID_DELIMITER, '/'); + if (!preg_match(sprintf(self::TRANSACTION_ID_PATTERN, $delimiters), $transactionId, $matches)) { + return null; + } + + return (int)$matches[1]; } /** @return int|null */ @@ -333,13 +429,6 @@ class ControllerExtensionPaymentPayanyway extends Controller { */ private function validateCallback(array $callback) { - if (null === $this->orderId) { - $this->sendResponse(self::FAIL_RESPONSE); - } - if (false === $this->orderInfo) { - $this->sendResponse(self::FAIL_RESPONSE); - } - if (empty($this->mntId) || empty($this->integrityCode)) { $this->sendResponse(self::FAIL_RESPONSE); } @@ -347,6 +436,11 @@ class ControllerExtensionPaymentPayanyway extends Controller { if ($this->mntId !== (int)$callback['MNT_ID']) { $this->sendResponse(self::FAIL_RESPONSE); } + + if ($callback['MNT_TEST_MODE'] !== ($this->testMode ? '1' : '0')) { + $this->log->write('PayAnyWay callback: MNT_TEST_MODE does not match module settings'); + $this->sendResponse(self::FAIL_RESPONSE); + } } /** @@ -381,20 +475,17 @@ class ControllerExtensionPaymentPayanyway extends Controller { * @throws \Exception */ private function handleCheckCallback(array $callbackData) { - if (!$this->checkSignature($callbackData, $callbackData['MNT_SIGNATURE'])) { - $this->sendResponse(self::FAIL_RESPONSE); - } - list($resultCode, $description) = $this->determineCheckResult($callbackData); $responseData = $this->buildBaseResponseData($callbackData, $resultCode, $description); - $products = $this->getAccountOrderProducts(); - $deliveryInfo = $this->getDeliveryInfo(); + if ($this->fiscalization) { + $responseData['client'] = $this->getClientInfo(true); + $responseData['items'] = $this->getCheckItems($this->getReceiptLines()); - $responseData['client'] = $this->getClientInfo(); - $responseData['items'] = $this->getCheckItems($products, $deliveryInfo); + $this->sendResponse($this->buildJsonResponse($responseData)); + } - $this->sendResponse($this->buildJsonResponse($responseData)); + $this->sendResponse($this->buildXmlResponse($responseData + $this->getResponseAttributes())); } /** @@ -403,13 +494,12 @@ class ControllerExtensionPaymentPayanyway extends Controller { * @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_AMOUNT' => $this->getOrderAmount(), 'MNT_CURRENCY_CODE' => $currencyCode, 'MNT_SUBSCRIBER_ID' => $callbackData['MNT_SUBSCRIBER_ID'], 'MNT_TEST_MODE' => $callbackData['MNT_TEST_MODE'], @@ -422,24 +512,43 @@ class ControllerExtensionPaymentPayanyway extends Controller { $isPayedOrder = $this->isOrderPaid(); if (!$isPayedOrder) { $this->load->language('extension/payment/payanyway'); - $this->addOrderHistory($this->successOrderStatusId, $this->language->get('text_payment_completed')); + $this->addOrderHistory($this->successOrderStatusId, $this->getPaymentCompletedComment($callbackData['MNT_OPERATION_ID'])); } $resultCode = 200; $description = $isPayedOrder ? 'Order already paid' : 'Order success paid'; $responseData = $this->buildBaseResponseData($callbackData, $resultCode, $description); - $products = $this->getAccountOrderProducts(); - $deliveryInfo = $this->getDeliveryInfo(); + $this->sendResponse($this->buildXmlResponse($responseData + $this->getResponseAttributes())); + } - $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; + /** + * @param string $operationId + * @return string + */ + private function getPaymentCompletedComment($operationId) { + $comment = $this->language->get('text_payment_completed'); + if (preg_match('/^[0-9]{1,20}$/', $operationId)) { + $comment .= '. ' . sprintf($this->language->get('text_payment_operation'), $operationId); + } - $this->sendResponse($this->buildXmlResponse($responseData)); + return $comment; + } + + /** + * @return array + * @throws \Exception + */ + private function getResponseAttributes() { + $inventory = $this->getInventoryJson($this->getReceiptLines()); + $client = $this->getClientInfo($this->fiscalization); + + return array( + 'inventory' => $inventory ?: null, + 'client' => $client ? json_encode(array($client), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null, + 'customer' => isset($client['email']) ? $client['email'] : null, + 'phone' => isset($client['phone']) ? ltrim($client['phone'], '+') : null, + ); } /** @@ -450,15 +559,14 @@ class ControllerExtensionPaymentPayanyway extends Controller { * @throws \Exception */ private function buildBaseResponseData(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' => $this->validateString($description, self::DESCRIPTION_MAX_LENGTH), - 'MNT_AMOUNT' => $this->formatPrice($orderTotal), + 'MNT_AMOUNT' => $this->getOrderAmount(), 'MNT_CURRENCY_CODE' => $callbackData['MNT_CURRENCY_CODE'], + 'MNT_CMS' => $this->getCms(), ); $signatureSource = $resultCode . $this->mntId . $callbackData['MNT_TRANSACTION_ID'] . $this->integrityCode; @@ -467,6 +575,18 @@ class ControllerExtensionPaymentPayanyway extends Controller { return $data; } + /** + * @param array $callbackData + * @return array + * @throws \Exception + */ + private function buildUnknownOrderResponse(array $callbackData) { + $responseData = $this->buildBaseResponseData($callbackData, 500, 'Order not found'); + unset($responseData['MNT_AMOUNT']); + + return $this->buildXmlResponse($responseData, false); + } + /** * @param array $callbackData * @return array @@ -487,7 +607,7 @@ class ControllerExtensionPaymentPayanyway extends Controller { return array(200, 'Order Paid'); } - if (in_array($orderStatusId, array(7, 9, 16), true)) { + if (in_array($orderStatusId, $this->canceledOrderStatusIds, true)) { return array(500, "Order status is {$orderStatus}"); } @@ -504,45 +624,251 @@ class ControllerExtensionPaymentPayanyway extends Controller { } /** - * @return array + * @return array цены в копейках * @throws \Exception */ - private function getDeliveryInfo() { - $this->load->model('account/order'); - foreach ($this->model_account_order->getOrderTotals($this->orderId) as $orderTotal) { - if (isset($orderTotal['code']) && ($orderTotal['code'] === 'shipping')) { - return $orderTotal; - } + private function getReceiptLines() { + $target = $this->toKopecks(isset($this->orderInfo['total']) ? $this->orderInfo['total'] : 0); + if ($target <= 0) { + return array(); } - return null; + $lines = $this->balanceReceiptLines($this->collectReceiptLines(), $target); + if (!empty($lines)) { + return $lines; + } + + return array( + array( + 'name' => self::PAYMENT_ITEM_NAME . $this->orderId, + 'unit' => $target, + 'quantity' => 1, + 'pm' => $this->paymentMethod, + 'po' => $this->paymentObject, + 'goods' => true + ), + ); } /** - * @param array $products - * @param array $deliveryInfo + * @return array + * @throws \Exception + */ + private function collectReceiptLines() { + $this->load->model('account/order'); + + $lines = array(); + foreach ($this->model_account_order->getOrderProducts($this->orderId) as $product) { + $unit = $this->toKopecks((float)$product['price'] + (float)$product['tax']); + $quantity = (int)$product['quantity']; + if (($unit > 0) && ($quantity > 0)) { + $lines[] = array( + 'name' => $product['name'], + 'unit' => $unit, + 'quantity' => $quantity, + 'pm' => $this->paymentMethod, + 'po' => $this->paymentObject, + 'goods' => true + ); + } + } + + foreach ($this->model_account_order->getOrderVouchers($this->orderId) as $voucher) { + $unit = $this->toKopecks($voucher['amount']); + if ($unit > 0) { + $lines[] = array( + 'name' => $voucher['description'], + 'unit' => $unit, + 'quantity' => 1, + 'pm' => self::VOUCHER_PAYMENT_METHOD, + 'po' => self::VOUCHER_PAYMENT_OBJECT, + 'goods' => false + ); + } + } + + foreach ($this->model_account_order->getOrderTotals($this->orderId) as $orderTotal) { + if (in_array($orderTotal['code'], array('sub_total', 'tax', 'total'), true)) { + continue; + } + + $unit = $this->toKopecks($orderTotal['value']); + if ($unit > 0) { + $lines[] = array( + 'name' => $orderTotal['title'], + 'unit' => $unit, + 'quantity' => 1, + 'pm' => $this->paymentMethod, + 'po' => self::SERVICE_PAYMENT_OBJECT, + 'goods' => false + ); + } + } + + return $lines; + } + + /** + * @param array $lines + * @param int $target сумма заказа в копейках + * @return array + */ + private function balanceReceiptLines(array $lines, $target) { + if (empty($lines)) { + return array(); + } + + $totals = array(); + foreach ($lines as $index => $line) { + $totals[$index] = $line['unit'] * $line['quantity']; + } + if (array_sum($totals) === $target) { + return $lines; + } + + foreach (array(true, false) as $goodsOnly) { + $adjustable = array(); + $fixed = 0; + foreach ($lines as $index => $line) { + if (!$goodsOnly || $line['goods']) { + $adjustable[] = $index; + } else { + $fixed += $totals[$index]; + } + } + if (empty($adjustable) || ($target - $fixed <= 0)) { + continue; + } + + return $this->distributeReceiptLines($lines, $totals, $adjustable, $target - $fixed); + } + + return array(); + } + + /** + * Разнос суммы $goal по строкам $adjustable пропорционально их сумме, методом наибольших + * остатков: доли округляются вниз, недостающие копейки — строкам с большим остатком. + * Целочисленное деление без intdiv() — модуль работает на PHP 5.6. + * + * @param array $lines + * @param array $totals суммы строк в копейках + * @param array $adjustable индексы строк, которые можно менять + * @param int $goal + * @return array + */ + private function distributeReceiptLines(array $lines, array $totals, array $adjustable, $goal) { + $base = 0; + foreach ($adjustable as $index) { + $base += $totals[$index]; + } + if ($base <= 0) { + return array(); + } + + $shares = array(); + $remainders = array(); + foreach ($adjustable as $index) { + if ($totals[$index] > (int)(PHP_INT_MAX / $goal)) { + return array(); + } + + $product = $totals[$index] * $goal; + $remainders[$index] = $product % $base; + $shares[$index] = ($product - $remainders[$index]) / $base; + } + arsort($remainders); + $left = $goal - array_sum($shares); + foreach (array_keys($remainders) as $index) { + if ($left <= 0) { + break; + } + $shares[$index]++; + $left--; + } + + $balanced = array(); + foreach ($lines as $index => $line) { + if (!isset($shares[$index])) { + $balanced[] = $line; + continue; + } + + $parts = $this->splitReceiptLine($line, $shares[$index]); + if (empty($parts)) { + return array(); + } + foreach ($parts as $part) { + $balanced[] = $part; + } + } + + return $balanced; + } + + /** + * Строка с новой суммой: целая цена за штуку, остаток копеек — второй строкой той же + * позиции с ценой на копейку выше. Нулевые части отбрасываются. + * + * @param array{name: string, unit: int, quantity: int, pm: string, po: string, goods: bool} $line + * @param int $total + * @return array + */ + private function splitReceiptLine(array $line, $total) { + $pieces = $line['quantity']; + $rest = $total % $pieces; + $unit = ($total - $rest) / $pieces; + + $parts = array(); + if ($rest > 0) { + $parts[] = array('unit' => $unit + 1, 'quantity' => $rest) + $line; + } + if (($unit > 0) && ($pieces - $rest > 0)) { + $parts[] = array('unit' => $unit, 'quantity' => $pieces - $rest) + $line; + } + + return $parts; + } + + /** + * @param float|string $amount + * @return int + */ + private function toKopecks($amount) { + $currencyValue = isset($this->orderInfo['currency_value']) ? (float)$this->orderInfo['currency_value'] : 0.0; + if ($currencyValue <= 0) { + $currencyValue = 1.0; + } + + return (int)round((float)$amount * $currencyValue * 100); + } + + /** + * @param array $lines * @return false|string */ - private function getPayInventoryJson(array $products, array $deliveryInfo) { - if (empty($products) && empty($deliveryInfo)) { + private function getInventoryJson(array $lines) { + if (empty($lines)) { 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::ITEM_NAME_MAX_LENGTH), - 'price' => $this->formatPrice($price), - 'quantity' => $this->formatQuantity($quantity), - 'vatTag' => $this->vatRate, - 'pm' => self::DEFAULT_PAYMENT_METHOD, - 'po' => self::DEFAULT_PAYMENT_OBJECT, + foreach ($lines as $line) { + $position = array( + 'name' => $this->validateString($line['name'], self::ITEM_NAME_MAX_LENGTH), + 'price' => $this->formatPrice($line['unit'] / 100), + 'quantity' => $this->formatQuantity($line['quantity']), 'measure' => self::DEFAULT_MEASURE, ); + + if ($this->fiscalization) { + $position += array( + 'vatTag' => $this->vatRate, + 'pm' => $line['pm'], + 'po' => $line['po'], + ); + } + $inventory[] = $position; } $name = isset($deliveryInfo['title']) ? $deliveryInfo['title'] : ''; @@ -560,18 +886,36 @@ class ControllerExtensionPaymentPayanyway extends Controller { return json_encode($inventory, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } - /** @return array */ - private function getClientInfo() { + /** + * Покупатель в чеке: хотя бы один из email/phone, ФИО — только вместе с контактом. + * Нет ни email, ни телефона — «Email для чеков» (если разрешено и задан), иначе пусто. + * + * @param bool $withReceiptEmail + * @return array + */ + private function getClientInfo($withReceiptEmail) { + $email = $this->getClientEmail(); + $phoneNumber = $this->getClientPhone(); + if ( + (null === $email) + && (null === $phoneNumber) + && $withReceiptEmail + && ('' !== $this->receiptEmail) + ) { + $email = $this->receiptEmail; + } + if ((null === $email) && (null === $phoneNumber)) { + return array(); + } + $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; } @@ -580,40 +924,23 @@ class ControllerExtensionPaymentPayanyway extends Controller { } /** - * @param array $products - * @param array $deliveryInfo - * @return non-empty-array + * @param array $lines + * @return array */ - private function getCheckItems(array $products, array $deliveryInfo) { + private function getCheckItems(array $lines) { $checkItems = 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; - + foreach ($lines as $line) { $checkItems[] = array( - 'name' => $this->validateString($name, self::ITEM_NAME_MAX_LENGTH), - 'price' => $this->formatPrice($price), - 'quantity' => $quantity, + 'name' => $this->validateString($line['name'], self::ITEM_NAME_MAX_LENGTH), + 'price' => $this->jsonNumber($line['unit']), + 'quantity' => $line['quantity'], 'vat' => $this->getVat($this->vatRate), 'measure' => self::DEFAULT_MEASURE, - 'paymentMethod' => self::DEFAULT_PAYMENT_METHOD, - 'paymentObject' => self::DEFAULT_PAYMENT_OBJECT, + 'paymentMethod' => $line['pm'], + 'paymentObject' => $line['po'], ); } - $name = isset($deliveryInfo['title']) ? $deliveryInfo['title'] : ''; - $price = isset($deliveryInfo['value']) ? (float)$deliveryInfo['value'] : 0; - $checkItems[] = array( - 'name' => $this->validateString($name, self::ITEM_NAME_MAX_LENGTH), - 'price' => $this->formatPrice($price), - 'quantity' => 1, - 'vat' => $this->getVat($this->vatRate), - 'measure' => self::DEFAULT_MEASURE, - 'paymentMethod' => self::DEFAULT_PAYMENT_METHOD, - 'paymentObject' => self::SHIPPING_PAYMENT_OBJECT, - ); - return $checkItems; } @@ -627,12 +954,12 @@ class ControllerExtensionPaymentPayanyway extends Controller { '1108' => 'vat5', '1109' => 'vat7', '1103' => 'vat10', - '7000' => 'vat20', + '1102' => 'vat20', '1113' => 'vat22', '1110' => 'vat105', '1111' => 'vat107', '1107' => 'vat110', - '7001' => 'vat120', + '1106' => 'vat120', '1114' => 'vat122', ); @@ -649,16 +976,14 @@ class ControllerExtensionPaymentPayanyway extends Controller { /**@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['name']; } } - return $orderStatus; + return 'unknown'; } /** @@ -676,21 +1001,33 @@ class ControllerExtensionPaymentPayanyway extends Controller { * @throws \JsonException */ public function buildJsonResponse(array $data) { + $client = $data['client']; + if (isset($client['phone']) && (strlen($client['phone']) > self::CHECK_CLIENT_PHONE_MAX_LENGTH)) { + unset($client['phone']); + } + if (!isset($client['email']) && !isset($client['phone'])) { + $client = array(); + } + $jsonData = array( - 'id' => $data['MNT_ID'], + 'id' => (string)$data['MNT_ID'], 'transactionId' => $data['MNT_TRANSACTION_ID'], - 'amount' => $data['MNT_AMOUNT'], + 'amount' => self::JSON_NUMBER_PREFIX . $data['MNT_AMOUNT'], 'signature' => $data['MNT_SIGNATURE'], - 'resultCode' => $data['MNT_RESULT_CODE'], + 'resultCode' => (string)$data['MNT_RESULT_CODE'], 'description' => $data['MNT_DESCRIPTION'], 'externalId' => $data['MNT_TRANSACTION_ID'], + 'cms' => $data['MNT_CMS'], 'receipt' => array( - 'client' => $data['client'], + 'client' => (object)$client, 'items' => $data['items'], ), ); $json = json_encode($jsonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if (false !== $json) { + $json = preg_replace('/"' . preg_quote(self::JSON_NUMBER_PREFIX, '/') . '([0-9]+\.[0-9]{2})"/', '$1', $json); + } return array( 'data' => $json ?: '', @@ -698,12 +1035,21 @@ class ControllerExtensionPaymentPayanyway extends Controller { ); } + /** + * @param int $kopecks + * @return string + */ + private function jsonNumber($kopecks) { + return self::JSON_NUMBER_PREFIX . $this->formatPrice($kopecks / 100); + } + /** * @param array $data + * @param bool $withAttributes * @return array * @throws \Exception */ - private function buildXmlResponse(array $data) { + private function buildXmlResponse(array $data, $withAttributes = true) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = false; @@ -718,6 +1064,7 @@ class ControllerExtensionPaymentPayanyway extends Controller { 'MNT_AMOUNT', 'MNT_CURRENCY_CODE', 'MNT_SIGNATURE', + 'MNT_CMS', ); foreach ($requiredFields as $field) { @@ -726,14 +1073,18 @@ class ControllerExtensionPaymentPayanyway extends Controller { } } + if (!$withAttributes) { + return $this->saveXmlResponse($dom); + } + $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, + 'CUSTOMER' => isset($data['customer']) ? $data['customer'] : null, + 'PHONE' => isset($data['phone']) ? $data['phone'] : null, ); foreach ($attributeMap as $key => $value) { @@ -745,6 +1096,14 @@ class ControllerExtensionPaymentPayanyway extends Controller { } } + return $this->saveXmlResponse($dom); + } + + /** + * @param \DOMDocument $dom + * @return array + */ + private function saveXmlResponse(\DOMDocument $dom) { $xml = $dom->saveXML(); return array( @@ -772,9 +1131,8 @@ class ControllerExtensionPaymentPayanyway extends Controller { 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 $this->normalizeEmail($email); } /** @@ -785,7 +1143,24 @@ class ControllerExtensionPaymentPayanyway extends Controller { $telephone = isset($this->orderInfo['telephone']) ? $this->orderInfo['telephone'] : ''; $digits = preg_replace('/\D/', '', trim((string)$telephone)); - return ('' !== $digits) ? '+' . $digits : null; + return (('' !== $digits) && (strlen($digits) + 1 <= self::CLIENT_PHONE_MAX_LENGTH)) ? '+' . $digits : null; + } + + /** + * @param mixed $email + * @return string|null + */ + private function normalizeEmail($email) { + $email = trim((string)$email); + if ( + ('' === $email) + || (strlen($email) > self::CLIENT_EMAIL_MAX_LENGTH) + || (false === filter_var($email, FILTER_VALIDATE_EMAIL)) + ) { + return null; + } + + return $email; } /** @@ -851,7 +1226,42 @@ class ControllerExtensionPaymentPayanyway extends Controller { $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'); + $this->testMode = (bool)$this->config->get('payment_payanyway_test_mode'); + + $fiscalization = $this->config->get('payment_payanyway_fiscalization'); + $this->fiscalization = (null === $fiscalization) ? true : (bool)$fiscalization; + $receiptEmail = $this->normalizeEmail($this->config->get('payment_payanyway_receipt_email')); + $this->receiptEmail = (null !== $receiptEmail) ? $receiptEmail : ''; + + $paymentMethod = (string)$this->config->get('payment_payanyway_payment_method'); + $this->paymentMethod = ('' !== $paymentMethod) ? $paymentMethod : self::DEFAULT_PAYMENT_METHOD; + $paymentObject = (string)$this->config->get('payment_payanyway_payment_object'); + $this->paymentObject = ('' !== $paymentObject) ? $paymentObject : self::DEFAULT_PAYMENT_OBJECT; + + $canceled = $this->config->get('payment_payanyway_canceled_order_status_ids'); + if (null === $canceled) { + $canceled = explode(',', self::DEFAULT_CANCELED_ORDER_STATUS_IDS); + } + $this->canceledOrderStatusIds = array_map('intval', (array)$canceled); + + $this->cmsName = $this->detectCmsName(); + } + + /** + * @return string + */ + private function detectCmsName() { + $cmsName = (string)$this->config->get('payment_payanyway_cms_name'); + if ('' !== $cmsName) { + return $cmsName; + } + + $legacy = (string)$this->config->get('payment_payanyway_cms_module_version'); + if (preg_match('/^(.+?) v/', $legacy, $matches)) { + return $matches[1]; + } + + return self::CMS_NAME; } /** diff --git a/upload/catalog/language/en-gb/extension/payment/payanyway.php b/upload/catalog/language/en-gb/extension/payment/payanyway.php index beede39..3002f67 100755 --- a/upload/catalog/language/en-gb/extension/payment/payanyway.php +++ b/upload/catalog/language/en-gb/extension/payment/payanyway.php @@ -12,4 +12,9 @@ $_['text_paw_back_to_card'] = 'Back to payment by card'; $_['text_order_confirmed'] = 'Order confirmed'; $_['text_payment_processing'] = 'Payment processing'; $_['text_payment_completed'] = 'Order is paid'; -$_['text_payment_error'] = 'Payment error'; +$_['text_payment_operation'] = 'Operation number: %s'; + +$_['text_description'] = 'Payment for order'; +$_['text_description_number'] = 'No. %s'; +$_['text_description_customer'] = 'from %s'; +$_['text_payment_error'] = 'Payment error'; diff --git a/upload/catalog/language/ru-ru/extension/payment/payanyway.php b/upload/catalog/language/ru-ru/extension/payment/payanyway.php index 2b2fdd5..c09b2f0 100755 --- a/upload/catalog/language/ru-ru/extension/payment/payanyway.php +++ b/upload/catalog/language/ru-ru/extension/payment/payanyway.php @@ -12,4 +12,9 @@ $_['text_paw_back_to_card'] = 'Вернуться к оплате картой $_['text_order_confirmed'] = 'Заказ подтвержден'; $_['text_payment_processing'] = 'Обработка платежа'; $_['text_payment_completed'] = 'Заказ оплачен'; -$_['text_payment_error'] = 'Ошибка оплаты'; +$_['text_payment_operation'] = 'Номер операции: %s'; + +$_['text_description'] = 'Оплата заказа'; +$_['text_description_number'] = '№%s'; +$_['text_description_customer'] = 'от %s'; +$_['text_payment_error'] = 'Ошибка оплаты';
payanyway
{{ help_test_mode }}
{{ help_fiscalization }}
{{ help_receipt_email }}
{{ help_callback_url }}