Sale 26.450.0: «Сбербанк Онлайн» переехал на новый шлюз и сломал наследников
Ломающее обновление
Удалены или изменены публичные API — прикладной код может перестать работать.
Весь sale 26.450.0 посвящён одному обработчику: SberbankOnlineHandler переписан с нуля под новый шлюз Сбера. Схема БД и ядро модуля не тронуты, дифф небольшой (11 файлов, +1195/−604), и магазинам без Сбера и Альфы можно обновляться спокойно. Если же магазин принимает оплату через Сбер или у вас в /local/ живёт наследник этого обработчика, после обновления надо руками проверить, что деньги и уведомления доходят.
Что сломается
Сбер теперь ходит на другой шлюз
Старые адреса securepayments.sberbank.ru/payment/ и 3dsec.sberbank.ru/payment/ из обработчика ушли. Запросы идут на https://epay.sberbank.ru/ecomm/gw/partner/api/v1/<action>, в тестовом режиме на ecomtest.sberbank.ru. Покупателя отправляют платить на https://payecom.ru/pay_ru?orderId=... (тест: sbox.payecom.ru). Действий три: register.do, getOrderStatusExtended.do и refund.do, тело запроса уходит как JSON.
Сервер магазина должен уметь достучаться до новых хостов. Если исходящие соединения у вас закрыты файрволом по белому списку, в нём прописаны старые адреса, и регистрация заказа начнёт падать.
Секретного ключа больше нет, подпись колбэка не проверяется
Из .description.php удалена настройка SBERBANK_SECRET_KEY, а вместе с ней метод isCheckSumCorrect(). Поле пропадёт из формы платёжной системы в админке. Сохранённое значение при этом никто не чистит: миграции в диффе нет, новый код его не читает.
Подлинность уведомления теперь проверяется иначе. processRequest() сверяет mdOrder с PS_INVOICE_ID платежа, номер заказа, operation === 'deposited' и status = 1. Встречный запрос в банк был и раньше, старый processRequest() тоже ходил в getOrderStatus(). Сумму по его ответу сверяли и раньше. Теперь к ней добавились номер заказа, валюта, mdOrder в атрибутах ответа getOrderStatusExtended.do и orderStatus = 2. Если запрос не прошёл или хоть одно поле не совпало, заказ останется неоплаченным, даже когда деньги списались.
Уведомление принимается только как POST с JSON
Старый обработчик читал параметры через $request->get('operation'), get('mdOrder') и так далее. Новый приватный getNotification() принимает только POST с JSON-телом. На всё остальное он возвращает пустой массив, и обработка на этом заканчивается.
Если колбэк приходит GET-запросом или POST-формой, а не JSON, isMyResponse() вернёт false и уведомление не обработается. Статусы оплат перестанут обновляться, и менеджеры увидят «оплаченные, но не оплаченные» заказы. Самописный прокси или тестовый скрипт, который дёргает URL уведомления, тоже придётся переделать под JSON.
Наследники SberbankOnlineHandler упадут
Из класса удалены:
- константы
PAYMENT_OPERATION_DEPOSITED,PAYMENT_STATUS_SUCCESS,RESPONSE_CODE_SUCCESS,PAYMENT_STATE_CREATED,PAYMENT_DELIMITER; - методы
isCheckSumCorrect(),registerOrder(),getMerchantParams(),getRegisterOrderParams(),getDescriptionCode(),getDescriptionCodesMap(); - переопределения
getIndicativeFields()иisMyResponseExtended(). Базовые версии вServiceHandlerостались (возвращают[]иtrue), но новыйpublic static function isMyResponse(Request $request, $paySystemId): boolих не вызывает, поэтому переопределять их в наследнике бесполезно.
Класс в /local/php_interface/include/sale_payment/, который наследует SberbankOnlineHandler и, например, переопределяет getRegisterOrderParams(), чтобы дописать свои поля в заказ, после обновления поведёт себя плохо в любом варианте. Обращение к удалённой константе или вызов parent::registerOrder() даст фатал. Переопределённый метод, который родитель больше не вызывает, перестанет работать, и ваши доработки молча исчезнут из запроса к банку.
У методов появились возвращаемые типы: static isMyResponse(Request $request, $paySystemId): bool (раньше класс наследовал его от ServiceHandler, теперь переопределяет сам), getPaymentIdFromRequest(Request $request): string, getUrlList(): array, getOrderDescription(Payment $payment): string. У isMyResponse() в базовом ServiceHandler типа нет, поэтому наследник, который переопределял его по старой сигнатуре без : bool, тоже упадёт. Переопределение без типа или с другим типом даёт фатал несовместимости сигнатур, и падает он при подключении файла, ещё до оплаты.
Рабочее переопределение теперь выглядит так:
<?php declare(strict_types=1);
namespace Sale\Handlers\PaySystem;
use Bitrix\Main\Loader;
use Bitrix\Sale\Payment;
use Bitrix\Sale\PaySystem;
Loader::includeModule('sale');
PaySystem\Manager::includeHandler('SberbankOnline');
final class ShopSberbankHandler extends SberbankOnlineHandler
{
protected function getOrderDescription(Payment $payment): string
{
$accountNumber = (string)$payment->getOrder()->getField('ACCOUNT_NUMBER');
return 'Оплата заказа №' . $accountNumber;
}
}
Альфа-Банк больше не Сбер
Раньше AlfaBankHandler наследовался от SberbankOnlineHandler. Теперь у него новый родитель AlfabankLegacyHandler, файл alfabank/legacyhandler.php подключается прямым require_once. По составу методов и констант это прежняя реализация Сбера: её вынесли отдельно, чтобы Альфа продолжила работать по старому протоколу.
Для самой оплаты через Альфу ничего не меняется. Для Альфа-Банка $handler instanceof SberbankOnlineHandler теперь возвращает false. Если на таком условии у вас держится общая логика для «сбероподобных» платёжек (в обработчике события, в отчёте, в выгрузке), Альфа из неё тихо выпадет, без ошибок. Условие придётся расширить:
<?php declare(strict_types=1);
use Sale\Handlers\PaySystem\AlfabankLegacyHandler;
use Sale\Handlers\PaySystem\SberbankOnlineHandler;
$isBankGateway = $handler instanceof SberbankOnlineHandler
|| $handler instanceof AlfabankLegacyHandler;
Скопированный шаблон оплаты остался без защиты
В template_bank_card.php адрес формы теперь берётся из нового параметра $params['FORM_ACTION'] (URL без query-строки), а не из $params['URL']. URL по-прежнему передаётся в setExtraParams(), поэтому копия шаблона в /local/ продолжит работать.
В штатном шаблоне имена и значения hidden-полей и action формы теперь проходят через htmlspecialcharsbx(), а раньше выводились как есть. Ваша копия этого фикса не получила и выводит значения из ответа банка без экранирования. Шаблон нужно перенести заново с нового оригинала. В своём варианте вывод должен выглядеть так:
<form action="<?= htmlspecialcharsbx($params['FORM_ACTION']) ?>" method="get">
<?php foreach ($params['FORM_PARAMS'] ?? [] as $name => $value): ?>
<input type="hidden"
name="<?= htmlspecialcharsbx($name) ?>"
value="<?= htmlspecialcharsbx($value) ?>">
<?php endforeach; ?>
</form>
Новое
isPaymentUrlValid(): проверка ссылки на оплату
SberbankOnlineHandler::isPaymentUrlValid(string $url, bool $testMode): bool — публичный статический метод. Пропускает только https на порту 443, без userinfo и fragment, с хостом из белого списка: payecom.ru и epay.sberbank.ru в бою, sbox.payecom.ru и ecomtest.sberbank.ru в тесте.
Обработчик применяет его к formUrl из ответа банка: невалидная ссылка в форму не попадает, вместо неё возвращается ошибка. Метод публичный, поэтому им можно пользоваться и в своём коде. Например, перед тем как отдать ссылку на оплату в мобильное приложение или в письмо:
<?php declare(strict_types=1);
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;
use Bitrix\Sale\PaySystem\Manager;
use Sale\Handlers\PaySystem\SberbankOnlineHandler;
Loader::includeModule('sale');
Manager::includeHandler('SberbankOnline');
function checkPaymentLink(string $url, bool $testMode): Result
{
$result = new Result();
if (!SberbankOnlineHandler::isPaymentUrlValid($url, $testMode)) {
$result->addError(new Error('Ссылка на оплату ведёт не на шлюз банка', 'PAYMENT_URL'));
}
return $result;
}
createHttpClient(): фабрика клиента, которую можно переопределить
Protected-метод createHttpClient(): Web\HttpClient собирает клиент с жёсткими настройками: проверка SSL включена, редиректы отключены, socketTimeout 10 секунд, streamTimeout 20, логгер заменён на \Psr\Log\NullLogger. Это одна из немногих точек расширения нового обработчика. Если запросам к банку нужен прокси, её и надо переопределять:
protected function createHttpClient(): \Bitrix\Main\Web\HttpClient
{
$client = parent::createHttpClient();
$client->setProxy('proxy.shop.local', 3128);
return $client;
}
Коды ошибок SBERBANK_*
Ошибки обработчика теперь приходят в ServiceResult как Main\Error с кодами шести видов: SBERBANK_SETTINGS, SBERBANK_TRANSPORT, SBERBANK_NOTIFICATION, SBERBANK_PAYMENT, SBERBANK_REFUND, SBERBANK_RESPONSE. По коду сразу понятно, где искать причину: в настройках, в сети, в данных платежа или в ответе банка. Если же банк сам вернул ошибку (errorCode не равен 0), её отдаёт send() с банковским errorCode в качестве кода, без префикса SBERBANK_.
foreach ($serviceResult->getErrors() as $error) {
$code = (string)$error->getCode();
// сетевую ошибку имеет смысл повторить, ошибку настроек — нет
$isNetworkProblem = $code === 'SBERBANK_TRANSPORT';
// отказ самого банка приходит с его errorCode
$isBankDecline = !str_starts_with($code, 'SBERBANK_');
}
AlfabankLegacyHandler
Новый класс Sale\Handlers\PaySystem\AlfabankLegacyHandler на 740 строк. Это старая реализация под новым именем, со всеми прежними registerOrder(), isCheckSumCorrect(), getIndicativeFields() и getDescriptionCodesMap().
БД
Схема не менялась, db.diff пуст. Значение SBERBANK_SECRET_KEY остаётся лежать в настройках платёжной системы, хотя читать его больше некому.
Мелочи и находки
- Регистрация заказа переживает обрыв связи. Если
register.doне удался, приватныйrecoverOrder()запрашиваетgetOrderStatusExtended.doпоorderNumberи вытаскиваетmdOrderиз атрибутов ответа. - Повторная регистрация устроена отдельно. Номер заказа в банке собирается как
<ID оплаты>_<ID платёжной системы>, например123_5. Если у оплаты уже естьPS_INVOICE_ID, а заказ в банке отклонён (orderStatus6) или сумма не совпала, обработчик регистрирует его заново с третьим сегментом:123_5_1, затем123_5_2. Суффикс попыток был и раньше (getOrderNumber(…, int $attempt),ORDER_ATTEMPT_NUMBER), новое здесь лимит в 36 символов и более узкое условие: заново регистрируется только неоплаченная оплата приorderStatus6 или приorderStatus0 с несовпавшей суммой, остальные статусы даютSBERBANK_PAYMENT, а ошибка запроса статуса возвращается как есть. Номер длиннее обработчик не отправит и вернётSBERBANK_PAYMENT. Учитывайте такие номера, если сверяете оплаты с выписками и кабинетом банка. PS_SUMберётся из платежа, а не из ответа банка:getMinorAmount($payment->getSum(), ...) / 100. Добавлены приватныеmatchesPayment()иgetCurrencyCode(string $currency): ?int, вuseпоявилсяBitrix\Currency\CurrencyTable.- Отладочный лог обеднел. Вызовы
PaySystem\Logger::addDebugInfo()изprocessRequest()исчезли, HTTP-клиенту принудительно ставитсяNullLogger. Разбирать инцидент «банк прислал уведомление, а заказ не оплатился» стало сложнее, в привычном месте ответа шлюза больше нет. - Редиректы у HTTP-клиента отключены, так что увести запрос на сторонний хост через 302 не получится.
- В языковых файлах
sberbankonline/lang/{ru,en}удалены фразыSALE_HPS_SBERBANK_SECRET_KEYиSALE_HPS_SBERBANK_SECRET_KEY_DESC, добавленыSALE_HPS_SBERBANK_ERROR_PAYMENT,…_ERROR_REFUNDи…_ERROR_RESPONSE(по коду в эталоне). - В
version.phpпропала завершающая запятая и пустая строка после<?php. Версия 26.450.0 датирована 2026-09-11.
Что делать
- Перед обновлением найдите в
/local/наследниковSberbankOnlineHandler, подойдёт grep поextends SberbankOnlineHandler. Сверьте их со списком удалённых методов и констант, добавьте возвращаемые типы в переопределения. - Там же найдите
instanceof SberbankOnlineHandlerиgetIndicativeFields. Первое больше не ловит Альфа-Банк, аgetIndicativeFields()у Сбера теперь возвращает пустой массив. - Если шаблон
template_bank_card.phpскопирован в/local/, перенесите его заново с обновлённого оригинала, сFORM_ACTIONиhtmlspecialcharsbx(). - Проверьте, что сервер видит
epay.sberbank.ruиecomtest.sberbank.ruпо 443 порту, а колбэк в кабинете Сбера приходит как POST с JSON. - После обновления проведите тестовую оплату целиком: регистрация заказа, переход на форму, возврат, смена статуса по уведомлению, возврат денег через
refund.do. Начните с тестового режима. - Поле секретного ключа в настройках платёжной системы искать не нужно, его убрали намеренно.
Устаревшие и удалённые API в этой версии
| Символ | Статус | Чем заменять |
|---|---|---|
Sale\Handlers\PaySystem\SberbankOnlineHandler::isCheckSumCorrect()
|
Удалено | — |
Sale\Handlers\PaySystem\SberbankOnlineHandler::registerOrder()
|
Удалено | — |
Sale\Handlers\PaySystem\SberbankOnlineHandler::getMerchantParams()
|
Удалено | — |
Sale\Handlers\PaySystem\SberbankOnlineHandler::getRegisterOrderParams()
|
Удалено | — |
Sale\Handlers\PaySystem\SberbankOnlineHandler::getDescriptionCode()
|
Удалено | — |
Sale\Handlers\PaySystem\SberbankOnlineHandler::getDescriptionCodesMap()
|
Удалено | — |
Sale\Handlers\PaySystem\SberbankOnlineHandler::PAYMENT_OPERATION_DEPOSITED
|
Удалено | — |
Sale\Handlers\PaySystem\SberbankOnlineHandler::PAYMENT_STATUS_SUCCESS
|
Удалено | — |
Sale\Handlers\PaySystem\SberbankOnlineHandler::RESPONSE_CODE_SUCCESS
|
Удалено | — |
Sale\Handlers\PaySystem\SberbankOnlineHandler::PAYMENT_STATE_CREATED
|
Удалено | — |
Sale\Handlers\PaySystem\SberbankOnlineHandler::PAYMENT_DELIMITER
|
Удалено | — |