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 ''; } 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 .= '' . $field . '' . $value . ''; } $info = '

Настройка счёта в личном кабинете ' . $link('https://moneta.ru/', 'MONETA.RU') . '

  1. ' . $link('https://payanyway.ru/partnerRegistration.htm', 'Зарегистрируйтесь') . ' в платёжной системе PayAnyWay и заполните все необходимые данные. Дождитесь проверки аккаунта и создайте расширенный счёт.
  2. Заполните настройки расширенного счёта в личном кабинете: «Мой счёт» -> «Управление счетами» -> «Редактировать счёт» -> «Показать дополнительные поля». ' . $accountTable . '
    Поле в личном кабинетеЗначение
  3. На вкладке «Основное» укажите номер счёта и тот же код проверки целостности данных. На демо-площадке demo.moneta.ru номер счёта свой.
  4. Для фискализации чеков по 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 удаляет его таблицы: настройки придётся ввести заново.

'; $Tab2 = '
' . HostCharset::toPage($info) . '
'; $Tab3 = $PHPShopGUI->setPay(false, false, $data['version'], true); $PHPShopGUI->setTab(['Основное', $Tab1, true], ['Инструкция', $Tab2], ['О модуле', $Tab3]); $ContentFooter = $PHPShopGUI->setInput('hidden', 'rowID', $data['id']) . $PHPShopGUI->setInput('submit', 'saveID', 'Применить', 'right', 80, '', 'but', 'actionUpdate.modules.edit'); $PHPShopGUI->setFooter($ContentFooter); return true; } // Обработка событий $PHPShopGUI->getAction(); // Вывод формы при старте $PHPShopGUI->setLoader($_POST['saveID'] ?? null, 'actionStart');