getParam('base.moneta.moneta_system'));
/**
* Право «Модули: редактирование» и запрос со страницы магазина — до любой записи настроек.
* Ядро само проверяет права, только если клиент прислал суффикс `.modules.edit` в имени
* действия, поэтому проверяем здесь ({@see SettingsWriteGuard}). При отказе выводит сообщение,
* как ядро при нехватке прав.
*/
function monetaSettingsWriteAllowed(): bool
{
global $PHPShopBase;
$reason = SettingsWriteGuard::denialReason(
(bool) $PHPShopBase->Rule->CheckedRules('modules', 'edit'),
$_SERVER,
);
if ($reason === null) {
return true;
}
$reasons = [
SettingsWriteGuard::DENIED_RIGHTS => 'у администратора нет права «Модули: редактирование»',
SettingsWriteGuard::DENIED_METHOD => 'запрос не POST (переход по ссылке)',
SettingsWriteGuard::DENIED_ORIGIN => 'запрос отправлен не со страницы магазина (Origin/Referer)',
];
(new ErrorLogLogger())->error('Настройки не сохранены: ' . ($reasons[$reason] ?? $reason) . '.');
if (isset($_REQUEST['ajax'])) {
exit(json_encode(['success' => false]));
}
if ($reason === SettingsWriteGuard::DENIED_RIGHTS) {
$PHPShopBase->Rule->BadUserFormaWindow();
} else {
echo '
' . HostCharset::toPage(
'Настройки не сохранены: запрос пришёл не со страницы этого магазина. '
. 'Откройте настройки модуля заново и нажмите «Применить».',
) . '
';
}
return false;
}
/**
* Кнопка «Обновить модуль»: SQL из updates/<версия в БД>/ и новая версия из module.xml.
*/
function actionBaseUpdate()
{
global $PHPShopModules, $PHPShopOrm;
if (!monetaSettingsWriteAllowed()) {
return true;
}
$PHPShopOrm->clean();
$option = $PHPShopOrm->select();
$newVersion = $PHPShopModules->getUpdate($option['version']);
$PHPShopOrm->clean();
$action = $PHPShopOrm->update(['version_new' => $newVersion]);
header('Location: ?path=modules&id=' . urlencode((string) $_GET['id']));
return $action;
}
function actionUpdate()
{
global $PHPShopOrm, $PHPShopModules;
if (!monetaSettingsWriteAllowed()) {
return true;
}
// Защита на время между заменой файлов и кнопкой «Обновить модуль». Пока она не нажата,
// таблица настроек — ещё от встроенного модуля «Монета» 1.0: колонок account_id,
// integrity_code и остальных нет (их добавляет updates/1.2/update_module.sql). UPDATE по ним
// упал бы, а ядро не показывает ошибку — введённое пропало бы. Поэтому сохранение
// пропускаем; на вкладке «Основное» в это время висит предупреждение (actionStart()).
// После миграции проверка всегда проходит: это один SELECT строки настроек на «Применить».
// clean() — сброс состояния ORM ядра до и после select(), как в actionBaseUpdate().
$PHPShopOrm->clean();
$row = $PHPShopOrm->select();
if (!is_array($row) || SettingsRepository::needsUpgrade($row)) {
header('Location: ?path=modules&id=' . urlencode((string) $_GET['id']));
return false;
}
$PHPShopOrm->clean();
// Витрины мультисайта, на которых включён модуль.
$PHPShopModules->updateOption($_GET['id'], $_POST['servers'] ?? null);
$post = static function (string $name): string {
$value = $_POST[$name] ?? '';
return is_string($value) ? trim($value) : '';
};
$update = [
'payment_server_new' => PaymentServer::fromSetting($post('payment_server_new'))->getValue(),
'test_mode_new' => $post('test_mode_new') === '1' ? '1' : '0',
'title_end_new' => strip_tags($post('title_end_new')),
'fiscalization_new' => $post('fiscalization_new') === '1' ? '1' : '0',
'sno_new' => TaxSystem::fromSetting($post('sno_new'))->getValue(),
'payment_method_new' => PaymentMethod::fromSetting($post('payment_method_new'))->getValue(),
'payment_object_new' => PaymentObject::fromSetting($post('payment_object_new'))->getValue(),
'goods_vat_new' => VatSetting::fromSetting($post('goods_vat_new'))->getValue(),
'delivery_vat_new' => VatSetting::fromSetting($post('delivery_vat_new'))->getValue(),
'status_new' => (int) $post('status_new'),
'paid_status_new' => (int) $post('paid_status_new') > 0
? (int) $post('paid_status_new')
: Settings::STATUS_PAID_BY_GATEWAY,
];
$receiptEmail = $post('receipt_email_new');
$update['receipt_email_new'] = strlen($receiptEmail) <= 64
&& filter_var($receiptEmail, FILTER_VALIDATE_EMAIL) !== false
? $receiptEmail
: '';
$accountId = preg_replace('/[^0-9]/', '', $post('account_id_new')) ?? '';
if ($accountId !== '') {
$update['account_id_new'] = $accountId;
}
$integrityCode = $post('integrity_code_new');
if ($integrityCode !== '') {
$update['integrity_code_new'] = $integrityCode;
}
// «Email для чеков» обязателен при фискализации: без адреса включение не сохраняется, а уже
// включённая фискализация молча не выключается — о пустом адресе предупреждает actionStart().
if ($update['fiscalization_new'] === '1' && $update['receipt_email_new'] === '') {
$update['fiscalization_new'] = ((string) ($row['fiscalization'] ?? '0') === '1') ? '1' : '0';
}
$PHPShopOrm->debug = false;
$action = $PHPShopOrm->update($update);
header('Location: ?path=modules&id=' . urlencode((string) $_GET['id']));
return $action;
}
function actionStart()
{
global $PHPShopGUI, $PHPShopOrm;
PHPShopObj::loadClass('order');
$data = $PHPShopOrm->select();
$statuses = (new PHPShopOrderStatusArray())->getArray();
$statuses = is_array($statuses) ? $statuses : [];
$settings = Settings::fromRow(is_array($data) ? $data : []);
$context = new ShopContext();
// На магазине с базой utf-8 ядро перекодирует только строки, которые само пропускает через
// __() (подписи полей, подсказки, вкладки). Тексты setAlert(..., false), пункты setSelect()
// и собственная разметка выводятся как есть — их перекодирует модуль. Строки из БД
// (статусы заказа) уже в кодировке страницы.
$page = static function (string $text): string {
return HostCharset::toPage($text);
};
$Tab1 = '';
$needsUpgrade = is_array($data) && SettingsRepository::needsUpgrade($data);
// Кнопка «Обновить модуль» ядра отправляет эту же форму: пустые обязательные поля старой
// схемы браузер не пропустил бы, и кнопка не срабатывала бы.
$requiredText = $needsUpgrade ? 'text' : 'text.required';
if ($needsUpgrade) {
$Tab1 .= $PHPShopGUI->setAlert(
HostCharset::toPage(
'Файлы модуля обновлены, а настройки ещё от встроенного модуля «Монета». Нажмите «Обновить модуль»'
. ' на вкладке «О модуле» - номер счёта и код проверки целостности перенесутся. До этого приём'
. ' оплат выключен, а изменения на этой вкладке не сохраняются.',
),
'warning',
false,
);
}
$Tab1 .= $PHPShopGUI->setField(
'Номер счёта',
$PHPShopGUI->setInput($requiredText, 'account_id_new', $settings->getAccountId(), false, 300)
. $PHPShopGUI->setHelp('Ваш номер расширенного счёта'),
);
// Сохранённый код в HTML не выводится: пустое поле при сохранении — «не менять» (actionUpdate()).
// Разметка своя: setInput() ядра не умеет autocomplete, а без new-password браузер подставил бы
// сюда сохранённый пароль администратора и при сохранении затёр бы им код.
$hasIntegrityCode = is_array($data) && (string) ($data['integrity_code'] ?? '') !== '';
$Tab1 .= $PHPShopGUI->setField(
'Код проверки целостности данных',
''
. $PHPShopGUI->setHelp(
'Код проверки целостности данных, указанный в настройках расширенного счёта'
. ($hasIntegrityCode ? '. Сохранённый код не показывается — оставьте поле пустым, чтобы его не менять' : ''),
),
);
$servers = [
[HostCharset::toPage('Боевой'), PaymentServer::PROD, $settings->getPaymentServer()->getValue()],
[HostCharset::toPage('Демо'), PaymentServer::DEMO, $settings->getPaymentServer()->getValue()],
];
$Tab1 .= $PHPShopGUI->setField('Сервер платежей', $PHPShopGUI->setSelect('payment_server_new', $servers, 300));
$Tab1 .= $PHPShopGUI->setField(
'Тестовый режим',
$PHPShopGUI->setCheckbox('test_mode_new', '1', '', $settings->isTestMode() ? 1 : 0)
. $PHPShopGUI->setHelp('Должен совпадать с флагом «Тестовый режим» в настройках расширенного счёта'),
);
// Только для чтения, с кнопкой копирования: navigator.clipboard есть лишь на HTTPS и localhost,
// на HTTP-админке — запасной путь через выделение поля и execCommand('copy').
$callbackUrl = htmlspecialchars($context->callbackUrl(), ENT_QUOTES, 'Windows-1251');
$Tab1 .= $PHPShopGUI->setField(
'URL для уведомлений',
HostCharset::toPage(
'
'
. ''
. '
',
)
. $PHPShopGUI->setHelp(
'Укажите этот адрес в настройках расширенного счёта как Check URL и Pay URL',
),
);
$Tab1 .= $PHPShopGUI->setField(
'Описание оплаты',
$PHPShopGUI->setTextarea(
'title_end_new',
htmlspecialchars($settings->getPaymentDescription(), ENT_QUOTES, 'Windows-1251'),
false,
300,
80,
)
. $PHPShopGUI->setHelp(
'Текст над суммой и кнопкой оплаты на странице «Заказ оформлен» и в личном кабинете. Пусто — без текста',
),
);
$Tab1 .= $PHPShopGUI->setField(
'Фискализация чеков',
$PHPShopGUI->setCheckbox('fiscalization_new', '1', '', $settings->isFiscalization() ? 1 : 0)
. $PHPShopGUI->setHelp('Фискализация чеков по 54-ФЗ средствами системы МОНЕТА сервисом kassa.payanyway.ru'),
);
// Поля чека для кассы видны только при включённой фискализации; значения
// сохраняются и при скрытых полях.
$fiscal = 'moneta-fiscal';
if ($settings->isFiscalization() && $settings->getReceiptEmail() === '') {
$Tab1 .= $PHPShopGUI->setAlert(
HostCharset::toPage(
'Фискализация включена, а «Email для чеков» не заполнен. Укажите адрес: на него придёт чек,'
. ' если у покупателя нет ни email, ни телефона.',
),
'warning',
false,
);
}
$Tab1 .= $PHPShopGUI->setField(
'Email для чеков',
$PHPShopGUI->setInput(
'email',
'receipt_email_new',
htmlspecialchars($settings->getReceiptEmail(), ENT_QUOTES, 'Windows-1251'),
false,
300,
)
. $PHPShopGUI->setHelp(
'Подставляется в чек, если у покупателя нет ни email, ни телефона — чек придёт на этот адрес, а ФИО покупателя в чеке сохранится',
),
1,
null,
$fiscal,
);
$Tab1 .= $PHPShopGUI->setField(
'Система налогообложения',
$PHPShopGUI->setSelect(
'sno_new',
SelectOptions::fromLabels(array_map($page, TaxSystem::LABELS), $settings->getTaxSystem()->getValue()),
300,
)
. $PHPShopGUI->setHelp('Если не передавать, касса возьмёт систему налогообложения из своих настроек'),
1,
null,
$fiscal,
);
// Ставки PHPShop не из перечня кассы (свободное число, например опечатка 23): позиции с ними
// уходят в чек «без НДС», о каждой такой позиции модуль пишет и в журнал PHP.
$vatWarnings = [];
try {
$vatRates = new HostVatRates(Database::fromHost());
$vatWarnings = VatRateWarnings::find(
$settings->isFiscalization(),
$vatRates->isEnabled(),
$settings->getGoodsVat(),
$settings->getDeliveryVat(),
$vatRates->systemPercent(),
$vatRates->enabledDeliveries(),
);
} catch (\Throwable $exception) {
(new ErrorLogLogger())->error('Ставки НДС PHPShop не проверены: ' . $exception->getMessage());
}
if ($vatWarnings !== []) {
$escape = static function (string $text): string {
return htmlspecialchars($text, ENT_QUOTES, 'Windows-1251');
};
$Tab1 .= $PHPShopGUI->setAlert(
HostCharset::toPage(
'Ставки НДС не из перечня кассы — эти позиции уходят в чек «без НДС»: '
. implode('; ', array_map($escape, $vatWarnings))
. '. Исправьте «Значение НДС» в разделе «Настройка цен» или в способе доставки'
. ' либо выберите ставку в полях ниже.',
),
'warning',
false,
);
}
$Tab1 .= $PHPShopGUI->setField(
'НДС на товары',
$PHPShopGUI->setSelect(
'goods_vat_new',
SelectOptions::fromLabels(
array_map($page, VatSetting::labels('Из настроек раздела «Настройка цен»')),
$settings->getGoodsVat()->getValue(),
),
300,
)
. $PHPShopGUI->setHelp('Одна ставка на все товары или «Значение НДС» из раздела «Настройка цен»'),
1,
null,
$fiscal,
);
$Tab1 .= $PHPShopGUI->setField(
'НДС на доставку',
$PHPShopGUI->setSelect(
'delivery_vat_new',
SelectOptions::fromLabels(
array_map($page, VatSetting::labels('Из настроек способа доставки')),
$settings->getDeliveryVat()->getValue(),
),
300,
)
. $PHPShopGUI->setHelp('Одна ставка на доставку или «Значение НДС» способа доставки.'),
1,
null,
$fiscal,
);
$Tab1 .= $PHPShopGUI->setField(
'Признак способа расчёта',
$PHPShopGUI->setSelect(
'payment_method_new',
SelectOptions::fromLabels(array_map($page, PaymentMethod::LABELS), $settings->getPaymentMethod()->getValue()),
300,
)
. $PHPShopGUI->setHelp('У всех позиций чека, включая доставку'),
1,
null,
$fiscal,
);
$Tab1 .= $PHPShopGUI->setField(
'Признак предмета расчёта',
$PHPShopGUI->setSelect(
'payment_object_new',
SelectOptions::fromLabels(array_map($page, PaymentObject::LABELS), $settings->getPaymentObject()->getValue()),
300,
)
. $PHPShopGUI->setHelp('У товаров; доставка в чеке — всегда «Услуга»'),
1,
null,
$fiscal,
);
// Скрытое поле email с ошибкой браузер не дал бы отправить, а подсказку не показал бы —
// на время скрытия оно становится текстовым и необязательным (адрес всё равно проверяет
// actionUpdate()); при фискализации «Email для чеков» обязателен.
$Tab1 .= '';
$Tab1 .= $PHPShopGUI->setField(
'Оплата при статусе',
$PHPShopGUI->setSelect(
'status_new',
OrderStatusOptions::build($statuses, $settings->getPayableStatus(), $page('Новый заказ'), Settings::STATUS_NEW),
300,
),
);
$Tab1 .= $PHPShopGUI->setField(
'Статус после оплаты',
$PHPShopGUI->setSelect(
'paid_status_new',
OrderStatusOptions::build(
$statuses,
$settings->getPaidStatus(),
$page('Оплачено платежными системами'),
Settings::STATUS_PAID_BY_GATEWAY,
),
300,
),
);
// Только символы cp1251: сборка перекодирует файл. Панель собирается здесь, а не через
// setInfo(): тот на UTF-8-базе пропускает блок через __() со strip_tags и вырезает таблицу.
$link = static function (string $url, string $text): string {
return '' . $text . '';
};
$kassa = $link('https://kassa.payanyway.ru/', 'kassa.payanyway.ru');
$accountRows = [
['Псевдоним', 'любой, например Монета'],
['Тестовый режим', 'как в настройке модуля'],
['Check URL и Pay URL', '' . $callbackUrl . ''],
['HTTP-метод (Check URL, Pay URL)', 'POST или GET'],
[
'Код проверки целостности данных',
'произвольный набор символов; тот же код укажите в настройках модуля',
],
['Подпись формы оплаты обязательна', 'Да'],
['Можно переопределять настройки в URL', 'Да'],
['Success URL', 'оставить пустым'],
['Fail URL', 'оставить пустым'],
['InProgress URL', 'оставить пустым'],
['Return URL', 'оставить пустым'],
];
$accountTable = '';
foreach ($accountRows as [$field, $value]) {
$accountTable .= '
' . $link('https://payanyway.ru/partnerRegistration.htm', 'Зарегистрируйтесь') . '
в платёжной системе PayAnyWay и заполните все необходимые данные. Дождитесь проверки аккаунта
и создайте расширенный счёт.
Заполните настройки расширенного счёта в личном кабинете: «Мой счёт» -> «Управление
счетами» -> «Редактировать счёт» -> «Показать дополнительные поля».
Поле в личном кабинете
Значение
' . $accountTable . '
На вкладке «Основное» укажите номер счёта и тот же код проверки целостности данных.
На демо-площадке demo.moneta.ru номер счёта свой.
Для фискализации чеков по 54-ФЗ средствами за рамками PHPShop настройте вашу кассу в сервисе
' . $kassa . ', в настройках расширенного счёта в MONETA.RU установите Pay URL
https://kassa.payanyway.ru/index.php?do=invoicepayurl, а в настройках кассы
пропишите Pay URL вашего интернет-магазина ' . '' . $callbackUrl . '' . '. В этом случае чек по 54-ФЗ
будет пробиваться через ' . $kassa . ', а запрос на Pay URL магазина будет проходить
через этот сервис транзитом.
Важно! Для кириллического домена Check URL и Pay URL укажите
в кодировке ' . $link('https://2ip.ru/punycode/', 'Punycode') . '.
Выключение модуля
Выключение модуля в PHPShop удаляет его таблицы: настройки придётся ввести заново.