Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bce8e8ded | ||
|
|
d850c2ca31 |
@@ -1,16 +1,16 @@
|
|||||||
# 🌟 Платёжный модуль PayAnyWay для [OpenCart](https://www.opencart.com/) v3.x
|
# 🌟 Платёжный модуль PayAnyWay (МОНЕТА) для [OpenCart](https://www.opencart.com/) v3.x
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
> **📦 Версия модуля: 1.0.5**
|
> **📦 Версия модуля: 1.0.6**
|
||||||
|
|
||||||
> **ℹ️ Совместимость версий OpenCart:**
|
> **ℹ️ Совместимость версий 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
|
> 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, 3.0.5.1
|
||||||
|
|
||||||
> **ℹ️ Совместимость версий OpenCart "Русская сборка":**
|
> **ℹ️ Совместимость версий OpenCart "Русская сборка":**
|
||||||
> 3.0.0.1, 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.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
|
> 3.0.0.1, 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.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, 3.0.5.1
|
||||||
|
|
||||||
> **ℹ️ Совместимость версий ocStore:**
|
> **ℹ️ Совместимость версий ocStore:**
|
||||||
> 3.0.2.0, 3.0.3.7, 3.0.3.9
|
> 3.0.2.0, 3.0.3.7, 3.0.3.9
|
||||||
@@ -48,30 +48,32 @@
|
|||||||
- **«Редактировать»** — для перехода к настройкам
|
- **«Редактировать»** — для перехода к настройкам
|
||||||
4. ✏️ Заполните соответствующие поля в настройках способа оплаты:
|
4. ✏️ Заполните соответствующие поля в настройках способа оплаты:
|
||||||
|
|
||||||
| Поле | Описание |
|
| Поле | Описание |
|
||||||
|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| **Включить модуль оплаты** | Включите или выключите модуль оплаты |
|
| **Включить модуль оплаты** | Включите или выключите модуль оплаты |
|
||||||
| **Номер счёта** | Ваш номер расширенного счёта в системе [MONETA.RU](https://moneta.ru/), только цифры |
|
| **Номер счёта** | Ваш номер расширенного счёта в системе [MONETA.RU](https://moneta.ru/), только цифры |
|
||||||
| **Код проверки целостности данных** | Код проверки целостности данных, указанный в настройках расширенного счёта в системе [MONETA.RU](https://moneta.ru/). Сохраняется ровно так, как введён, в том числе с символами `&`, `"`, `<`, `>` |
|
| **Код проверки целостности данных** | Код проверки целостности данных, указанный в настройках расширенного счёта в системе [MONETA.RU](https://moneta.ru/) |
|
||||||
| **Сервер платежей** | `Боевой` / `Демо` (демо-площадка MONETA.RU) |
|
| **Сервер платежей** | `Боевой` / `Демо` (демо-площадка MONETA.RU) |
|
||||||
| **Тестовый режим** | Имитация оплаты без реального списания средств. Должен совпадать с настройкой счёта в MONETA.RU: уведомление в другом режиме модуль отклоняет |
|
| **Тестовый режим** | Имитация оплаты без реального списания средств. Должен совпадать с настройкой счёта в MONETA.RU |
|
||||||
| **Фискализация чеков** | Включайте, только если к счёту подключён кассовый сервис [kassa.payanyway.ru](https://kassa.payanyway.ru/). Включена — на проверочный запрос модуль отвечает чеком для кассы (без кассы MONETA.RU вернёт ошибку `-700`). Выключена — в операцию уходят только позиции заказа и контакты покупателя. По умолчанию включена |
|
| **Фискализация чеков** | Включайте, только если к счёту подключён кассовый сервис [kassa.payanyway.ru](https://kassa.payanyway.ru/) |
|
||||||
| **Email для чеков** | Обязателен при фискализации. Подставляется в чек, только если у покупателя нет ни e-mail, ни телефона (ФИО покупателя в чеке остаётся) |
|
| **Email для чеков** | Обязателен при фискализации. Подставляется в чек, только если у покупателя нет ни e-mail, ни телефона (ФИО покупателя в чеке остаётся) |
|
||||||
| **НДС** | Ставка НДС всех позиций чека |
|
| **НДС** | Ставка НДС всех позиций чека |
|
||||||
| **Признак способа расчёта** | Для товаров, доставки и сборов. Обычно `Полный расчёт`. Проданные подарочные сертификаты всегда идут авансом |
|
| **Признак способа расчёта** | Для товаров, доставки и сборов. Обычно `Полный расчёт`. Проданные подарочные сертификаты всегда идут авансом |
|
||||||
| **Признак предмета расчёта** | Для товаров. Доставка и сборы всегда идут услугой, подарочные сертификаты — платежом |
|
| **Признак предмета расчёта** | Для товаров. Доставка и сборы всегда идут услугой, подарочные сертификаты — платежом |
|
||||||
| **Валюта платежа** | `RUB` |
|
| **Валюта платежа** | `RUB` |
|
||||||
| **Статусы заказа** | Выберите соответствующие статусы заказа из списка |
|
| **Статусы заказа** | Выберите соответствующие статусы заказа из списка |
|
||||||
| **Обрабатываемый** | Укажите статус заказа, который выставляется при его создании |
|
| **Обрабатываемый** | Укажите статус заказа, который выставляется при его создании |
|
||||||
| **Успешный** | Укажите статус заказа, который выставляется при успешной оплате |
|
| **Успешный** | Укажите статус заказа, который выставляется при успешной оплате |
|
||||||
| **Неуспешный** | Укажите статус заказа, который выставляется при ошибке оплаты |
|
| **Неуспешный** | Укажите статус заказа, который выставляется при ошибке оплаты |
|
||||||
| **Отменённые** | Заказ в этих статусах не оплачивается: на проверочный запрос модуль отвечает отказом. По умолчанию `Canceled`, `Canceled Reversal`, `Voided` |
|
| **Отменённые** | Заказ в этих статусах не оплачивается: на проверочный запрос и уведомление об оплате модуль отвечает отказом (деньги за такую оплату возвращаются в личном кабинете MONETA.RU, номер операции — в журнале ошибок). Статусы Обрабатываемого, Успешного и Неуспешного заказа выбрать нельзя. По умолчанию `Canceled`, `Canceled Reversal`, `Voided` |
|
||||||
| **URL для уведомлений** | Адрес Check URL и Pay URL для настроек счёта MONETA.RU, кнопка «Скопировать» |
|
| **URL для уведомлений** | Адрес Check URL и Pay URL для настроек счёта MONETA.RU, кнопка «Скопировать» |
|
||||||
| **Регион отображения** | Географическая зона, в которой будет отображаться способ оплаты |
|
| **Регион отображения** | Географическая зона, в которой будет отображаться способ оплаты |
|
||||||
| **Порядок сортировки** | Определяет порядок отображения платежного метода в списке методов оплаты при оформлении заказа. Чем меньше число, тем выше позиция в списке |
|
| **Порядок сортировки** | Определяет порядок отображения платежного метода в списке методов оплаты при оформлении заказа. Чем меньше число, тем выше позиция в списке |
|
||||||
|
|
||||||
Сумма позиций чека всегда равна сумме оплаты: налоги OpenCart входят в цену товаров, скидки (купоны, подарочные сертификаты, бонусные баллы) распределяются по товарам, доставка и сборы идут отдельными позициями. В истории заказа при оплате сохраняется номер операции MONETA.RU: «Заказ оплачен. Номер операции: N».
|
Сумма позиций чека всегда равна сумме оплаты: налоги OpenCart входят в цену товаров, скидки (купоны, подарочные сертификаты, бонусные баллы) распределяются по товарам, доставка и сборы идут отдельными позициями. В истории заказа при оплате сохраняется номер операции MONETA.RU: «Заказ оплачен. Номер операции: N».
|
||||||
|
|
||||||
|
> ℹ️ Сумма платёжной формы записывается в номер транзакции (`MNT_TRANSACTION_ID`, например `42_1500000_20261022120000UTC`; время — UTC). Если после показа формы сумма заказа изменилась, оплата по старой форме отклоняется: покупателю нужно заново открыть оплату заказа.
|
||||||
|
|
||||||
> ⚠️ **После обновления модуля** нажмите **«Модули / Расширения» → «Модификации» → «Обновить»**, затем откройте настройки PayAnyWay, проверьте новые поля (фискализация после обновления включена) и нажмите **«Сохранить»**. Пока при включённой фискализации не задан «Email для чеков», над формой настроек показывается предупреждение.
|
> ⚠️ **После обновления модуля** нажмите **«Модули / Расширения» → «Модификации» → «Обновить»**, затем откройте настройки PayAnyWay, проверьте новые поля (фискализация после обновления включена) и нажмите **«Сохранить»**. Пока при включённой фискализации не задан «Email для чеков», над формой настроек показывается предупреждение.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<modification>
|
<modification>
|
||||||
<name>PayAnyWay</name>
|
<name>PayAnyWay (МОНЕТА)</name>
|
||||||
<code>payanyway</code>
|
<code>payanyway</code>
|
||||||
<version>1.0.5</version>
|
<version>1.0.6</version>
|
||||||
<author>PayAnyWay</author>
|
<author>PayAnyWay (МОНЕТА)</author>
|
||||||
<link>https://payanyway.ru/</link>
|
<link>https://payanyway.ru/</link>
|
||||||
<!--
|
<!--
|
||||||
При методе POST MONETA.RU переносит query string Check/Pay URL в тело запроса:
|
При методе POST MONETA.RU переносит query string Check/Pay URL в тело запроса:
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
class ControllerExtensionPaymentPayanyway extends Controller {
|
class ControllerExtensionPaymentPayanyway extends Controller {
|
||||||
const CMS_NAME = 'OpenCart';
|
const CMS_NAME = 'OpenCart';
|
||||||
const MODULE_NAME = 'payanyway';
|
const MODULE_VERSION = '1.0.6';
|
||||||
const MODULE_VERSION = '1.0.5';
|
|
||||||
const MODULE_DEFAULT_SORT_ORDER = 0;
|
const MODULE_DEFAULT_SORT_ORDER = 0;
|
||||||
|
|
||||||
/** Маркер вставки OCMOD из install.xml в роутере витрины (приём Check/Pay URL методом POST). */
|
/** Маркер вставки OCMOD из install.xml в роутере витрины (приём Check/Pay URL методом POST). */
|
||||||
@@ -30,7 +29,6 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
'vat122' => '1114'
|
'vat122' => '1114'
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Признак способа расчёта (kassaspecification.pdf, pm). */
|
|
||||||
private $paymentMethods = array(
|
private $paymentMethods = array(
|
||||||
'full_prepayment',
|
'full_prepayment',
|
||||||
'prepayment',
|
'prepayment',
|
||||||
@@ -41,7 +39,6 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
'credit_payment'
|
'credit_payment'
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Признак предмета расчёта (kassaspecification.pdf, po). */
|
|
||||||
private $paymentObjects = array(
|
private $paymentObjects = array(
|
||||||
'commodity',
|
'commodity',
|
||||||
'excise',
|
'excise',
|
||||||
@@ -108,7 +105,9 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
|
|
||||||
$data['payment_payanyway_status'] = $this->retrieveValue('payment_payanyway_status');
|
$data['payment_payanyway_status'] = $this->retrieveValue('payment_payanyway_status');
|
||||||
$data['payment_payanyway_account'] = $this->retrieveValue('payment_payanyway_account');
|
$data['payment_payanyway_account'] = $this->retrieveValue('payment_payanyway_account');
|
||||||
$data['payment_payanyway_integrity_code'] = $this->getIntegrityCode('payment_payanyway_integrity_code');
|
$data['integrity_code_placeholder'] = $this->hasIntegrityCode()
|
||||||
|
? $this->language->get('text_integrity_code_saved')
|
||||||
|
: $this->language->get('entry_integrity_code_description');
|
||||||
$data['payment_payanyway_server'] = $this->retrieveValue('payment_payanyway_server');
|
$data['payment_payanyway_server'] = $this->retrieveValue('payment_payanyway_server');
|
||||||
$data['payment_payanyway_test_mode'] = (string)$this->retrieveValue('payment_payanyway_test_mode');
|
$data['payment_payanyway_test_mode'] = (string)$this->retrieveValue('payment_payanyway_test_mode');
|
||||||
$data['payment_payanyway_fiscalization'] = $this->isFiscalizationEnabled() ? '1' : '0';
|
$data['payment_payanyway_fiscalization'] = $this->isFiscalizationEnabled() ? '1' : '0';
|
||||||
@@ -182,7 +181,8 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
'payment_payanyway_integrity_code' => '',
|
'payment_payanyway_integrity_code' => '',
|
||||||
'payment_payanyway_server' => 'prod',
|
'payment_payanyway_server' => 'prod',
|
||||||
'payment_payanyway_test_mode' => 0,
|
'payment_payanyway_test_mode' => 0,
|
||||||
'payment_payanyway_fiscalization' => 1,
|
'payment_payanyway_fiscalization' => 0,
|
||||||
|
'payment_payanyway_vat_rate' => $this->availableVatRates['vat_none'],
|
||||||
'payment_payanyway_receipt_email' => '',
|
'payment_payanyway_receipt_email' => '',
|
||||||
'payment_payanyway_payment_method' => self::DEFAULT_PAYMENT_METHOD,
|
'payment_payanyway_payment_method' => self::DEFAULT_PAYMENT_METHOD,
|
||||||
'payment_payanyway_payment_object' => self::DEFAULT_PAYMENT_OBJECT,
|
'payment_payanyway_payment_object' => self::DEFAULT_PAYMENT_OBJECT,
|
||||||
@@ -246,9 +246,10 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
$this->error['account'] = $this->language->get('error_account');
|
$this->error['account'] = $this->language->get('error_account');
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenCart экранирует весь POST (htmlspecialchars в system/library/request.php): код с & " < >
|
|
||||||
// сохранился бы экранированным, и подписи не сошлись бы с MONETA.RU.
|
|
||||||
$integrityCode = $this->decodePostedString($request, 'payment_payanyway_integrity_code');
|
$integrityCode = $this->decodePostedString($request, 'payment_payanyway_integrity_code');
|
||||||
|
if (('' === $integrityCode) && $this->hasIntegrityCode()) {
|
||||||
|
$integrityCode = (string)$this->config->get('payment_payanyway_integrity_code');
|
||||||
|
}
|
||||||
if ('' === $integrityCode) {
|
if ('' === $integrityCode) {
|
||||||
$this->error['integrity_code'] = $this->language->get('error_integrity_code');
|
$this->error['integrity_code'] = $this->language->get('error_integrity_code');
|
||||||
}
|
}
|
||||||
@@ -278,7 +279,9 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
$canceled = isset($request->post['payment_payanyway_canceled_order_status_ids'])
|
$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();
|
: array();
|
||||||
$request->post['payment_payanyway_canceled_order_status_ids'] = array_values(array_unique(array_map('intval', $canceled)));
|
$request->post['payment_payanyway_canceled_order_status_ids'] = array_values(array_unique(array_filter(array_map('intval', $canceled), function ($statusId) {
|
||||||
|
return $statusId > 0;
|
||||||
|
})));
|
||||||
|
|
||||||
if (empty($request->post['payment_payanyway_pending_order_status_id'])) {
|
if (empty($request->post['payment_payanyway_pending_order_status_id'])) {
|
||||||
$this->error['pending_order_status'] = $this->language->get('error_order_status');
|
$this->error['pending_order_status'] = $this->language->get('error_order_status');
|
||||||
@@ -290,6 +293,19 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
$this->error['error_order_status'] = $this->language->get('error_order_status');
|
$this->error['error_order_status'] = $this->language->get('error_order_status');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Статус ожидания или ошибки среди отменённых: Check и Pay URL ответили бы на настоящую оплату
|
||||||
|
// 500, и MONETA.RU прекратила бы уведомления — деньги списаны, заказ не оплачен.
|
||||||
|
$moduleStatuses = array();
|
||||||
|
foreach (array('pending', 'success', 'error') as $status) {
|
||||||
|
$key = 'payment_payanyway_' . $status . '_order_status_id';
|
||||||
|
if (!empty($request->post[$key])) {
|
||||||
|
$moduleStatuses[] = (int)$request->post[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (array_intersect($request->post['payment_payanyway_canceled_order_status_ids'], $moduleStatuses)) {
|
||||||
|
$this->error['canceled_order_status'] = $this->language->get('error_canceled_order_status');
|
||||||
|
}
|
||||||
|
|
||||||
$sortOrder = isset($request->post['payment_payanyway_sort_order'])
|
$sortOrder = isset($request->post['payment_payanyway_sort_order'])
|
||||||
? (int)$request->post['payment_payanyway_sort_order']
|
? (int)$request->post['payment_payanyway_sort_order']
|
||||||
: self::MODULE_DEFAULT_SORT_ORDER;
|
: self::MODULE_DEFAULT_SORT_ORDER;
|
||||||
@@ -311,6 +327,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
$data['error_pending_order_status'] = isset($this->error['pending_order_status']) ? $this->error['pending_order_status'] : '';
|
$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_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'] : '';
|
$data['error_error_order_status'] = isset($this->error['error_order_status']) ? $this->error['error_order_status'] : '';
|
||||||
|
$data['error_canceled_order_status'] = isset($this->error['canceled_order_status']) ? $this->error['canceled_order_status'] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<int, array{text: string, href: string}> */
|
/** @return array<int, array{text: string, href: string}> */
|
||||||
@@ -331,12 +348,9 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @return bool код проверки целостности уже сохранён */
|
||||||
* @param string $key
|
private function hasIntegrityCode() {
|
||||||
* @return string
|
return '' !== (string)$this->config->get('payment_payanyway_integrity_code');
|
||||||
*/
|
|
||||||
private function getIntegrityCode($key) {
|
|
||||||
return htmlspecialchars((string)$this->retrieveRawValue($key), ENT_QUOTES, 'UTF-8');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// Heading
|
// Heading
|
||||||
$_['heading_title'] = 'PayAnyWay';
|
$_['heading_title'] = 'PayAnyWay (MONETA)';
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
$_['text_extension'] = 'Extensions';
|
$_['text_extension'] = 'Extensions';
|
||||||
@@ -15,6 +15,7 @@ $_['entry_account_label'] = 'Account number';
|
|||||||
$_['entry_account_description'] = 'Your extended account number in the MONETA.RU system';
|
$_['entry_account_description'] = 'Your extended account number in the MONETA.RU system';
|
||||||
$_['entry_integrity_code_label'] = 'Integrity Code';
|
$_['entry_integrity_code_label'] = 'Integrity Code';
|
||||||
$_['entry_integrity_code_description'] = 'Data integrity verification code specified in the extended account settings in the MONETA.RU system';
|
$_['entry_integrity_code_description'] = 'Data integrity verification code specified in the extended account settings in the MONETA.RU system';
|
||||||
|
$_['text_integrity_code_saved'] = 'The code is saved. Leave the field empty to keep it';
|
||||||
$_['entry_server_label'] = 'Payment server';
|
$_['entry_server_label'] = 'Payment server';
|
||||||
$_['entry_server_prod_label'] = 'Production';
|
$_['entry_server_prod_label'] = 'Production';
|
||||||
$_['entry_server_demo_label'] = 'Demo';
|
$_['entry_server_demo_label'] = 'Demo';
|
||||||
@@ -89,8 +90,8 @@ $_['help_sort_order'] = 'Determines the display order of the payment m
|
|||||||
$_['help_test_mode'] = 'Simulates payments without charging. Must match the account setting in MONETA.RU';
|
$_['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_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_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_canceled_order_status'] = 'Orders in these statuses are not paid: the module rejects the check request and the payment notification';
|
||||||
$_['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';
|
$_['help_callback_url'] = 'Set this address as Check URL and Pay URL in the extended account settings';
|
||||||
|
|
||||||
// Button
|
// Button
|
||||||
$_['button_integrity_code_toggle'] = 'Show or hide';
|
$_['button_integrity_code_toggle'] = 'Show or hide';
|
||||||
@@ -106,4 +107,5 @@ $_['error_permission'] = 'Warning: You do not have permission to modify paym
|
|||||||
$_['error_account'] = 'Account number is required and must contain digits only';
|
$_['error_account'] = 'Account number is required and must contain digits only';
|
||||||
$_['error_integrity_code'] = 'Account Integrity Code Required';
|
$_['error_integrity_code'] = 'Account Integrity Code Required';
|
||||||
$_['error_order_status'] = 'Select order status';
|
$_['error_order_status'] = 'Select order status';
|
||||||
|
$_['error_canceled_order_status'] = 'Pending, Success and Error order statuses cannot be among Canceled';
|
||||||
$_['error_receipt_email'] = 'Enter a valid e-mail no longer than 64 characters';
|
$_['error_receipt_email'] = 'Enter a valid e-mail no longer than 64 characters';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// Heading
|
// Heading
|
||||||
$_['heading_title'] = 'PayAnyWay';
|
$_['heading_title'] = 'PayAnyWay (МОНЕТА)';
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
$_['text_extension'] = 'Расширения';
|
$_['text_extension'] = 'Расширения';
|
||||||
@@ -15,6 +15,7 @@ $_['entry_account_label'] = 'Номер счёта';
|
|||||||
$_['entry_account_description'] = 'Ваш номер расширенного счёта в системе MONETA.RU';
|
$_['entry_account_description'] = 'Ваш номер расширенного счёта в системе MONETA.RU';
|
||||||
$_['entry_integrity_code_label'] = 'Код проверки целостности данных';
|
$_['entry_integrity_code_label'] = 'Код проверки целостности данных';
|
||||||
$_['entry_integrity_code_description'] = 'Код проверки целостности данных, указанный в настройках расширенного счёта в системе MONETA.RU';
|
$_['entry_integrity_code_description'] = 'Код проверки целостности данных, указанный в настройках расширенного счёта в системе MONETA.RU';
|
||||||
|
$_['text_integrity_code_saved'] = 'Код сохранён. Оставьте поле пустым, чтобы не менять его';
|
||||||
$_['entry_server_label'] = 'Сервер платежей';
|
$_['entry_server_label'] = 'Сервер платежей';
|
||||||
$_['entry_server_prod_label'] = 'Боевой';
|
$_['entry_server_prod_label'] = 'Боевой';
|
||||||
$_['entry_server_demo_label'] = 'Демо';
|
$_['entry_server_demo_label'] = 'Демо';
|
||||||
@@ -89,8 +90,8 @@ $_['help_sort_order'] = 'Определяет порядок отоб
|
|||||||
$_['help_test_mode'] = 'Имитация оплаты без реального списания средств. Режим должен совпадать с настройкой счёта в MONETA.RU';
|
$_['help_test_mode'] = 'Имитация оплаты без реального списания средств. Режим должен совпадать с настройкой счёта в MONETA.RU';
|
||||||
$_['help_fiscalization'] = 'Включайте, только если к счёту подключён кассовый сервис kassa.payanyway.ru';
|
$_['help_fiscalization'] = 'Включайте, только если к счёту подключён кассовый сервис kassa.payanyway.ru';
|
||||||
$_['help_receipt_email'] = 'Обязателен при фискализации. Подставляется в чек, только если у покупателя нет ни e-mail, ни телефона: без контакта касса чек не примет, поэтому он уйдёт на этот адрес (ФИО покупателя в чеке остаётся)';
|
$_['help_receipt_email'] = 'Обязателен при фискализации. Подставляется в чек, только если у покупателя нет ни e-mail, ни телефона: без контакта касса чек не примет, поэтому он уйдёт на этот адрес (ФИО покупателя в чеке остаётся)';
|
||||||
$_['help_canceled_order_status'] = 'Заказ в этих статусах не оплачивается: на проверочный запрос модуль отвечает отказом';
|
$_['help_canceled_order_status'] = 'Заказ в этих статусах не оплачивается: на проверочный запрос и уведомление об оплате модуль отвечает отказом';
|
||||||
$_['help_callback_url'] = 'Укажите этот адрес в настройках счёта MONETA.RU как Check URL и Pay URL. Он должен быть доступен из интернета';
|
$_['help_callback_url'] = 'Укажите этот адрес в настройках расширенного счёта как Check URL и Pay URL';
|
||||||
|
|
||||||
// Button
|
// Button
|
||||||
$_['button_integrity_code_toggle'] = 'Показать или скрыть';
|
$_['button_integrity_code_toggle'] = 'Показать или скрыть';
|
||||||
@@ -106,4 +107,5 @@ $_['error_permission'] = 'Недостаточно прав для изме
|
|||||||
$_['error_account'] = 'Номер счёта обязателен и состоит только из цифр';
|
$_['error_account'] = 'Номер счёта обязателен и состоит только из цифр';
|
||||||
$_['error_integrity_code'] = 'Код проверки целостности данных обязателен';
|
$_['error_integrity_code'] = 'Код проверки целостности данных обязателен';
|
||||||
$_['error_order_status'] = 'Выберите статус заказа';
|
$_['error_order_status'] = 'Выберите статус заказа';
|
||||||
|
$_['error_canceled_order_status'] = 'Статусы Обрабатываемого, Успешного и Неуспешного заказа не могут быть среди Отменённых';
|
||||||
$_['error_receipt_email'] = 'Укажите корректный e-mail не длиннее 64 символов';
|
$_['error_receipt_email'] = 'Укажите корректный e-mail не длиннее 64 символов';
|
||||||
|
|||||||
@@ -71,8 +71,8 @@
|
|||||||
<label class="col-sm-2 control-label" for="input-integrity-code">{{ entry_integrity_code_label }}</label>
|
<label class="col-sm-2 control-label" for="input-integrity-code">{{ entry_integrity_code_label }}</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="password" name="payment_payanyway_integrity_code" value="{{ payment_payanyway_integrity_code }}"
|
<input type="password" name="payment_payanyway_integrity_code" value=""
|
||||||
placeholder="{{ entry_integrity_code_description }}" id="input-integrity-code"
|
placeholder="{{ integrity_code_placeholder }}" id="input-integrity-code"
|
||||||
class="form-control" autocomplete="off"/>
|
class="form-control" autocomplete="off"/>
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" class="btn btn-default" id="button-integrity-code-toggle"
|
<button type="button" class="btn btn-default" id="button-integrity-code-toggle"
|
||||||
@@ -293,6 +293,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if error_canceled_order_status %}
|
||||||
|
<div class="text-danger">{{ error_canceled_order_status }}</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
class ControllerExtensionPaymentPayanyway extends Controller {
|
class ControllerExtensionPaymentPayanyway extends Controller {
|
||||||
const MODULE_NAME = 'payanyway';
|
const MODULE_NAME = 'payanyway';
|
||||||
const MODULE_VERSION = '1.0.5';
|
const MODULE_VERSION = '1.0.6';
|
||||||
const CMS_NAME = 'OpenCart';
|
const CMS_NAME = 'OpenCart';
|
||||||
|
|
||||||
const PAYMENT_SERVER_PROD = 'prod';
|
const PAYMENT_SERVER_PROD = 'prod';
|
||||||
@@ -20,8 +20,12 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
const TEXT_CONTENT_TYPE = 'text/plain; charset=UTF-8';
|
const TEXT_CONTENT_TYPE = 'text/plain; charset=UTF-8';
|
||||||
|
|
||||||
const TRANSACTION_ID_DELIMITER = '_';
|
const TRANSACTION_ID_DELIMITER = '_';
|
||||||
const LEGACY_TRANSACTION_ID_DELIMITER = '|';
|
const TRANSACTION_ID_ZONE = 'UTC';
|
||||||
const TRANSACTION_ID_PATTERN = '/^([1-9][0-9]*)[%s]([0-9]{14})([A-Z0-9+-]{1,6})?$/';
|
const TRANSACTION_ID_PATTERN = '/^([1-9][0-9]*)_(0|[1-9][0-9]{0,15})_([0-9]{14})UTC$/D';
|
||||||
|
const LEGACY_TRANSACTION_ID_PATTERN = '/^([1-9][0-9]*)\\|([0-9]{14})$/D';
|
||||||
|
const LEGACY_ZONED_TRANSACTION_ID_PATTERN = '/^([1-9][0-9]*)_([0-9]{14})([A-Z]{1,6}|[+-][0-9]{2}|[+-][0-9]{4})?$/D';
|
||||||
|
const LEGACY_TRANSACTION_MAX_AGE_DAYS = 14;
|
||||||
|
const OPERATION_ID_PATTERN = '/^[0-9]{1,20}$/D';
|
||||||
const DESCRIPTION_MAX_LENGTH = 500;
|
const DESCRIPTION_MAX_LENGTH = 500;
|
||||||
|
|
||||||
const CLIENT_NAME_MAX_LENGTH = 256;
|
const CLIENT_NAME_MAX_LENGTH = 256;
|
||||||
@@ -40,6 +44,8 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
const DEFAULT_CANCELED_ORDER_STATUS_IDS = '7,9,16';
|
const DEFAULT_CANCELED_ORDER_STATUS_IDS = '7,9,16';
|
||||||
|
|
||||||
const JSON_NUMBER_PREFIX = '__payanyway_number:';
|
const JSON_NUMBER_PREFIX = '__payanyway_number:';
|
||||||
|
const JSON_NUMBER_KEYS = 'amount|price';
|
||||||
|
const ORDER_LOCK_TIMEOUT_SECONDS = 10;
|
||||||
|
|
||||||
/** @var int */
|
/** @var int */
|
||||||
private $mntId;
|
private $mntId;
|
||||||
@@ -71,7 +77,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
/** @var bool */
|
/** @var bool */
|
||||||
private $fiscalization;
|
private $fiscalization;
|
||||||
|
|
||||||
/** @var string «Email для чеков»: подставляется, если у покупателя нет ни email, ни телефона */
|
/** @var string */
|
||||||
private $receiptEmail;
|
private $receiptEmail;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
@@ -113,8 +119,8 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
$data['action'] = (self::PAYMENT_SERVER_PROD === $this->server) ? self::PAYMENT_URL_PROD : self::PAYMENT_URL_DEMO;
|
$data['action'] = (self::PAYMENT_SERVER_PROD === $this->server) ? self::PAYMENT_URL_PROD : self::PAYMENT_URL_DEMO;
|
||||||
|
|
||||||
$data['MNT_ID'] = $this->mntId;
|
$data['MNT_ID'] = $this->mntId;
|
||||||
$data['MNT_TRANSACTION_ID'] = $this->createTransactionId();
|
|
||||||
$data['MNT_AMOUNT'] = $this->getOrderAmount();
|
$data['MNT_AMOUNT'] = $this->getOrderAmount();
|
||||||
|
$data['MNT_TRANSACTION_ID'] = $this->createTransactionId($data['MNT_AMOUNT']);
|
||||||
$data['MNT_CURRENCY_CODE'] = $this->currency;
|
$data['MNT_CURRENCY_CODE'] = $this->currency;
|
||||||
$data['MNT_TEST_MODE'] = $this->testMode ? '1' : '0';
|
$data['MNT_TEST_MODE'] = $this->testMode ? '1' : '0';
|
||||||
$data['MNT_DESCRIPTION'] = $this->getDescription();
|
$data['MNT_DESCRIPTION'] = $this->getDescription();
|
||||||
@@ -159,17 +165,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
|
|
||||||
$this->load->language('extension/payment/payanyway');
|
$this->load->language('extension/payment/payanyway');
|
||||||
|
|
||||||
$orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null;
|
$json['success'] = $this->changeStatusUnderOrderLock(true, $this->pendingOrderStatusId, $this->language->get('text_order_confirmed'));
|
||||||
$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->addHeader('Content-Type: application/json');
|
||||||
$this->response->setOutput(json_encode($json));
|
$this->response->setOutput(json_encode($json));
|
||||||
@@ -182,10 +178,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
public function success() {
|
public function success() {
|
||||||
$this->load->language('extension/payment/payanyway');
|
$this->load->language('extension/payment/payanyway');
|
||||||
|
|
||||||
$orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null;
|
$this->changeStatusUnderOrderLock(false, $this->pendingOrderStatusId, $this->language->get('text_payment_processing'));
|
||||||
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));
|
$this->response->redirect($this->url->link('checkout/success', '', true));
|
||||||
}
|
}
|
||||||
@@ -197,10 +190,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
public function fail() {
|
public function fail() {
|
||||||
$this->load->language('extension/payment/payanyway');
|
$this->load->language('extension/payment/payanyway');
|
||||||
|
|
||||||
$orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null;
|
$this->changeStatusUnderOrderLock(false, $this->errorOrderStatusId, $this->language->get('text_payment_error'));
|
||||||
if ((null !== $orderStatusId) && ($orderStatusId !== $this->successOrderStatusId)) {
|
|
||||||
$this->addOrderHistory($this->errorOrderStatusId, $this->language->get('text_payment_error'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->response->redirect($this->url->link('checkout/failure', '', true));
|
$this->response->redirect($this->url->link('checkout/failure', '', true));
|
||||||
}
|
}
|
||||||
@@ -218,9 +208,18 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
return $this->model_checkout_order->getOrder($this->orderId);
|
return $this->model_checkout_order->getOrder($this->orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return string */
|
/**
|
||||||
private function createTransactionId() {
|
* @param string $amount
|
||||||
return $this->orderId . self::TRANSACTION_ID_DELIMITER . date('YmdHisT');
|
* @return string
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function createTransactionId($amount) {
|
||||||
|
$kopecks = ltrim(str_replace('.', '', $amount), '0');
|
||||||
|
$issuedAt = new \DateTime('now', new \DateTimeZone(self::TRANSACTION_ID_ZONE));
|
||||||
|
|
||||||
|
return $this->orderId
|
||||||
|
. self::TRANSACTION_ID_DELIMITER . (('' !== $kopecks) ? $kopecks : '0')
|
||||||
|
. self::TRANSACTION_ID_DELIMITER . $issuedAt->format('YmdHis') . self::TRANSACTION_ID_ZONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -313,6 +312,61 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
return (null !== $phone) ? $phone : '';
|
return (null !== $phone) ? $phone : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return bool */
|
||||||
|
private function isPaidWithModule() {
|
||||||
|
return isset($this->orderInfo['payment_code']) && (self::MODULE_NAME === $this->orderInfo['payment_code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $orderStatusId
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isAwaitingPayment($orderStatusId) {
|
||||||
|
return in_array($orderStatusId, array(0, $this->pendingOrderStatusId, $this->errorOrderStatusId), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool $allowFailed подтверждение формы допускает повторную попытку после «Неуспешного»
|
||||||
|
* @param int $orderStatusId
|
||||||
|
* @param string $comment
|
||||||
|
* @return bool
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function changeStatusUnderOrderLock($allowFailed, $orderStatusId, $comment) {
|
||||||
|
if (!$this->isStatusChangeableByCustomer($allowFailed)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->load->model('extension/payment/payanyway');
|
||||||
|
if (!$this->model_extension_payment_payanyway->acquireOrderLock($this->orderId, self::ORDER_LOCK_TIMEOUT_SECONDS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->orderInfo = $this->getOrderInfo();
|
||||||
|
$changeable = $this->isStatusChangeableByCustomer($allowFailed)
|
||||||
|
&& !$this->model_extension_payment_payanyway->hasOrderStatusInHistory($this->orderId, $this->successOrderStatusId);
|
||||||
|
if ($changeable) {
|
||||||
|
$this->addOrderHistory($orderStatusId, $comment);
|
||||||
|
}
|
||||||
|
$this->model_extension_payment_payanyway->releaseOrderLock($this->orderId);
|
||||||
|
|
||||||
|
return $changeable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool $allowFailed
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isStatusChangeableByCustomer($allowFailed) {
|
||||||
|
if (empty($this->orderInfo) || !$this->isPaidWithModule()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderStatusId = (int)$this->orderInfo['order_status_id'];
|
||||||
|
|
||||||
|
return $allowFailed ? $this->isAwaitingPayment($orderStatusId) : in_array($orderStatusId, array(0, $this->pendingOrderStatusId), true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $orderStatusId
|
* @param int $orderStatusId
|
||||||
* @param string $comment
|
* @param string $comment
|
||||||
@@ -338,22 +392,40 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
|
|
||||||
$this->validateCallback($callback);
|
$this->validateCallback($callback);
|
||||||
|
|
||||||
$isCheck = 'CHECK' === $callback['MNT_COMMAND'];
|
if (!$this->hasRequiredOperationId($callback)) {
|
||||||
if ($isCheck && !$this->checkSignature($callback, $callback['MNT_SIGNATURE'])) {
|
$this->log->write('PayAnyWay callback: MNT_OPERATION_ID is missing or not numeric');
|
||||||
$this->sendResponse(self::FAIL_RESPONSE);
|
$this->sendResponse(self::FAIL_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->orderId = $this->parseOrderIdFromTransactionId($callback['MNT_TRANSACTION_ID']);
|
if (!$this->checkSignature($callback, $callback['MNT_SIGNATURE'])) {
|
||||||
|
$this->log->write('PayAnyWay callback: MNT_SIGNATURE does not match');
|
||||||
|
$this->sendResponse(self::FAIL_RESPONSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
$transaction = $this->parseTransactionId($callback['MNT_TRANSACTION_ID']);
|
||||||
|
$this->orderId = (null !== $transaction) ? $transaction['order_id'] : null;
|
||||||
if (null === $this->orderId) {
|
if (null === $this->orderId) {
|
||||||
$this->log->write('PayAnyWay callback: MNT_TRANSACTION_ID has unexpected format');
|
$this->log->write('PayAnyWay callback: MNT_TRANSACTION_ID has unexpected format');
|
||||||
$this->sendResponse(self::FAIL_RESPONSE);
|
$this->sendResponse(self::FAIL_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$isCheck = 'CHECK' === $callback['MNT_COMMAND'];
|
||||||
|
$this->load->model('extension/payment/payanyway');
|
||||||
|
if (!$isCheck && !$this->model_extension_payment_payanyway->acquireOrderLock($this->orderId, self::ORDER_LOCK_TIMEOUT_SECONDS)) {
|
||||||
|
$this->log->write('PayAnyWay callback: order is locked by another request');
|
||||||
|
$this->sendResponse(self::FAIL_RESPONSE);
|
||||||
|
}
|
||||||
|
|
||||||
$this->orderInfo = $this->getOrderInfo();
|
$this->orderInfo = $this->getOrderInfo();
|
||||||
if (empty($this->orderInfo)) {
|
if (empty($this->orderInfo)) {
|
||||||
$this->sendResponse($isCheck ? $this->buildUnknownOrderResponse($callback) : self::FAIL_RESPONSE);
|
$this->sendResponse($isCheck ? $this->buildUnknownOrderResponse($callback) : self::FAIL_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->isPaidWithModule()) {
|
||||||
|
$this->log->write('PayAnyWay callback: order is not paid with PayAnyWay');
|
||||||
|
$this->sendResponse($isCheck ? $this->buildUnknownOrderResponse($callback) : self::FAIL_RESPONSE);
|
||||||
|
}
|
||||||
|
|
||||||
$isCheck ? $this->handleCheckCallback($callback) : $this->handlePayCallback($callback);
|
$isCheck ? $this->handleCheckCallback($callback) : $this->handlePayCallback($callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,19 +456,31 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $transactionId
|
* @param mixed $transactionId
|
||||||
* @return int|null
|
* @return array{order_id: int, amount: string|null, issued_at: string}|null
|
||||||
*/
|
*/
|
||||||
private function parseOrderIdFromTransactionId($transactionId) {
|
private function parseTransactionId($transactionId) {
|
||||||
if (!is_string($transactionId)) {
|
if (!is_string($transactionId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$delimiters = preg_quote(self::TRANSACTION_ID_DELIMITER . self::LEGACY_TRANSACTION_ID_DELIMITER, '/');
|
if (preg_match(self::TRANSACTION_ID_PATTERN, $transactionId, $matches)) {
|
||||||
if (!preg_match(sprintf(self::TRANSACTION_ID_PATTERN, $delimiters), $transactionId, $matches)) {
|
$kopecks = str_pad($matches[2], 3, '0', STR_PAD_LEFT);
|
||||||
return null;
|
|
||||||
|
return array(
|
||||||
|
'order_id' => (int)$matches[1],
|
||||||
|
'amount' => substr($kopecks, 0, -2) . '.' . substr($kopecks, -2),
|
||||||
|
'issued_at' => $matches[3],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)$matches[1];
|
if (preg_match(self::LEGACY_TRANSACTION_ID_PATTERN, $transactionId, $matches)
|
||||||
|
|| (preg_match(self::LEGACY_ZONED_TRANSACTION_ID_PATTERN, $transactionId, $matches)
|
||||||
|
&& (!isset($matches[3]) || !preg_match('/^[+-]/', $matches[3]) || ($matches[3] === date('T'))))
|
||||||
|
) {
|
||||||
|
return array('order_id' => (int)$matches[1], 'amount' => null, 'issued_at' => $matches[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return int|null */
|
/** @return int|null */
|
||||||
@@ -433,7 +517,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
$this->sendResponse(self::FAIL_RESPONSE);
|
$this->sendResponse(self::FAIL_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->mntId !== (int)$callback['MNT_ID']) {
|
if (!$this->isOwnAccount($callback)) {
|
||||||
$this->sendResponse(self::FAIL_RESPONSE);
|
$this->sendResponse(self::FAIL_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +527,23 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $callback
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isOwnAccount(array $callback) {
|
||||||
|
return (string)$this->mntId === $callback['MNT_ID'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $callback
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function hasRequiredOperationId(array $callback) {
|
||||||
|
return ('CHECK' === $callback['MNT_COMMAND'])
|
||||||
|
|| (1 === preg_match(self::OPERATION_ID_PATTERN, $callback['MNT_OPERATION_ID']));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $data
|
* @param array<string, mixed> $data
|
||||||
* @return bool
|
* @return bool
|
||||||
@@ -475,6 +576,10 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
private function handleCheckCallback(array $callbackData) {
|
private function handleCheckCallback(array $callbackData) {
|
||||||
|
if (!$this->isOrderPaid()) {
|
||||||
|
$this->rejectPaymentMismatch($callbackData);
|
||||||
|
}
|
||||||
|
|
||||||
list($resultCode, $description) = $this->determineCheckResult($callbackData);
|
list($resultCode, $description) = $this->determineCheckResult($callbackData);
|
||||||
$responseData = $this->buildBaseResponseData($callbackData, $resultCode, $description);
|
$responseData = $this->buildBaseResponseData($callbackData, $resultCode, $description);
|
||||||
|
|
||||||
@@ -494,26 +599,15 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
private function handlePayCallback(array $callbackData) {
|
private function handlePayCallback(array $callbackData) {
|
||||||
$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->getOrderAmount(),
|
|
||||||
'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(self::FAIL_RESPONSE);
|
|
||||||
}
|
|
||||||
|
|
||||||
$isPayedOrder = $this->isOrderPaid();
|
$isPayedOrder = $this->isOrderPaid();
|
||||||
if (!$isPayedOrder) {
|
if (!$isPayedOrder) {
|
||||||
|
$this->rejectPaymentMismatch($callbackData);
|
||||||
|
$this->rejectCanceledOrder($callbackData);
|
||||||
|
|
||||||
$this->load->language('extension/payment/payanyway');
|
$this->load->language('extension/payment/payanyway');
|
||||||
$this->addOrderHistory($this->successOrderStatusId, $this->getPaymentCompletedComment($callbackData['MNT_OPERATION_ID']));
|
$this->addOrderHistory($this->successOrderStatusId, $this->getPaymentCompletedComment($callbackData['MNT_OPERATION_ID']));
|
||||||
}
|
}
|
||||||
|
$this->model_extension_payment_payanyway->releaseOrderLock($this->orderId);
|
||||||
|
|
||||||
$resultCode = 200;
|
$resultCode = 200;
|
||||||
$description = $isPayedOrder ? 'Order already paid' : 'Order success paid';
|
$description = $isPayedOrder ? 'Order already paid' : 'Order success paid';
|
||||||
@@ -522,6 +616,80 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
$this->sendResponse($this->buildXmlResponse($responseData + $this->getResponseAttributes()));
|
$this->sendResponse($this->buildXmlResponse($responseData + $this->getResponseAttributes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $callbackData
|
||||||
|
* @return void
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function rejectPaymentMismatch(array $callbackData) {
|
||||||
|
$mismatch = $this->getPaymentMismatch($callbackData);
|
||||||
|
if (null !== $mismatch) {
|
||||||
|
$this->log->write('PayAnyWay callback: ' . $mismatch);
|
||||||
|
$this->sendResponse(self::FAIL_RESPONSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Заказ отменили до оплаты — оплата его не проводит: подписанный 500 без атрибутов прекращает
|
||||||
|
* повторы, номер операции в логе нужен администратору для возврата в кабинете MONETA.RU.
|
||||||
|
* Как Check URL и остальные модули.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $callbackData
|
||||||
|
* @return void
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function rejectCanceledOrder(array $callbackData) {
|
||||||
|
$response = $this->buildCanceledOrderPayResponse($callbackData);
|
||||||
|
if (null !== $response) {
|
||||||
|
$this->log->write('PayAnyWay callback: order ' . $this->orderId . ' is canceled, payment ' . $callbackData['MNT_OPERATION_ID'] . ' must be refunded');
|
||||||
|
$this->sendResponse($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $callbackData
|
||||||
|
* @return array{data: string, contentType: string}|null
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function buildCanceledOrderPayResponse(array $callbackData) {
|
||||||
|
$orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : 0;
|
||||||
|
if (!in_array($orderStatusId, $this->canceledOrderStatusIds, true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->buildXmlResponse($this->buildBaseResponseData($callbackData, 500, 'Order status is ' . $this->getOrderStatusById($orderStatusId)), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $callbackData
|
||||||
|
* @return string|null
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function getPaymentMismatch(array $callbackData) {
|
||||||
|
$transaction = $this->parseTransactionId($callbackData['MNT_TRANSACTION_ID']);
|
||||||
|
if ((null === $transaction) || ($transaction['order_id'] !== $this->orderId)) {
|
||||||
|
return 'MNT_TRANSACTION_ID has unexpected format';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $transaction['amount']) {
|
||||||
|
$issuedAt = \DateTime::createFromFormat('!YmdHis', $transaction['issued_at']);
|
||||||
|
if ((false === $issuedAt) || ($issuedAt->modify('+' . self::LEGACY_TRANSACTION_MAX_AGE_DAYS . ' days') < new \DateTime())) {
|
||||||
|
return 'MNT_TRANSACTION_ID of the previous format has expired';
|
||||||
|
}
|
||||||
|
} elseif (('' !== $callbackData['MNT_AMOUNT']) && ($callbackData['MNT_AMOUNT'] !== $transaction['amount'])) {
|
||||||
|
return 'MNT_AMOUNT does not match MNT_TRANSACTION_ID';
|
||||||
|
} elseif ($transaction['amount'] !== $this->getOrderAmount()) {
|
||||||
|
return 'order amount changed after the payment form was issued';
|
||||||
|
}
|
||||||
|
|
||||||
|
$isCheck = 'CHECK' === $callbackData['MNT_COMMAND'];
|
||||||
|
if ((!$isCheck || ('' !== $callbackData['MNT_AMOUNT'])) && !$this->isOrderAmountMatched($callbackData)) {
|
||||||
|
return 'MNT_AMOUNT or MNT_CURRENCY_CODE does not match the order';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $operationId
|
* @param string $operationId
|
||||||
* @return string
|
* @return string
|
||||||
@@ -603,7 +771,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
return array(100, "Order status is {$orderStatus}");
|
return array(100, "Order status is {$orderStatus}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->successOrderStatusId === $orderStatusId) {
|
if ($this->isOrderPaid()) {
|
||||||
return array(200, 'Order Paid');
|
return array(200, 'Order Paid');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,13 +782,34 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
return array(402, 'Order created, but not paid');
|
return array(402, 'Order created, but not paid');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return bool */
|
/**
|
||||||
|
* @return bool
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
private function isOrderPaid() {
|
private function isOrderPaid() {
|
||||||
if (!isset($this->orderInfo['order_status_id'])) {
|
if (!isset($this->orderInfo['order_status_id'])) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->successOrderStatusId === (int)$this->orderInfo['order_status_id'];
|
if ($this->successOrderStatusId === (int)$this->orderInfo['order_status_id']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->load->model('extension/payment/payanyway');
|
||||||
|
|
||||||
|
return $this->model_extension_payment_payanyway->hasOrderStatusInHistory((int)$this->orderId, $this->successOrderStatusId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $callbackData
|
||||||
|
* @return bool
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function isOrderAmountMatched(array $callbackData) {
|
||||||
|
$currencyCode = isset($this->orderInfo['currency_code']) ? (string)$this->orderInfo['currency_code'] : '';
|
||||||
|
|
||||||
|
return ($callbackData['MNT_AMOUNT'] === $this->getOrderAmount())
|
||||||
|
&& ($callbackData['MNT_CURRENCY_CODE'] === $currencyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -936,7 +1125,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
* @param string $vatRate
|
* @param string $vatRate
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getVat($vatRate) {
|
private function getVat($vatRate) {
|
||||||
$map = array(
|
$map = array(
|
||||||
'1104' => 'vat0',
|
'1104' => 'vat0',
|
||||||
'1108' => 'vat5',
|
'1108' => 'vat5',
|
||||||
@@ -959,7 +1148,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
* @return string
|
* @return string
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function getOrderStatusById($orderStatusId) {
|
private function getOrderStatusById($orderStatusId) {
|
||||||
$this->load->model('localisation/order_status');
|
$this->load->model('localisation/order_status');
|
||||||
/**@var array<int, array{order_status_id: string, name: string}> $orderStatuses */
|
/**@var array<int, array{order_status_id: string, name: string}> $orderStatuses */
|
||||||
$orderStatuses = $this->model_localisation_order_status->getOrderStatuses();
|
$orderStatuses = $this->model_localisation_order_status->getOrderStatuses();
|
||||||
@@ -988,7 +1177,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
* @return array
|
* @return array
|
||||||
* @throws \JsonException
|
* @throws \JsonException
|
||||||
*/
|
*/
|
||||||
public function buildJsonResponse(array $data) {
|
private function buildJsonResponse(array $data) {
|
||||||
$client = $data['client'];
|
$client = $data['client'];
|
||||||
if (isset($client['phone']) && (strlen($client['phone']) > self::CHECK_CLIENT_PHONE_MAX_LENGTH)) {
|
if (isset($client['phone']) && (strlen($client['phone']) > self::CHECK_CLIENT_PHONE_MAX_LENGTH)) {
|
||||||
unset($client['phone']);
|
unset($client['phone']);
|
||||||
@@ -1014,7 +1203,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
|
|
||||||
$json = json_encode($jsonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
$json = json_encode($jsonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
if (false !== $json) {
|
if (false !== $json) {
|
||||||
$json = preg_replace('/"' . preg_quote(self::JSON_NUMBER_PREFIX, '/') . '([0-9]+\.[0-9]{2})"/', '$1', $json);
|
$json = preg_replace('/"(' . self::JSON_NUMBER_KEYS . ')":"' . preg_quote(self::JSON_NUMBER_PREFIX, '/') . '([0-9]+\.[0-9]{2})"/', '"$1":$2', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
@@ -1057,7 +1246,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
|
|
||||||
foreach ($requiredFields as $field) {
|
foreach ($requiredFields as $field) {
|
||||||
if (isset($data[$field]) && ($data[$field] !== '')) {
|
if (isset($data[$field]) && ($data[$field] !== '')) {
|
||||||
$root->appendChild($dom->createElement($field, (string)$data[$field]));
|
$root->appendChild($this->createTextElement($dom, $field, (string)$data[$field]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1078,8 +1267,8 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
foreach ($attributeMap as $key => $value) {
|
foreach ($attributeMap as $key => $value) {
|
||||||
if (!empty($value)) {
|
if (!empty($value)) {
|
||||||
$attrElement = $dom->createElement('ATTRIBUTE');
|
$attrElement = $dom->createElement('ATTRIBUTE');
|
||||||
$attrElement->appendChild($dom->createElement('KEY', $key));
|
$attrElement->appendChild($this->createTextElement($dom, 'KEY', $key));
|
||||||
$attrElement->appendChild($dom->createElement('VALUE', (string)$value));
|
$attrElement->appendChild($this->createTextElement($dom, 'VALUE', (string)$value));
|
||||||
$attributes->appendChild($attrElement);
|
$attributes->appendChild($attrElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1087,6 +1276,22 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
|||||||
return $this->saveXmlResponse($dom);
|
return $this->saveXmlResponse($dom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createElement($name, $value) не экранирует «&» — текст только через createTextNode().
|
||||||
|
*
|
||||||
|
* @param \DOMDocument $dom
|
||||||
|
* @param string $name
|
||||||
|
* @param string $value
|
||||||
|
* @return \DOMElement
|
||||||
|
* @throws DOMException
|
||||||
|
*/
|
||||||
|
private function createTextElement(\DOMDocument $dom, $name, $value) {
|
||||||
|
$element = $dom->createElement($name);
|
||||||
|
$element->appendChild($dom->createTextNode($value));
|
||||||
|
|
||||||
|
return $element;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \DOMDocument $dom
|
* @param \DOMDocument $dom
|
||||||
* @return array
|
* @return array
|
||||||
|
|||||||
@@ -23,6 +23,54 @@ class ModelExtensionPaymentPayanyway extends Model {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Блокировка заказа на время Pay URL (GET_LOCK MySQL/MariaDB): два уведомления подряд
|
||||||
|
* не обрабатываются параллельно. Снимается releaseOrderLock() или с закрытием соединения.
|
||||||
|
*
|
||||||
|
* @param int $orderId
|
||||||
|
* @param int $timeoutSeconds
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function acquireOrderLock($orderId, $timeoutSeconds) {
|
||||||
|
$query = $this->db->query("SELECT GET_LOCK('" . $this->db->escape($this->getOrderLockName($orderId)) . "', " . (int)$timeoutSeconds . ") AS acquired");
|
||||||
|
|
||||||
|
return isset($query->row['acquired']) && (1 === (int)$query->row['acquired']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $orderId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function releaseOrderLock($orderId) {
|
||||||
|
$this->db->query("SELECT RELEASE_LOCK('" . $this->db->escape($this->getOrderLockName($orderId)) . "')");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Заказ когда-либо получал этот статус — по истории заказа.
|
||||||
|
*
|
||||||
|
* @param int $orderId
|
||||||
|
* @param int $orderStatusId
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasOrderStatusInHistory($orderId, $orderStatusId) {
|
||||||
|
$query = $this->db->query("SELECT 1 FROM `" . DB_PREFIX . "order_history`
|
||||||
|
WHERE `order_id` = '" . (int)$orderId . "'
|
||||||
|
AND `order_status_id` = '" . (int)$orderStatusId . "'
|
||||||
|
LIMIT 1");
|
||||||
|
|
||||||
|
return $query->num_rows > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Имя блокировки — не длиннее 64 символов и своё для каждой БД магазина.
|
||||||
|
*
|
||||||
|
* @param int $orderId
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getOrderLockName($orderId) {
|
||||||
|
return 'payanyway_' . substr(md5(DB_DATABASE . '.' . DB_PREFIX), 0, 16) . '_' . (int)$orderId;
|
||||||
|
}
|
||||||
|
|
||||||
/** @return bool */
|
/** @return bool */
|
||||||
private function isModuleEnabled() {
|
private function isModuleEnabled() {
|
||||||
return (bool)$this->config->get('payment_payanyway_status');
|
return (bool)$this->config->get('payment_payanyway_status');
|
||||||
|
|||||||
Reference in New Issue
Block a user