10 Commits
Author SHA1 Message Date
ivan.sadovin 7f81c7f2fe Merge branch 'main' into 'release'
feat: add fiscalization settings, POST callbacks and fix callback issues

See merge request products/cashier/cms/OpenCart3!14
2026-10-21 11:41:23 +03:00
ivan.sadovin 6598b95bb8 feat: add fiscalization settings, POST callbacks and fix callback issues 2026-10-21 11:41:23 +03:00
ivan.sadovin d1ac3d989b Merge branch 'main' into 'release'
fix: changed check response from xml to json

See merge request products/cashier/cms/OpenCart3!12
2026-07-07 19:19:14 +03:00
ivan.sadovin d53430155a fix: changed check response from xml to json 2026-07-07 19:19:14 +03:00
ivan.sadovin f94dc40eee Merge branch 'feature/merge-main-to-release' into 'release'
Feature/merge main to release

See merge request products/cashier/cms/OpenCart3!10
2026-07-03 15:11:52 +03:00
ivan.sadovin c3189d2439 Feature/merge main to release 2026-07-03 15:11:52 +03:00
ivan.sadovin 83f1d8d101 Merge branch 'main' into 'release'
feat: refactor callback

See merge request products/cashier/cms/OpenCart3!6
2026-05-19 18:13:11 +03:00
ivan.sadovin 1e168ded8c feat: refactor callback 2026-05-19 18:13:11 +03:00
ivan.sadovin d57dba7f7c Merge branch 'main' into 'release'
v1.0.0

See merge request products/cashier/cms/OpenCart3!4
2026-05-15 17:04:16 +03:00
ivan.sadovin d6d5580b3a v1.0.0 2026-05-15 17:04:16 +03:00
12 changed files with 2662 additions and 59 deletions
+99 -59
View File
@@ -1,93 +1,133 @@
# openCart3
# 🌟 Платёжный модуль PayAnyWay для [OpenCart](https://www.opencart.com/) v3.x
![openCart](https://img.shields.io/badge/openCart-3.x-0070C0?logo=php&logoColor=white)
![PHP](https://img.shields.io/badge/PHP-5.6%2B-purple)
![License](https://img.shields.io/badge/License-MIT-green)
> **📦 Версия модуля: 1.0.4**
## Getting started
> **ℹ️ Совместимость версий 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
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
> **ℹ️ Совместимость версий 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
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
> **ℹ️ Совместимость версий ocStore:**
> 3.0.2.0, 3.0.3.7, 3.0.3.9
## Add your files
> **ℹ️ Примечание по валюте**
> Данный модуль работает **только с валютой RUB (Российский рубль)**.
> Перед активацией модуля убедитесь, что на сайте в качестве базовой валюты установлен RUB, либо у пользователя выбран счёт в RUB. Использование других валют не поддерживается.
* [Create](https://docs.gitlab.com/user/project/repository/web_editor/#create-a-file) or [upload](https://docs.gitlab.com/user/project/repository/web_editor/#upload-a-file) files
* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
## 🏗️ Структура модуля
```
cd existing_repo
git remote add origin https://git.moneta.ru:8000/products/cashier/cms/openCart3.git
git branch -M main
git push -uf origin main
├── upload
│ ├── admin
│ │ ├── controller/extension/payment/payanyway.php
│ │ ├── language/{en-gb,ru-ru}/extension/payment/payanyway.php
│ │ ├── view/image/payment/payanyway.png
│ │ └── view/template/extension/payment/payanyway.twig
│ └── catalog
│ ├── 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 — OCMOD: приём Check/Pay URL методом POST (правка роутера витрины)
└── README.md
```
## Integrate with your tools
## 🚀 Установка и настройка модуля
* [Set up project integrations](https://git.moneta.ru:8000/products/cashier/cms/openCart3/-/settings/integrations)
1. 📥 [Скачайте архив модуля](https://git.pub.moneta.ru/CMS/openCart3/releases) и переименуйте в `payanyway.ocmod.zip`.
2. 📂 Загрузите расширение через панель управления интернет-магазином.
Выберите раздел **"Модули / Расширения"** → **"Установка расширений"**. Нажмите "Загрузить" и выберите скачанный архив.
3. ⚙️ В разделе **"Модули / Расширения"** → **"Модули / Расширения"** выберите тип расширения **"Оплата"**.
В списке модулей оплаты найдите **PayAnyWay** и нажмите:
- **«Активировать»** — для включения модуля
- **«Редактировать»** — для перехода к настройкам
4. ✏️ Заполните соответствующие поля в настройках способа оплаты:
## Collaborate with your team
| Поле | Описание |
|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Включить модуль оплаты** | Включите или выключите модуль оплаты |
| **Номер счёта** | Ваш номер расширенного счёта в системе [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, кнопка «Скопировать» |
| **Регион отображения** | Географическая зона, в которой будет отображаться способ оплаты |
| **Порядок сортировки** | Определяет порядок отображения платежного метода в списке методов оплаты при оформлении заказа. Чем меньше число, тем выше позиция в списке |
* [Invite team members and collaborators](https://docs.gitlab.com/user/project/members/)
* [Create a new merge request](https://docs.gitlab.com/user/project/merge_requests/creating_merge_requests/)
* [Automatically close issues from merge requests](https://docs.gitlab.com/user/project/issues/managing_issues/#closing-issues-automatically)
* [Enable merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/)
* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
Сумма позиций чека всегда равна сумме оплаты: налоги OpenCart входят в цену товаров, скидки (купоны, подарочные сертификаты, бонусные баллы) распределяются по товарам, доставка и сборы идут отдельными позициями. В истории заказа при оплате сохраняется номер операции MONETA.RU: «Заказ оплачен. Номер операции: N».
## Test and Deploy
> ⚠️ **После обновления модуля** нажмите **«Модули / Расширения» → «Модификации» → «Обновить»**, затем откройте настройки PayAnyWay, проверьте новые поля (фискализация после обновления включена) и нажмите **«Сохранить»**. Пока при включённой фискализации не задан «Email для чеков», над формой настроек показывается предупреждение.
Use the built-in continuous integration in GitLab.
---
* [Get started with GitLab CI/CD](https://docs.gitlab.com/ci/quick_start/)
* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/user/application_security/sast/)
* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/topics/autodevops/requirements/)
* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/user/clusters/agent/)
* [Set up protected environments](https://docs.gitlab.com/ci/environments/protected_environments/)
## 🔧 Инструкция по настройке счета PayAnyWay
***
1. 📝 [Зарегистрируйтесь](https://payanyway.ru/partnerRegistration.htm) в платёжной системе PayAnyWay и заполните все необходимые данные. Дождитесь проверки аккаунта и создайте **расширенный счет**.
# Editing this README
2. ⚙️ Заполните настройки расширенного счета (раздел **«Мой счет» → «Управление счетами» → «Редактировать счет»**):
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
| Параметр | Значение |
|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Тестовый режим** | `Нет` |
| **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<sup>\*</sup>** | `GET` или `POST` (требуется настройка) |
| **Можно переопределять настройки в URL** | `Да` |
| **Подпись формы оплаты обязательна** | `Да` |
| **Код проверки целостности данных** | `ваш_код` (произвольный набор символов) |
| **Success URL** | Оставить пустым |
| **Fail URL** | Оставить пустым |
| **InProgress URL** | Необязательное поле. URL страницы магазина, куда должен попасть покупатель после успешного запроса на авторизацию средств, до подтверждения списания и зачисления средств. Поддерживается ограниченным количеством методов оплаты. Переход пользователя произойдет независимо от получения магазином средств и отчета о проведенной оплате |
| **Return URL** | Оставить пустым |
## Suggestions for a good README
> ⚠️ **Важно!** Для кириллического домена PayURL и CheckURL должны быть указаны в кодировке [Punycode](https://2ip.ru/punycode/).
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
### <sup>\*</sup> 📨 Уведомления методом POST
## Name
Choose a self-explaining name for your project.
При **HTTP method** = `POST` MONETA.RU переносит параметры из адреса Check URL / Pay URL в тело запроса, в том числе `route=extension/payment/payanyway/callback`. Штатный OpenCart 3 ищет маршрут только в адресе и без доработки отвечает главной страницей, а платёжная форма показывает «Код ошибки: 302».
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
Модуль решает это модификацией OCMOD из `install.xml`: маршрут колбэка (и только его) берётся из тела запроса. Чтобы модификация заработала:
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
1. Устанавливайте модуль только через **«Модули / Расширения» → «Установка расширений»** (архив `payanyway.ocmod.zip`). При заливке файлов по FTP модификация не регистрируется.
2. После установки или обновления модуля откройте **«Модули / Расширения» → «Модификации»** (Extensions → Modifications) и нажмите синюю кнопку **«Обновить»** вверху справа. Повторяйте это после включения, выключения или удаления любой модификации и после кнопки «Очистить».
3. Откройте настройки PayAnyWay. Если модификация не применена, над формой будет жёлтое предупреждение. Причину смотрите на вкладке «Лог» в «Модификациях»: строка `NOT FOUND` у файла `catalog/controller/startup/router.php` означает, что роутер уже изменён другой модификацией.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
Если модификацию применить не удаётся, выберите в настройках счёта **HTTP method** = `GET`: при GET модификация не нужна.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
---
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## 📚 Полезные ресурсы OpenCart
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
* [OpenCart](https://www.opencart.com/)
* [Документация](https://docs.opencart.com/)
* [Релизы](https://github.com/opencart/opencart/releases)
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## 📚 Полезные ресурсы OpenCart "Русская сборка"
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
* [Русская сборка OpenCart](https://opencart-russia.ru/)
* [Документация и FAQ](https://forum.opencart-russia.ru/forums/wiki/)
* [Форум поддержки](https://forum.opencart-russia.ru/)
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
## 📚 Полезные ресурсы OsStore
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
* [OsStore](https://ocstore.com/)
* [Форум поддержки](https://opencartforum.com/)
---
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
**✅ Модуль настроен, приятных платежей!** 💰🎉
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<modification>
<name>PayAnyWay</name>
<code>payanyway</code>
<version>1.0.4</version>
<author>PayAnyWay</author>
<link>https://payanyway.ru/</link>
<!--
При методе POST MONETA.RU переносит query string Check/Pay URL в тело запроса:
route=extension/payment/payanyway/callback приходит в POST, а OpenCart берёт маршрут
только из GET и отдаёт главную страницу. Маршрут колбэка (и только его) берётся из POST,
если в GET маршрута нет. После установки — «Расширения» → «Модификации» → «Обновить».
Строка не найдена (роутер изменён другой модификацией или сборкой) — запись в ocmod.log,
админка модуля показывает предупреждение (маркер payanyway-post-route).
-->
<file path="catalog/controller/startup/router.php">
<operation error="log">
<search><![CDATA[if (isset($this->request->get['route']) && $this->request->get['route'] != 'startup/router') {]]></search>
<add position="before"><![CDATA[
// payanyway-post-route: маршрут Check/Pay URL, пришедший в теле POST (см. install.xml модуля PayAnyWay).
if (!isset($this->request->get['route']) && isset($this->request->post['route']) && ($this->request->post['route'] === 'extension/payment/payanyway/callback')) {
$this->request->get['route'] = $this->request->post['route'];
}
]]></add>
</operation>
</file>
</modification>
+516
View File
@@ -0,0 +1,516 @@
<?php
/**
* @property ModelSettingSetting $model_setting_setting
* @property ModelLocalisationCurrency $model_localisation_currency
* @property ModelLocalisationOrderStatus $model_localisation_order_status
* @property ModelLocalisationGeoZone $model_localisation_geo_zone
*/
class ControllerExtensionPaymentPayanyway extends Controller {
const CMS_NAME = 'OpenCart';
const MODULE_NAME = 'payanyway';
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',
'vat0' => '1104',
'vat5' => '1108',
'vat7' => '1109',
'vat10' => '1103',
'vat22' => '1113',
'vat105' => '1110',
'vat107' => '1111',
'vat110' => '1107',
'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();
/**
* @return void
* @throws \Exception
*/
public function index() {
$this->load->language('extension/payment/payanyway');
$this->document->setTitle($this->language->get('heading_title'));
if (($this->request->server['REQUEST_METHOD'] === 'POST') && $this->validate($this->request)) {
$this->load->model('setting/setting');
$this->model_setting_setting->editSetting('payment_payanyway', $this->request->post);
$this->session->data['success'] = $this->language->get('text_success');
$this->response->redirect($this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true));
}
$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);
$data['cancel'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true);
$data['payment_payanyway_status'] = $this->retrieveValue('payment_payanyway_status');
$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();
$data['payment_payanyway_currency'] = $this->retrieveValue('payment_payanyway_currency');
$data['order_statuses'] = $this->getAvailableOrderStatuses();
$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['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');
$data['footer'] = $this->load->controller('common/footer');
$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
*/
public function install() {
$defaultSettings = array(
'payment_payanyway_status' => 0,
'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_name' => $this->getCmsName(),
);
$orderStatuses = $this->getAvailableOrderStatuses();
$pendingStatus = array_filter($orderStatuses, function ($status) {
return (int)$status['order_status_id'] === 1;
});
if (!empty($pendingStatus)) {
$pendingKey = key($pendingStatus);
$defaultSettings['payment_payanyway_pending_order_status_id'] = $pendingStatus[$pendingKey]['order_status_id'];
}
$successStatus = array_filter($orderStatuses, function ($status) {
return (int)$status['order_status_id'] === 5;
});
if (!empty($successStatus)) {
$successKey = key($successStatus);
$defaultSettings['payment_payanyway_success_order_status_id'] = $successStatus[$successKey]['order_status_id'];
}
$errorStatus = array_filter($orderStatuses, function ($status) {
return (int)$status['order_status_id'] === 10;
});
if (!empty($errorStatus)) {
$errorKey = key($errorStatus);
$defaultSettings['payment_payanyway_error_order_status_id'] = $errorStatus[$errorKey]['order_status_id'];
}
$this->load->model('setting/setting');
$this->model_setting_setting->editSetting('payment_payanyway', $defaultSettings);
}
/**
* @return void
* @throws \Exception
*/
public function uninstall() {
$this->load->model('setting/setting');
$this->model_setting_setting->deleteSetting('payment_payanyway');
}
/**
* @param \Request $request
* @return bool
*/
private function validate($request) {
if (!$this->user->hasPermission('modify', 'extension/payment/payanyway')) {
$this->error['warning'] = $this->language->get('error_permission');
}
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');
}
// 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');
}
if (empty($request->post['payment_payanyway_success_order_status_id'])) {
$this->error['success_order_status'] = $this->language->get('error_order_status');
}
if (empty($request->post['payment_payanyway_error_order_status_id'])) {
$this->error['error_order_status'] = $this->language->get('error_order_status');
}
$sortOrder = isset($request->post['payment_payanyway_sort_order'])
? (int)$request->post['payment_payanyway_sort_order']
: self::MODULE_DEFAULT_SORT_ORDER;
$request->post['payment_payanyway_sort_order'] = $sortOrder;
$request->post['payment_payanyway_cms_name'] = $this->getCmsName();
return empty($this->error);
}
/**
* @param array<string, mixed> $data
* @return void
*/
private function applyValidationErrors(array &$data) {
$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'] : '';
}
/** @return array<int, array{text: string, href: string}> */
private function getBreadcrumbs() {
return array(
array(
'text' => $this->language->get('text_home'),
'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true),
),
array(
'text' => $this->language->get('text_extension'),
'href' => $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true),
),
array(
'text' => $this->language->get('heading_title'),
'href' => $this->url->link('extension/payment/payanyway', 'user_token=' . $this->session->data['user_token'], true),
),
);
}
/**
* @param string $key
* @return string
*/
private function getIntegrityCode($key) {
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<string, string>
*/
private function getOptions(array $values, $labelPrefix) {
$result = array();
foreach ($values as $value) {
$result[$value] = $this->language->get($labelPrefix . $value);
}
return $result;
}
/** @return array<string, mixed> */
private function getModuleVatRates() {
$result = array();
foreach ($this->availableVatRates as $vat => $value) {
$label = 'entry_' . $vat . '_label';
$result[$value] = $this->language->get($label);
}
return $result;
}
/**
* @return array<string, mixed>
* @throws \Exception
*/
private function getModuleCurrencies() {
$this->load->model('localisation/currency');
$shopCurrencies = $this->model_localisation_currency->getCurrencies();
$result = array();
foreach ($this->supportedCurrencies as $currencyCode) {
$result[$currencyCode] = isset($shopCurrencies[$currencyCode])
? $shopCurrencies[$currencyCode]
: $this->getDefaultCurrencyDefinition($currencyCode);
}
return $result;
}
/**
* @param string $currencyCode
* @return array<string, mixed>
*/
private function getDefaultCurrencyDefinition($currencyCode) {
$definitions = array(
'RUB' => array(
'currency_id' => 0,
'title' => 'Рубль',
'code' => 'RUB',
'symbol_left' => '',
'symbol_right' => '₽',
'decimal_place' => '2',
'value' => 1.00000000,
'status' => '1',
'date_modified' => date('Y-m-d H:i:s'),
),
);
return isset($definitions[$currencyCode]) ? $definitions[$currencyCode] : array();
}
/**
* @return array<string, mixed>
* @throws \Exception
*/
private function getAvailableOrderStatuses() {
$this->load->model('localisation/order_status');
return $this->model_localisation_order_status->getOrderStatuses();
}
/**
* @return array<string, mixed>
* @throws \Exception
*/
private function getAvailableGeoZones() {
$this->load->model('localisation/geo_zone');
return $this->model_localisation_geo_zone->getGeoZones();
}
/**
* @return string
*/
private function getCmsName() {
$this->load->language('common/footer');
$textFooter = $this->language->get('text_footer');
$textFooter = is_string($textFooter) ? $textFooter : '';
return $this->detectCmsByPatterns($textFooter);
}
private function detectCmsByPatterns($textFooter) {
$patterns = [
'ocStore' => '/ocStore/i',
'Russian OpenCart' => '/opencart-russia/i',
self::CMS_NAME => '/opencart\.com/i'
];
if (!empty($textFooter)) {
foreach ($patterns as $cms => $pattern) {
if (preg_match($pattern, $textFooter)) {
return $cms;
}
}
}
return self::CMS_NAME;
}
/**
* @param string $name
* @return mixed
*/
private function retrieveValue($name) {
if (!is_string($name)) {
throw new \InvalidArgumentException("The name must be a string");
}
if (isset($this->request->post[$name])) {
return $this->request->post[$name];
}
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;
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
// Heading
$_['heading_title'] = 'PayAnyWay';
// Text
$_['text_extension'] = 'Extensions';
$_['text_success'] = 'Settings for the PayAnyWay payment method have been saved successfully';
$_['text_edit'] = 'Editing module settings';
$_['text_payanyway'] = '<a href="https://www.payanyway.ru/" target="_blank"><img src="view/image/payment/payanyway.png" alt="PayAnyWay" title="PayAnyWay" width="163" height="18"></a>';
// Entry
$_['entry_status_label'] = 'Enable payment module';
$_['entry_account_label'] = 'Account number';
$_['entry_account_description'] = 'Your extended account number in the MONETA.RU system';
$_['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_server_label'] = 'Payment server';
$_['entry_server_prod_label'] = 'Production';
$_['entry_server_demo_label'] = 'Demo';
$_['entry_vat_rate_label'] = 'VAT';
$_['entry_vat_none_label'] = 'Without VAT';
$_['entry_vat0_label'] = '0%';
$_['entry_vat5_label'] = '5%';
$_['entry_vat7_label'] = '7%';
$_['entry_vat10_label'] = '10%';
$_['entry_vat22_label'] = '22%';
$_['entry_vat105_label'] = '5% (calculated rate 5/105)';
$_['entry_vat107_label'] = '7% (calculated rate 7/107)';
$_['entry_vat110_label'] = '10% (calculated rate 10/110)';
$_['entry_vat122_label'] = '22% (calculated rate 22/122)';
$_['entry_currency_label'] = 'Payment acceptance';
$_['entry_currency_rub_label'] = 'Russian Ruble';
$_['entry_currency_description'] = 'The currencies in the MONETA.RU system and in the store must match';
$_['entry_select_order_status_label'] = ' --- Select status --- ';
$_['entry_order_statuses_label'] = 'Order Statuses';
$_['entry_pending_order_status_label'] = 'Pending';
$_['entry_success_order_status_label'] = 'Success';
$_['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';
$_['help_error_order_status'] = 'Specify the order status that is set upon payment error';
$_['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 <a href="%s">Extensions → Modifications → Refresh</a> 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 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';
+109
View File
@@ -0,0 +1,109 @@
<?php
// Heading
$_['heading_title'] = 'PayAnyWay';
// Text
$_['text_extension'] = 'Расширения';
$_['text_success'] = 'Настройки для способа оплаты PayAnyWay успешно сохранены';
$_['text_edit'] = 'Редактирование настроек модуля';
$_['text_payanyway'] = '<a href="https://www.payanyway.ru/" target="_blank"><img src="view/image/payment/payanyway.png" alt="PayAnyWay" title="PayAnyWay" width="163" height="18"></a>';
// Entry
$_['entry_status_label'] = 'Включить модуль оплаты';
$_['entry_account_label'] = 'Номер счёта';
$_['entry_account_description'] = 'Ваш номер расширенного счёта в системе MONETA.RU';
$_['entry_integrity_code_label'] = 'Код проверки целостности данных';
$_['entry_integrity_code_description'] = 'Код проверки целостности данных, указанный в настройках расширенного счёта в системе MONETA.RU';
$_['entry_server_label'] = 'Сервер платежей';
$_['entry_server_prod_label'] = 'Боевой';
$_['entry_server_demo_label'] = 'Демо';
$_['entry_vat_rate_label'] = 'НДС';
$_['entry_vat_none_label'] = 'Без НДС';
$_['entry_vat0_label'] = '0%';
$_['entry_vat5_label'] = '5%';
$_['entry_vat7_label'] = '7%';
$_['entry_vat10_label'] = '10%';
$_['entry_vat22_label'] = '22%';
$_['entry_vat105_label'] = '5% (расчётная ставка 5/105)';
$_['entry_vat107_label'] = '7% (расчётная ставка 7/107)';
$_['entry_vat110_label'] = '10% (расчётная ставка 10/110)';
$_['entry_vat122_label'] = '22% (расчётная ставка 22/122)';
$_['entry_currency_label'] = 'Валюта платежа';
$_['entry_currency_rub_label'] = 'Рубль';
$_['entry_currency_description'] = 'Валюты в системе MONETA.RU и в магазине должны совпадать';
$_['entry_select_order_status_label'] = ' --- Выберите статус --- ';
$_['entry_order_statuses_label'] = 'Статусы заказа';
$_['entry_pending_order_status_label'] = 'Обрабатываемый';
$_['entry_success_order_status_label'] = 'Успешный';
$_['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'] = 'Укажите статус заказа, который выставляется при успешной оплате';
$_['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 не дойдут до модуля. Нажмите <a href="%s">Расширения → Модификации → Обновить</a> и проверьте лог модификаций. При методе GET в настройках счёта модификация не нужна.';
$_['warning_receipt_email'] = 'Фискализация чеков включена, а «Email для чеков» не задан: покупателю без e-mail и телефона чек не будет пробит. Укажите адрес в настройках ниже или выключите фискализацию, если кассовый сервис не подключён.';
// Error
$_['error_permission'] = 'Недостаточно прав для изменения настроек способа оплаты PayAnyWay';
$_['error_account'] = 'Номер счёта обязателен и состоит только из цифр';
$_['error_integrity_code'] = 'Код проверки целостности данных обязателен';
$_['error_order_status'] = 'Выберите статус заказа';
$_['error_receipt_email'] = 'Укажите корректный e-mail не длиннее 64 символов';
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+384
View File
@@ -0,0 +1,384 @@
{{ header }} {{ column_left }}
<div id="content">
<div class="page-header">
<div class="container-fluid">
<div class="pull-right">
<button type="submit" form="form-payment" data-toggle="tooltip" title="{{ button_save }}"
class="btn btn-primary"><i class="fa fa-save"></i></button>
<a href="{{ cancel }}" data-toggle="tooltip" title="{{ button_cancel }}" class="btn btn-default"><i
class="fa fa-reply"></i></a></div>
<h1>{{ heading_title }}</h1>
<ul class="breadcrumb">
{% for breadcrumb in breadcrumbs %}
<li><a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a></li>
{% endfor %}
</ul>
</div>
</div>
<div class="container-fluid">
{% if warning_ocmod %}
<div class="alert alert-warning"><i class="fa fa-exclamation-triangle"></i> {{ warning_ocmod }}</div>
{% endif %}
{% if warning_receipt_email %}
<div class="alert alert-warning"><i class="fa fa-exclamation-triangle"></i> {{ warning_receipt_email }}</div>
{% endif %}
{% if error_warning %}
<div class="alert alert-danger"><i class="fa fa-exclamation-circle"></i> {{ error_warning }}
<button type="button" class="close" data-dismiss="alert">&times;</button>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-pencil"></i> {{ text_edit }}</h3>
</div>
<div class="panel-heading text-center">
<a href="https://www.payanyway.ru/" target="_blank">
<img src="view/image/payment/payanyway.png" alt="PayAnyWay" title="PayAnyWay" width="336" height="50"/>
</a>
</div>
<div class="panel-body">
<form action="{{ action }}" method="post" enctype="multipart/form-data" id="form-payment"
class="form-horizontal">
{# Вкл/выкл платежного модуля #}
<div class="form-group">
<div class="col-sm-10 col-sm-offset-2">
<div class="form-check" style="display: flex; column-gap: 8px;">
<input type="hidden" name="payment_payanyway_status" value="0"/>
<input class="form-check-input" type="checkbox" name="payment_payanyway_status" id="input-status"
value="1"
{% if payment_payanyway_status == "1" %} checked{% endif %}>
<label class="form-check-label" for="input-status">{{ entry_status_label }}</label>
</div>
</div>
</div>
{# Номер счета #}
<div class="form-group required">
<label class="col-sm-2 control-label" for="input-account">{{ entry_account_label }}</label>
<div class="col-sm-10">
<input type="number" oninput="this.value = this.value.replace(/[^0-9]/g, '')" step="1"
name="payment_payanyway_account" value="{{ payment_payanyway_account }}"
placeholder="{{ entry_account_description }}" id="input-account" class="form-control"/>
{% if error_account %}
<div class="text-danger">{{ error_account }}</div>
{% endif %}
</div>
</div>
{# Код проверки целостности данных #}
<div class="form-group required">
<label class="col-sm-2 control-label" for="input-integrity-code">{{ entry_integrity_code_label }}</label>
<div class="col-sm-10">
<div class="input-group">
<input type="password" name="payment_payanyway_integrity_code" value="{{ payment_payanyway_integrity_code }}"
placeholder="{{ entry_integrity_code_description }}" id="input-integrity-code"
class="form-control" autocomplete="off"/>
<span class="input-group-btn">
<button type="button" class="btn btn-default" id="button-integrity-code-toggle"
data-toggle="tooltip" title="{{ button_integrity_code_toggle }}"><i class="fa fa-eye"></i></button>
</span>
</div>
{% if error_integrity_code %}
<div class="text-danger">{{ error_integrity_code }}</div>
{% endif %}
</div>
</div>
{# Сервер платежей #}
<div class="form-group">
<label class="col-sm-2 control-label" for="input-server">{{ entry_server_label }}</label>
<div class="col-sm-10">
<select name="payment_payanyway_server" id="input-server" class="form-control">
<option value="prod" {% if payment_payanyway_server == 'prod' %} selected="selected" {% endif %}>{{ entry_server_prod_label }}</option>
<option value="demo" {% if payment_payanyway_server == 'demo' %} selected="selected" {% endif %}>{{ entry_server_demo_label }}</option>
</select>
</div>
</div>
{# Тестовый режим #}
<div class="form-group">
<div class="col-sm-10 col-sm-offset-2">
<div class="form-check" style="display: flex; column-gap: 8px;">
<input type="hidden" name="payment_payanyway_test_mode" value="0"/>
<input class="form-check-input" type="checkbox" name="payment_payanyway_test_mode" id="input-test-mode"
value="1"
{% if payment_payanyway_test_mode == "1" %} checked{% endif %}>
<label class="form-check-label" for="input-test-mode">{{ entry_test_mode_label }}</label>
</div>
<p class="help-block">{{ help_test_mode }}</p>
</div>
</div>
{# Фискализация чеков #}
<div class="form-group">
<div class="col-sm-10 col-sm-offset-2">
<div class="form-check" style="display: flex; column-gap: 8px;">
<input type="hidden" name="payment_payanyway_fiscalization" value="0"/>
<input class="form-check-input" type="checkbox" name="payment_payanyway_fiscalization"
id="input-fiscalization" value="1"
{% if payment_payanyway_fiscalization == "1" %} checked{% endif %}>
<label class="form-check-label" for="input-fiscalization">{{ entry_fiscalization_label }}</label>
</div>
<p class="help-block">{{ help_fiscalization }}</p>
</div>
</div>
{# Поля чека — только при фискализации. Скрываются, а не блокируются: заблокированное
поле браузер не отправляет, и значение потерялось бы. #}
<div id="fiscalization-fields">
{# Email для чеков #}
<div class="form-group required">
<label class="col-sm-2 control-label" for="input-receipt-email">{{ entry_receipt_email_label }}</label>
<div class="col-sm-10">
<input type="email" name="payment_payanyway_receipt_email" value="{{ payment_payanyway_receipt_email }}"
placeholder="shop@example.ru" id="input-receipt-email" class="form-control" maxlength="64"/>
<p class="help-block">{{ help_receipt_email }}</p>
{% if error_receipt_email %}
<div class="text-danger">{{ error_receipt_email }}</div>
{% endif %}
</div>
</div>
{# НДС #}
<div class="form-group">
<label class="col-sm-2 control-label" for="input-vat-rate">{{ entry_vat_rate_label }}</label>
<div class="col-sm-10">
<select name="payment_payanyway_vat_rate" id="input-vat-rate" class="form-control">
{% for vat_id, label in payment_payanyway_vat_rates %}
<option value="{{ vat_id }}"
{% if vat_id == payment_payanyway_vat_rate %}selected="selected"{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
</div>
{# Признак способа расчёта #}
<div class="form-group">
<label class="col-sm-2 control-label" for="input-payment-method">{{ entry_payment_method_label }}</label>
<div class="col-sm-10">
<select name="payment_payanyway_payment_method" id="input-payment-method" class="form-control">
{% for value, label in payment_payanyway_payment_methods %}
<option value="{{ value }}" {% if value == payment_payanyway_payment_method %}selected="selected"{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
{# Признак предмета расчёта #}
<div class="form-group">
<label class="col-sm-2 control-label" for="input-payment-object">{{ entry_payment_object_label }}</label>
<div class="col-sm-10">
<select name="payment_payanyway_payment_object" id="input-payment-object" class="form-control">
{% for value, label in payment_payanyway_payment_objects %}
<option value="{{ value }}" {% if value == payment_payanyway_payment_object %}selected="selected"{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
{# Валюта платежа #}
<div class="form-group">
<label class="col-sm-2 control-label" for="input-currency">{{ entry_currency_label }}</label>
<div class="col-sm-10">
<select name="payment_payanyway_currency" id="input-currency" class="form-control">
{% for code, data in payment_payanyway_currencies %}
<option value="{{ code }}" {% if code == payment_payanyway_currency %}selected="selected"{% endif %}>
{{ data.code }} ({{ entry_currency_rub_label }})
</option>
{% endfor %}
</select>
<p class="help-block">{{ entry_currency_description }}</p>
</div>
</div>
{# Статусы заказа #}
<div class="form-group required">
<label class="col-sm-2 control-label">{{ entry_order_statuses_label }}</label>
<div class="col-sm-10">
<div class="panel panel-default" style="margin-bottom: 0;">
<div class="panel-body">
{# pending #}
<div class="form-group">
<label class="col-sm-3 control-label" for="input-pending-order-status">
<span data-toggle="tooltip"
title="{{ help_pending_order_status }}">{{ entry_pending_order_status_label }}</span>
</label>
<div class="col-sm-9">
<select name="payment_payanyway_pending_order_status_id" id="input-pending-order-status"
class="form-control">
<option value="" disabled
selected="selected">{{ entry_select_order_status_label|default(text_select) }}</option>
{% for order_status in order_statuses %}
<option value="{{ order_status.order_status_id }}"
{% if order_status.order_status_id == payment_payanyway_pending_order_status_id %}selected="selected"{% endif %}>
{{ order_status.name }}
</option>
{% endfor %}
</select>
{% if error_pending_order_status %}
<div class="text-danger">{{ error_order_status }}</div>
{% endif %}
</div>
</div>
{# success #}
<div class="form-group">
<label class="col-sm-3 control-label" for="input-success-order-status">
<span data-toggle="tooltip"
title="{{ help_success_order_status }}">{{ entry_success_order_status_label }}</span>
</label>
<div class="col-sm-9">
<select name="payment_payanyway_success_order_status_id" id="input-success-order-status"
class="form-control">
<option value="" disabled
selected="selected">{{ entry_select_order_status_label|default(text_select) }}</option>
{% for order_status in order_statuses %}
<option value="{{ order_status.order_status_id }}"
{% if order_status.order_status_id == payment_payanyway_success_order_status_id %}selected="selected"{% endif %}>
{{ order_status.name }}
</option>
{% endfor %}
</select>
{% if error_success_order_status %}
<div class="text-danger">{{ error_order_status }}</div>
{% endif %}
</div>
</div>
{# error #}
<div class="form-group">
<label class="col-sm-3 control-label" for="input-error-order-status">
<span data-toggle="tooltip"
title="{{ help_error_order_status }}">{{ entry_error_order_status_label }}</span>
</label>
<div class="col-sm-9">
<select name="payment_payanyway_error_order_status_id" id="input-error-order-status"
class="form-control">
<option value="" disabled
selected="selected">{{ entry_select_order_status_label|default(text_select) }}</option>
{% for order_status in order_statuses %}
<option value="{{ order_status.order_status_id }}"
{% if order_status.order_status_id == payment_payanyway_error_order_status_id %}selected="selected"{% endif %}>
{{ order_status.name }}
</option>
{% endfor %}
</select>
{% if error_error_order_status %}
<div class="text-danger">{{ error_order_status }}</div>
{% endif %}
</div>
</div>
{# canceled #}
<div class="form-group">
<label class="col-sm-3 control-label">
<span data-toggle="tooltip"
title="{{ help_canceled_order_status }}">{{ entry_canceled_order_status_label }}</span>
</label>
<div class="col-sm-9">
<div class="well well-sm" style="height: 150px; overflow: auto; margin-bottom: 0;">
{% for order_status in order_statuses %}
<div class="checkbox">
<label>
<input type="checkbox" name="payment_payanyway_canceled_order_status_ids[]"
value="{{ order_status.order_status_id }}"
{% if order_status.order_status_id in payment_payanyway_canceled_order_status_ids %} checked="checked"{% endif %}/>
{{ order_status.name }}
</label>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{# module_geo_zone #}
<div class="form-group">
<label class="col-sm-2 control-label" for="input-geo-zone">
<span data-toggle="tooltip" title="{{ help_geo_zone }}">{{ entry_geo_zone_label }}</span>
</label>
<div class="col-sm-10">
<select name="payment_payanyway_geo_zone_id" id="input-geo-zone" class="form-control">
<option value="0">{{ text_all_zones }}</option>
{% for geo_zone in geo_zones %}
{% if geo_zone.geo_zone_id == payment_payanyway_geo_zone_id %}
<option value="{{ geo_zone.geo_zone_id }}" selected="selected">{{ geo_zone.name }}</option>
{% else %}
<option value="{{ geo_zone.geo_zone_id }}">{{ geo_zone.name }}</option>
{% endif %}
{% endfor %}
</select>
<p class="help-block">{{ entry_geo_zone_description }}</p>
</div>
</div>
{# URL для уведомлений #}
<div class="form-group">
<label class="col-sm-2 control-label" for="input-callback-url">{{ entry_callback_url_label }}</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" value="{{ callback_url }}" id="input-callback-url" class="form-control" readonly/>
<span class="input-group-btn">
<button type="button" class="btn btn-default" id="button-callback-url-copy">{{ button_copy }}</button>
</span>
</div>
<p class="help-block">{{ help_callback_url }}</p>
</div>
</div>
{# sort-order #}
<div class="form-group">
<label class="col-sm-2 control-label" for="input-sort-order">
<span data-toggle="tooltip" title="{{ help_sort_order }}">{{ entry_sort_order_label }}</span>
</label>
<div class="col-sm-10">
<input type="number" oninput="this.value = this.value.replace(/[^-0-9]/g, '').replace(/(?!^-)-/g, '')"
step="1" name="payment_payanyway_sort_order" value="{{ payment_payanyway_sort_order }}"
placeholder="{{ module_sort_order_label }}" id="input-sort-order" class="form-control"/>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<style>
{# Разделитель админки — .form-group + .form-group: обёртка полей чека его разрывает. #}
#fiscalization-fields > .form-group:first-child,
#fiscalization-fields + .form-group {
border-top: 1px solid #ededed;
}
</style>
<script>
$(function () {
function toggleFiscalizationFields() {
$('#fiscalization-fields').toggle($('#input-fiscalization').is(':checked'));
}
$('#input-fiscalization').on('change', toggleFiscalizationFields);
toggleFiscalizationFields();
$('#button-integrity-code-toggle').on('click', function () {
var $input = $('#input-integrity-code');
$input.attr('type', $input.attr('type') === 'password' ? 'text' : 'password');
$(this).find('i').toggleClass('fa-eye fa-eye-slash');
});
$('#button-callback-url-copy').on('click', function () {
var $button = $(this);
$('#input-callback-url').trigger('select');
document.execCommand('copy');
$button.text('{{ text_copied }}');
setTimeout(function () { $button.text('{{ button_copy }}'); }, 2000);
});
});
</script>
{{ footer }}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
<?php
// Text
$_['text_locale'] = 'en';
$_['text_title'] = 'PayAnyWay (MIR, VISA, MasterCard and 30+ other ways)';
$_['text_ap_pay_with'] = 'Pay with';
$_['text_ap_and_other'] = 'or another payment method';
$_['text_ap_or'] = 'or';
$_['text_paw_pay'] = 'Pay';
$_['text_paw_list_methods'] = 'List of payment methods';
$_['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_operation'] = 'Operation number: %s';
$_['text_description'] = 'Payment for order';
$_['text_description_number'] = 'No. %s';
$_['text_description_customer'] = 'from %s';
$_['text_payment_error'] = 'Payment error';
@@ -0,0 +1,20 @@
<?php
// Text
$_['text_locale'] = 'ru';
$_['text_title'] = 'PayAnyWay (МИР, Visa, MasterCard и более 30-и способов оплаты)';
$_['text_ap_pay_with'] = 'Оплатить с';
$_['text_ap_and_other'] = 'или другой способ оплаты';
$_['text_ap_or'] = 'или';
$_['text_paw_pay'] = 'Оплатить';
$_['text_paw_list_methods'] = 'Список способов оплаты';
$_['text_paw_back_to_card'] = 'Вернуться к оплате картой';
$_['text_order_confirmed'] = 'Заказ подтвержден';
$_['text_payment_processing'] = 'Обработка платежа';
$_['text_payment_completed'] = 'Заказ оплачен';
$_['text_payment_operation'] = 'Номер операции: %s';
$_['text_description'] = 'Оплата заказа';
$_['text_description_number'] = '№%s';
$_['text_description_customer'] = 'от %s';
$_['text_payment_error'] = 'Ошибка оплаты';
+58
View File
@@ -0,0 +1,58 @@
<?php
class ModelExtensionPaymentPayanyway extends Model {
/**
* @param array<string, mixed> $address
* @return array<string, mixed>
*/
public function getMethod(array $address) {
$this->load->language('extension/payment/payanyway');
if (!$this->isModuleEnabled() ||
!$this->isCurrencySupported() ||
!$this->isGeoZoneValid($address)
) {
return array();
}
return array(
'code' => 'payanyway',
'title' => $this->language->get('text_title'),
'terms' => '',
'sort_order' => $this->config->get('payment_payanyway_sort_order'),
);
}
/** @return bool */
private function isModuleEnabled() {
return (bool)$this->config->get('payment_payanyway_status');
}
/** @return bool */
private function isCurrencySupported() {
$shopCurrency = isset($this->session->data['currency']) ? $this->session->data['currency'] : '';
return $shopCurrency === $this->config->get('payment_payanyway_currency');
}
/**
* @param array<string, mixed> $address
* @return bool
*/
private function isGeoZoneValid(array $address) {
$geoZoneId = (int)$this->config->get('payment_payanyway_geo_zone_id');
if ($geoZoneId === 0) {
return true;
}
$query = $this->db->query(
"SELECT * FROM " . DB_PREFIX . "zone_to_geo_zone
WHERE geo_zone_id = '" . $geoZoneId . "'
AND country_id = '" . (int)$address['country_id'] . "'
AND (zone_id = '" . (int)$address['zone_id'] . "' OR zone_id = '0')"
);
return $query->num_rows > 0;
}
}
@@ -0,0 +1,45 @@
<form id="payment-form" action="{{ action }}" method="post">
<input type="hidden" name="MNT_ID" value="{{ MNT_ID }}"/>
<input type="hidden" name="MNT_TRANSACTION_ID" value="{{ MNT_TRANSACTION_ID }}"/>
<input type="hidden" name="MNT_AMOUNT" value="{{ MNT_AMOUNT }}"/>
<input type="hidden" name="MNT_CURRENCY_CODE" value="{{ MNT_CURRENCY_CODE }}"/>
<input type="hidden" name="MNT_TEST_MODE" value="{{ MNT_TEST_MODE }}"/>
<input type="hidden" name="MNT_DESCRIPTION" value="{{ MNT_DESCRIPTION }}"/>
<input type="hidden" name="MNT_SUBSCRIBER_ID" value="{{ MNT_SUBSCRIBER_ID }}"/>
<input type="hidden" name="MNT_SIGNATURE" value="{{ MNT_SIGNATURE }}"/>
<input type="hidden" name="MNT_SUCCESS_URL" value="{{ MNT_SUCCESS_URL }}"/>
<input type="hidden" name="MNT_FAIL_URL" value="{{ MNT_FAIL_URL }}"/>
<input type="hidden" name="MNT_RETURN_URL" value="{{ MNT_RETURN_URL }}"/>
<input type="hidden" name="moneta.locale" value="{{ moneta_locale }}"/>
<input type="hidden" name="MNT_CMS" value="{{ MNT_CMS }}"/>
<div class="buttons">
<div class="{% if ap_use %} text-center {% else %} pull-right {% endif %}">
<input type="submit" value="{{ btn_confirm }}" class="btn btn-primary"/>
</div>
</div>
</form>
<script>
$(document).ready(function () {
$('#payment-form').on('submit', function (e) {
e.preventDefault();
$.ajax({
url: '{{ confirm_url }}',
type: 'POST',
data: $(this).serialize(),
dataType: 'json',
success: function (response) {
if (response.success) {
$('#payment-form').off('submit').submit();
} else {
window.location.href = '{{ MNT_FAIL_URL }}';
}
},
error: function () {
window.location.href = '{{ MNT_FAIL_URL }}';
}
});
});
});
</script>