Skip to content

ТЗ — Платёжные шлюзы РФ (cms/pay-gateways-ru)

Слой: 🟡 функц-модуль · Зрелость доноров: ★★★ · Донор: notal (notal/src/app/Services/ — YooKassa) Статус: ТЗ к разработке

Назначение и возможности

Коннекторы российских платёжных шлюзов (ЮKassa, Т-Банк, Робокасса, СБП) поверх cms/integrations-bus: реализуют контракт PaymentGateway из cms/commerce-payments, дают checkout единый выбор способа оплаты без привязки к конкретному провайдеру.

  • Коннекторы ЮKassa, Т-Банк (Тинькофф-эквайринг), Робокасса, СБП (динамический QR)
  • provides: payment-gateway — реализация контракта PaymentGateway для каждого шлюза
  • Создание платежа с редиректом/виджетом на стороне шлюза
  • Приём колбэков через cms/webhooks-in, обработка идемпотентна по event_id шлюза
  • Возвраты (полные и частичные) через API конкретного шлюза
  • Сохранённые методы оплаты (токены карт) — источник для cms/commerce-subscriptions
  • Передача фискальных данных чека в запросе создания платежа (если ОФД на стороне шлюза)
  • Тестовый/боевой режим на шлюз, переключаемый в настройках без правки кода

Зависимости и выключение

requires: cms/integrations-bus, cms/webhooks-in, cms/commerce-payments · suggests: cms/commerce-subscriptions, cms/ofd · provides: payment-gateway

Поведение при выключении: коды соответствующих шлюзов пропадают из списка активных на checkout в cms/commerce-payments, оформление заказа продолжает работать через оставшиеся активные способы оплаты (или офлайн-оплату) — заказы не теряются.

Модель данных

ТаблицаКлючевые поляПримечание
cms_pay_gateways_ru_connectorsid, code, driver, is_test_mode, is_activeреестр подключённых шлюзов, креды — ссылки на .env
cms_pay_gateways_ru_saved_methodsid, user_id, gateway_code, token, masked_pan, expires_atсохранённые методы оплаты для рекуррентов
cms_pay_gateways_ru_fiscal_logid, payment_id, gateway_code, receipt_payload (json), statusжурнал передачи фискальных данных в запросе

Индексы: FK user_id/payment_idconstrained() + index(); receipt_payloadjson() + cast array; cms_pay_gateways_ru_fiscal_log — журнал, append-only, BRIN по created_at; status — PHP Enum.

ПДн-паспорт: cms_pay_gateways_ru_fiscal_log.receipt_payload может содержать ФИО, email/телефон покупателя и состав чека (обязательно при send_fiscal_data, 54-ФЗ). Срок хранения — по требованиям к фискальным документам (не короче срока хранения заказа в commerce-orders); ретеншн-джоба не удаляет запись целиком, а обезличивает нефискальные поля после истечения срока. «Выгрузить всё по субъекту» — агрегируется на стороне commerce-payments по payment_id → order_id; «забыть по запросу» (UserDeleted) обезличивает receipt_payload, кроме юридически обязательных фискальных реквизитов. cms_pay_gateways_ru_saved_methods.masked_pan — не ПДн в чистом виде (маскированный номер), но токен — чувствительный секрет: не логируется, не попадает в аналитику.

Входные и выходные данные

Входы

ВходИсточникКаналПроверка
Создание платежаPOST /api/v1/checkout/payments/{gateway} (форма checkout)APIFormRequest whitelist (order_id, amount, currency, return_url); Idempotency-Key обязателен
Возврат платежаPOST /api/v1/admin/pay-gateways-ru/{code}/refundAPIpay-gateways-ru.manage, Idempotency-Key, сумма ≤ остаток к возврату
Переключение коннектораPUT /api/v1/admin/pay-gateways-ru/{code}APIpay-gateways-ru.manage, схема настроек группы
Колбэк ЮKassaВебхук шлюза → cms/webhooks-inКанал 5 (WebhookReceived)Подпись по секрету ЮKassa (Basic/HMAC), event_id, IP-диапазон ЮKassa
Колбэк Т-БанкВебхук шлюза → cms/webhooks-inКанал 5 (WebhookReceived)Token-подпись: SHA-256 от отсортированных параметров + пароль терминала
Колбэк РобокассаВебхук шлюза → cms/webhooks-inКанал 5 (WebhookReceived)Подпись MD5/SHA (OutSum:InvId:Пароль2)
Колбэк СБПВебхук агрегатора → cms/webhooks-inКанал 5 (WebhookReceived)Подпись провайдера СБП, sbp_qr_ttl_seconds для актуальности QR

Всё, что не входит в этот перечень (произвольные поля запроса, необъявленные типы колбэков, лишние HTTP-заголовки) — отклоняется на границе (422/403), whitelist- принцип §11 стандарта: перечислено — принимается, не перечислено — отвергается.

Выходы

ВыходФорматПолучатель
Ответ создания платежа{"data": {"payment_id", "redirect_url"/"qr_payload", "status"}}Checkout (клиент)
Ответ шлюзу на колбэк200 OK (тело — по протоколу конкретного шлюза)Шлюз
PayGatewayConnected / PayGatewaySavedMethodAdded / PayGatewayFiscalDataRejectedканал 1 (событие)cms/audit, подписчики ядра
Реализация PaymentGateway (pay/capture/refund/status/callback)канал 3 (provides: payment-gateway)cms/commerce-payments
Вызов PaymentsService::confirm()/refund()канал 4 (requires)cms/commerce-payments
Список подключённых шлюзов и режимовGET /api/v1/admin/pay-gateways-ruАдминка

Настройки (группа pay-gateways-ru)

КлючТипДефолтaffectsPageCacheОписание
pay-gateways-ru.enabled_connectorsarray[]нетАктивные коды шлюзов (yookassa, tbank, robokassa, sbp)
pay-gateways-ru.test_modearray[]нетКоды шлюзов, работающих в тест-режиме
pay-gateways-ru.sbp_qr_ttl_secondsint900нетВремя жизни динамического СБП-QR
pay-gateways-ru.send_fiscal_databooltrueнетПередавать чек в запросе создания платежа
pay-gateways-ru.saved_methods_enabledbooltrueнетРазрешить сохранение токенов карт
pay-gateways-ru.max_payment_attemptsint5нетЛимит попыток оплаты одного заказа (защита от перебора/абьюза)
pay-gateways-ru.max_payment_amountint (minor units)5000000нетМаксимальная сумма одного платежа на шлюз — страховка от ошибки ввода/фрода
pay-gateways-ru.emergency_disabledarray[]нетKill-switch: коды шлюзов, аварийно отключённые без выключения модуля (быстрее, чем правка enabled_connectors)

Секреты шлюзов (API-ключи, shop_id, секрет подписи колбэка) — только .envconfig/pay-gateways-ru.php, в settings-store не хранятся.

Стоимость внешних API: комиссия шлюза — тариф провайдера по договору, в конфиге не фиксируется (плавающая). Фактический процент/сумма комиссии виден в личном кабинете шлюза; если провайдер возвращает её в колбэке — попадает в fiscal_log для сверки, но модуль не гарантирует и не пересчитывает комиссию самостоятельно.

API

МетодПутьДоступНазначение
POST/api/v1/checkout/payments/{gateway}auth/guest (Idempotency-Key)Создание платежа в конкретном шлюзе
POST/api/v1/admin/pay-gateways-ru/{code}/refundadmin (pay-gateways-ru.manage, Idempotency-Key)Полный/частичный возврат платежа
GET/api/v1/payments/{id}/saved-methodsauth (владелец)Список сохранённых методов оплаты
DELETE/api/v1/payments/saved-methods/{id}auth (владелец)Удаление сохранённого метода
GET/api/v1/admin/pay-gateways-ruadmin (pay-gateways-ru.view)Список подключённых шлюзов и режимов
PUT/api/v1/admin/pay-gateways-ru/{code}admin (pay-gateways-ru.manage)Включение/выключение, переключение тест/бой

Список сохранённых методов — keyset-пагинация, не OFFSET.

Компоненты

Блоки: способ оплаты СБП-QR на checkout. Filament: ресурс коннекторов (статус, режим), журнал фискальных данных, журнал обмена по платежу. Команды: cms:pay-gateways-ru:doctor --json (проверка доступности каждого подключённого шлюза), cms:pay-gateways-ru:reconcile --json (плановая сверка реестров).

События и обмен

СобытиеКогдаPayload
PayGatewayConnectedшлюз подключён и прошёл проверку кредовgateway_code, is_test_mode
PayGatewaySavedMethodAddedпользователь сохранил метод оплатыuser_id, gateway_code, method_id
PayGatewayFiscalDataRejectedшлюз отклонил переданные фискальные данныеpayment_id, gateway_code, reason

Слушает: WebhookReceived из cms/webhooks-in (колбэки шлюзов), маршрутизация по provider. Реализует provides: payment-gateway — потребитель cms/commerce-payments. Весь обмен со шлюзами — через cms/integrations-bus.

Взаимодействия

Сущность/модульКаналНаправлениеЧто происходит
cms/webhooks-in5 (событие)webhooks-in → pay-gateways-ruПриём и проверка подписи колбэка, издание WebhookReceived, модуль слушает и маршрутизирует по provider
cms/commerce-payments3 (provides)pay-gateways-ru → commerce-paymentsРеализация PaymentGateway (pay/capture/refund/status/callback)
cms/commerce-payments4 (requires)pay-gateways-ru → commerce-paymentsВызов PaymentsService::confirm()/refund() при подтверждённом колбэке
cms/commerce-subscriptions3 (suggests)commerce-subscriptions → pay-gateways-ruЧтение сохранённых токенов карт для рекуррентных списаний
cms/ofd4 (suggests, при включении — фактический requires)pay-gateways-ru → cms/ofdЕсли ОФД вынесен отдельным модулем — чек уходит туда, а не в запрос шлюза
cms/integrations-busвнешний каналpay-gateways-ru ↔ шлюзВсе HTTP-вызовы к API шлюзов — через единую шину интеграций (лог, HTTP-клиент, ретраи)
cms/audit1 (событие)pay-gateways-ru → auditПодключение шлюза, сохранение метода оплаты, отклонение чека, аварийное отключение

Любая связь вне этой таблицы — скрытая зависимость (анти-паттерн стандарта).

Фоновая работа

Создание платежа и обработка колбэка — синхронный HTTP-вызов шлюза (не через очередь, пользователь ждёт редирект), но с коротким таймаутом и понятной ошибкой при недоступности. Возвраты — из очереди pay-gateways-ru с ретраями (backoff) и circuit-breaker на шлюз; при открытом breaker шлюз временно исключается из активных на checkout. Плановая сверка реестров с каждым шлюзом — ежедневная джоба (cms:pay-gateways-ru:reconcile), результат — отчёт расхождений в админке, без автоисправления.

Мини-ранбук

СимптомЧто проверитьКоманда
Оплата не проходит у всех клиентов на одном шлюзеHealth-чек коннектора, статус circuit breaker, доступность API шлюзаcms:pay-gateways-ru:doctor --json
Расхождение суммы заказа и колбэкаЖурнал обмена конкретного платежа, отчёт плановой сверкиcms:pay-gateways-ru:reconcile --date=<день> --json
Возврат «завис» (создан, не подтверждён шлюзом)Очередь pay-gateways-ru, failed jobs, статус возврата у шлюзаphp artisan queue:failed + cms:pay-gateways-ru:doctor --json

Производительность и кеш

  • Ожидаемые объёмы (оценочно): платежи — от десятков до нескольких тысяч в сутки на клиентский сайт студии; фискальный лог растёт 1:1 с платежами при send_fiscal_data.
  • Горячие пути: создание платежа — синхронный вызов шлюза с таймаутом ≤2 с и graceful fallback (допустимое исключение §9 стандарта, не анти-паттерн — бюджет фиксирован явно); список активных коннекторов на checkout — из кеша настроек группы, 0 запросов к БД; обработка колбэка — быстрая запись статуса, тяжёлая реакция (фискализация, письма, бонусы) — уносится в очередь событиями commerce-payments, не здесь.
  • Возвраты и плановая сверка — только из очереди pay-gateways-ru, к API шлюза не обращаются синхронно из запроса админа (кроме исключения выше — создание платежа).
  • Индексы — см. «Модель данных»: user_id/payment_id (FK + index), status (PHP Enum), BRIN по cms_pay_gateways_ru_fiscal_log.created_at.
  • Собственных тегов кеша нет — статусы платежей и возвратов читаются напрямую (низкий трафик операции, консистентность важнее скорости кеша); список активных коннекторов — в общем кеше настроек группы (0 запросов на горячем пути checkout).

Безопасность

  • Подпись колбэков каждого шлюза верифицируется на входе через cms/webhooks-in, запросы без валидной подписи отклоняются до бизнес-логики (алгоритмы — см. «Входные и выходные данные»: HMAC/Basic у ЮKassa, token-подпись у Т-Банка, MD5/SHA у Робокассы).
  • Обработка колбэка идемпотентна по event_id шлюза — повторная доставка не создаёт дублирующий переход статуса платежа.
  • Защита от replay: там, где шлюз передаёт временную метку (ЮKassa, Т-Банк) — колбэк с меткой вне окна (например ±5 минут) отклоняется; СБП/Робокасса не дают timestamp — защита исключительно идемпотентностью по event_id/InvId и коротким окном обработки очереди возвратов.
  • Секреты (API-ключи, секрет подписи) — только .env, whitelist источников колбэков по IP/домену шлюза, где это поддерживается провайдером.
  • Права: pay-gateways-ru.view, pay-gateways-ru.manage.

Матрица ролей:

ДействиеАдминМенеджерРедакторStudio
Просмотр списка шлюзов и статусов (pay-gateways-ru.view)
Переключение шлюза, тест/бой режим (pay-gateways-ru.manage)
Ручной возврат платежа
Аварийное отключение шлюза (emergency_disabled)
Правка секретов шлюза (.env)✅ (только через деплой)

UX-требования

Админ: список коннекторов показывает статус живости (последний успешный колбэк/ проверка), не только чекбокс вкл/выкл; журнал обмена доступен по конкретному платежу (запросы/колбэки, время, статус, без сырых секретов); ошибки — на человеческом языке («ЮKassa не отвечает, платежи временно недоступны», не «Error 500» или голый код шлюза); подтверждение необратимых операций — ручной возврат и переключение бой/тест режима (может сорвать реальные платежи) требуют модального подтверждения.

Покупатель: при ошибке оплаты (таймаут/отказ шлюза) корзина и данные заказа сохраняются, показывается понятное сообщение и предложение выбрать другой способ оплаты, а не пустая ошибка; редирект на шлюз — с индикатором ожидания (спиннер), не белый экран, в рамках бюджета ≤2 с; для СБП-QR — видимый остаток времени жизни QR и кнопка «обновить QR» после истечения.

Крайние случаи и типовые баги

  • Двойной колбэк (тот же event_id доставлен дважды) → идемпотентность по event_id, второй колбэк не создаёт второй переход статуса платежа, шлюзу отвечаем 200 в обоих случаях.

  • Колбэк на несуществующий/чужой order_id (устаревший заказ, тестовые данные просочились в бой) → лог + алерт в cms/health, платёж не создаётся; шлюзу отдаём 200 (иначе он будет бесконечно ретраить), инцидент виден в журнале обмена админки.

  • Расхождение суммы колбэка с суммой заказа → алерт администратору, не автоподтверждение платежа: это принципиально — сумма не единственный источник истины, расхождение может быть багом интеграции или попыткой мошенничества; платёж переходит в статус «требует проверки», а не «оплачен».

  • Плановая сверка реестров с шлюзом (ежедневно, cms:pay-gateways-ru:reconcile) → расхождения видны в админке отдельным списком, без автоисправления.

  • Возврат (полный/частичный) → идемпотентность по Idempotency-Key: повторный запрос возврата с тем же ключом не списывает средства дважды, возвращает результат первого запроса.

  • СБП QR протух (истёк sbp_qr_ttl_seconds) → новый QR выдаётся по явному запросу клиента, старый недействителен и отклоняется шлюзом при попытке оплаты.

  • Таймаут при создании платежа → ретрай с тем же Idempotency-Key не создаёт дублирующий платёж — сервис возвращает уже созданный платёж по ключу идемпотентности.

  • Смена схемы ответа API шлюза (breaking change у провайдера) → парсинг ответа падает предсказуемо: платёж помечается «требует проверки», circuit breaker открывается на этот шлюз, алерт разработчику через cms/health — не молчаливая порча данных.

  • Rate-limit провайдера (чаще всего — на возвраты) → очередь pay-gateways-ru не долбит API повторно, backoff между попытками, при исчерпании ретраев — failed job + алерт, не бесконечный цикл.

  • Недоступность конкретного шлюза (или всех сразу) → checkout деградирует до оставшихся активных способов оплаты; если недоступный шлюз — единственный активный, показывается явная ошибка «оплата временно недоступна», а не пустой список способов.

    ⚠️ Противоречие (снято ниже): конвенция ядра «200 + meta.degraded, не 5xx» для взаимозаменяемого провайдера (ревизия контрактов core.md, п.2) формально описана для чтения (поиск, курсы, подсказки), а платёж создаётся явным выбором конкретного шлюза пользователем — это мутация, а не auto-selected чтение. Разрешение: список доступных способов оплаты на checkout (чтение) подчиняется конвенции 200 + meta.degraded — недоступный шлюз просто не попадает в список; попытка создать платёж на уже недоступном/отключённом шлюзе — предметная ошибка (409/503 с code: gateway_unavailable), а не тихий 200 с фиктивным успехом. Это одно и то же поведение с крипто-платежами (см. crypto-payments.md).

Донорский код

Что взятьПуть
Интеграция ЮKassa (создание платежа, колбэки, возвраты)notal/src/app/Services/

Legacy-импорт: собственные таблицы модуля (connectors, saved_methods, fiscal_log) — служебные, историю платежей не хранят; исторические платежи и их статусы принадлежат cms/commerce-payments/commerce-orders и импортируются их cms:commerce-payments:import-legacy. Токены сохранённых карт (saved_methods.token) — не переносятся импортом: токен привязан к merchant-аккаунту конкретного шлюза, при смене учётной записи провайдера токен теряет силу — клиентам предлагается пересохранить карту при первой новой оплате.

Тесты и приёмка

  • [ ] Контрактный тест: каждый коннектор реализует PaymentGateway из cms/commerce-payments
  • [ ] Мок внешнего API шлюза — тест создания платежа, возврата и обработки колбэка
  • [ ] Колбэк с повторным event_id не создаёт дублирующий переход статуса (идемпотентность)
  • [ ] Ретраи создания платежа при таймауте шлюза не создают дублирующий платёж
  • [ ] При недоступности шлюза (circuit breaker открыт) checkout деградирует до оставшихся активных способов оплаты
  • [ ] СБП-QR становится недействительным после истечения sbp_qr_ttl_seconds (тест)
  • [ ] Права pay-gateways-ru.manage разграничивают просмотр и переключение тест/бой режима
  • [ ] Контрактный тест деградации: создание платежа на недоступном шлюзе возвращает предметную ошибку (code: gateway_unavailable), список способов на checkout — 200 без выбывшего шлюза
  • [ ] Дубль вебхука (тот же event_id, разное тело) обрабатывается один раз, второй ответ — 200 без побочных эффектов
  • [ ] Расхождение суммы колбэка с суммой заказа не подтверждает платёж автоматически (тест на алерт + статус «требует проверки»)
  • [ ] Плановая сверка реестров находит внесённое расхождение на синтетических данных
  • [ ] Контрактный набор cms-testing зелёный, feature-тест на каждый роут; тестовая БД только pay_gateways_ru_test, migrate:fresh запрещён

Внутренняя база знаний студии