Skip to content

ТЗ — Крипто-платежи (cms/crypto-payments)

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

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

Приём криптовалютных платежей как альтернативного способа оплаты заказа: генерация адреса/инвойса, мониторинг подтверждений в блокчейне, фиксация курса на момент выставления инвойса. Реализует контракт PaymentGateway из cms/commerce-payments.

  • Генерация уникального адреса/инвойса на заказ через cms/integrations-bus
  • provides: payment-gateway — реализация контракта PaymentGateway
  • Мониторинг подтверждений в очереди (порог confirmations настраивается на валюту)
  • Фиксация курса на момент создания инвойса с TTL (после истечения — курс пересчитывается)
  • Сценарий недоплаты: заказ помечается частично оплаченным, ожидает доплаты или ручного решения
  • Сценарий переплаты: излишек фиксируется как баланс/возврат по решению администратора
  • Журнал транзакций блокчейна, привязанных к инвойсу

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

requires: cms/integrations-bus, cms/commerce-payments · provides: payment-gateway

Поведение при выключении: криптовалюта пропадает из списка активных способов оплаты на checkout, ранее созданные неоплаченные инвойсы помечаются истёкшими — оформление заказа продолжает работать через оставшиеся способы оплаты.

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

ТаблицаКлючевые поляПримечание
cms_crypto_payments_invoicesid, order_id, currency_code, address, amount_crypto, rate_fixed, rate_ttl_at, statusинвойс на заказ, курс фиксирован на TTL
cms_crypto_payments_transactionsid, invoice_id, tx_hash, confirmations, amount_receivedтранзакции блокчейна по инвойсу

Индексы: FK order_id/invoice_idconstrained() + index(); status — PHP Enum; amount_crypto/amount_received — integer minor units (сатоши/wei-подобные) с currency_code, float запрещён; cms_crypto_payments_transactions — append-only, BRIN по created_at; tx_hash — уникальный индекс (идемпотентность по транзакции).

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

Входы

ВходИсточникКаналПроверка
Создание инвойсаPOST /api/v1/checkout/payments/crypto (форма checkout)APIFormRequest whitelist (order_id, currency_code); Idempotency-Key обязателен
Ручное решение по инвойсуPOST /api/v1/admin/crypto-payments/{id}/resolveAPIcrypto-payments.manage, whitelist действий (accept_underpayment, refund_overpayment, reject)
Транзакция блокчейнаВебхук провайдера/ноды → cms/webhooks-in, либо плановый опрос из очередиКанал 5Подпись провайдера (где поддерживается) или подтверждение из независимого опроса ноды; идемпотентность по tx_hash
Курс валютыЗапрос к оракулу/бирже через cms/integrations-busВнешний канал (синхронный, при создании инвойса)Таймаут ≤2 с, fallback-источник курса при недоступности основного

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

Выходы

ВыходФорматПолучатель
Ответ создания инвойса{"data": {"invoice_id", "address", "amount_crypto", "rate_fixed", "rate_ttl_at"}}Checkout (клиент)
Статус инвойса{"data": {"status", "confirmations", "confirmations_required"}}Клиент/личный кабинет (поллинг)
CryptoInvoiceCreated / CryptoPaymentConfirmed / CryptoPaymentUnderpaidканал 1 (событие)cms/audit, подписчики ядра
Реализация PaymentGateway (pay/capture/refund/status/callback)канал 3 (provides: payment-gateway)cms/commerce-payments
Вызов PaymentsService::confirm()канал 4 (requires)cms/commerce-payments

Настройки (группа crypto-payments)

КлючТипДефолтaffectsPageCacheОписание
crypto-payments.enabled_currenciesarray[]нетАктивные криптовалюты (btc, usdt-trc20 и т.п.)
crypto-payments.confirmations_requiredarray{}нетПорог подтверждений на валюту
crypto-payments.rate_ttl_minutesint15нетВремя жизни зафиксированного курса инвойса
crypto-payments.underpayment_tolerance_percentint1нетДопустимое отклонение недоплаты без ручной проверки
crypto-payments.max_invoice_amountint (minor units)3000000нетМаксимальная сумма одного инвойса в валюте расчёта
crypto-payments.max_active_invoices_per_orderint3нетЛимит одновременных неоплаченных инвойсов на заказ (защита от спама адресов)
crypto-payments.aml_kill_switcharray[]нетKill-switch: валюты, приём которых аварийно приостановлен (AML-инцидент/санкционный риск) без выключения модуля

Секреты доступа к блокчейн-провайдеру/нодам — только .env.

Стоимость внешних API: комиссия сети (gas/miner fee) в конфиге не фиксируется — динамическая, приходит в ответе провайдера/оракула на момент создания инвойса и видна администратору в журнале транзакций конкретного инвойса; модуль её не гарантирует и не компенсирует клиенту при колебаниях.

API

МетодПутьДоступНазначение
POST/api/v1/checkout/payments/cryptoauth/guest (Idempotency-Key)Создание крипто-инвойса на заказ
GET/api/v1/payments/crypto/{id}auth (владелец)Статус инвойса и число подтверждений
GET/api/v1/admin/crypto-paymentsadmin (crypto-payments.view)Список инвойсов с фильтрами по статусу
POST/api/v1/admin/crypto-payments/{id}/resolveadmin (crypto-payments.manage)Ручное решение по недоплате/переплате

Список инвойсов в админке — keyset-пагинация, не OFFSET.

Компоненты

Filament: ресурс инвойсов (статус, подтверждения, курс), форма ручного решения по недоплате/переплате, журнал транзакций по инвойсу. Команды: cms:crypto-payments:poll-confirmations --json (опрос блокчейна из очереди по расписанию), cms:crypto-payments:doctor --json (доступность провайдера/ноды на валюту).

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

СобытиеКогдаPayload
CryptoInvoiceCreatedинвойс создан, курс зафиксированinvoice_id, order_id, currency_code, rate_fixed
CryptoPaymentConfirmedдостигнут порог подтвержденийinvoice_id, order_id, tx_hash
CryptoPaymentUnderpaidполучена сумма меньше требуемойinvoice_id, amount_received, amount_expected

Реализует provides: payment-gateway — потребитель cms/commerce-payments. Обмен с блокчейн-провайдером — только через cms/integrations-bus.

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

Сущность/модульКаналНаправлениеЧто происходит
cms/webhooks-in5 (событие)webhooks-in → crypto-paymentsПриём вебхука о поступлении транзакции (если провайдер поддерживает), издание WebhookReceived
cms/commerce-payments3 (provides)crypto-payments → commerce-paymentsРеализация PaymentGateway
cms/commerce-payments4 (requires)crypto-payments → commerce-paymentsВызов PaymentsService::confirm() по достижении порога подтверждений
cms/integrations-busвнешний каналcrypto-payments ↔ блокчейн-провайдер/оракулОпрос ноды/API курса, все внешние вызовы — через шину
cms/audit1 (событие)crypto-payments → auditРучное решение по недоплате/переплате, срабатывание AML kill-switch
cms/health1/метрикиcrypto-payments → healthНедоступность ноды/оракула, отставание очереди опроса подтверждений

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

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

Опрос подтверждений блокчейна — именованная очередь crypto-payments, по расписанию (не по запросу пользователя); ретраи с backoff при недоступности провайдера, circuit breaker на источник данных блокчейна. Создание инвойса — синхронный вызов с таймаутом и graceful fallback (ошибка «попробуйте позже», не 500).

Мини-ранбук

СимптомЧто проверитьКоманда
Подтверждения не растут ни у одного инвойсаДоступность ноды/провайдера, circuit breaker, отставание очереди crypto-paymentscms:crypto-payments:doctor --json
Инвойс завис в «ожидании» дольше обычногоrate_ttl_at/confirmations инвойса, журнал его транзакцийcms:crypto-payments:poll-confirmations --json для конкретного invoice_id
Расхождение суммы (недоплата/переплата) не разрешено вовремяСписок инвойсов со статусом «требует решения»GET /api/v1/admin/crypto-payments?filter[status]=needs_review

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

  • Ожидаемые объёмы (оценочно): крипта — нишевый способ оплаты, единицы-десятки инвойсов в сутки на сайт; журнал транзакций растёт по мере доначислений (несколько записей на инвойс при недоплате/доплате).
  • Горячие пути: опрос блокчейна — плановая джоба по расписанию, пачками (не по запросу пользователя), бюджет — таймаут ≤2 с на запрос к ноде/провайдеру с backoff; статус инвойса на фронте — поллинг клиента по индексированному invoice_id, без тяжёлых джойнов.
  • Создание инвойса (курс + адрес) — синхронный вызов с таймаутом ≤2 с и graceful fallback («попробуйте позже») — допустимое исключение §9 стандарта, бюджет фиксирован явно, а не «как получится».
  • Индексы — см. «Модель данных»: order_id/invoice_id (FK + index), tx_hash — уникальный индекс (идемпотентность), rate_ttl_at — для выборки инвойсов с истёкшим курсом, status — для admin-списка.
  • Собственных тегов кеша нет — статус инвойса и число подтверждений читаются напрямую (критично для точности оплаты, не кешируются).

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

  • Опрос подтверждений идемпотентен по tx_hash — повторный опрос той же транзакции не дублирует событие и не создаёт повторный переход статуса. Дедупликация на двух уровнях: cms/webhooks-in — по event_id вебхука (если провайдер его даёт), и сам модуль — по уникальному индексу tx_hash в cms_crypto_payments_transactions.
  • Переплата не завершает платёж автоматически — обязательное ручное решение администратора (POST /resolve): излишек не зачисляется и не возвращается сам по себе, это защита от автоматического злоупотребления и ошибок курса.
  • Защита от replay: опрос ноды — не вебхук, replay в классическом смысле неприменим (источник истины — сама транзакция в блокчейне); для провайдеров с вебхуком о поступлении — подпись провайдера + окно по timestamp там, где он его даёт.
  • Секреты доступа к блокчейн-API/нодам — только .env.
  • Права: crypto-payments.view, crypto-payments.manage.

ПДн-паспорт: модуль не хранит ФИО/email/телефон — только order_id (связь с заказом в commerce-orders) и адрес блокчейна кошелька/инвойса (публичный идентификатор сети, не персональные данные по 152-ФЗ). Ручное решение администратора по AML-риску при необходимости сверяется с данными заказа в commerce-orders — не дублируется в таблицах этого модуля.

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

ДействиеАдминМенеджерРедакторStudio
Просмотр списка инвойсов (crypto-payments.view)
Ручное решение по недоплате/переплате (crypto-payments.manage)
AML kill-switch (аварийная остановка валюты)
Правка секретов провайдера/ноды (.env)✅ (только через деплой)

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

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

Покупатель: при истечении курса до оплаты — понятное сообщение («курс устарел, создайте новый инвойс») с кнопкой пересоздания, а не зависший счёт; статус оплаты обновляется по мере подтверждений без перезагрузки страницы (поллинг); при недоступности крипто-оплаты — предложение альтернативного способа, не обрыв оформления заказа.

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

  • Порог подтверждений сети (confirmations_required на валюту) → до достижения порога инвойс в статусе «ожидает подтверждения», после — переходит в «подтверждён» и вызывает PaymentsService::confirm(); частичные подтверждения не считаются оплатой.

  • Недоплата в пределах допуска (underpayment_tolerance_percent) → засчитывается как полная оплата автоматически, без участия администратора.

  • Недоплата вне допуска → инвойс переходит в статус «требует решения», доплата разрешена (тот же адрес донасчитывает сумму) или ручное решение администратора — автозачёт запрещён.

  • Переплата → излишек не засчитывается автоматически ни в каком случае: обязательно ручное решение администратора (accept/refund) — самый частый риск ошибки в донорском коде er, поэтому вынесен отдельным правилом, а не подразумевается.

  • Курс инвойса истёк (rate_ttl_minutes) до поступления оплаты → инвойс помечается «курс устарел», оплата по старому адресу всё ещё может быть учтена вручную (не теряется физически), но клиенту предлагается создать новый инвойс с актуальным курсом; старый — недействителен для новых платежей на checkout.

  • AML-риск (подозрительный источник средств, санкционный адрес) → kill-switch (aml_kill_switch) аварийно останавливает приём валюты без выключения модуля; уже поступившие подозрительные транзакции не зачисляются автоматически — только ручная проверка администратора, событие уходит в cms/audit.

  • Недоступность блокчейн-провайдера/ноды → опрос откладывается (circuit breaker), деньги не теряются (транзакция в блокчейне не зависит от нашей доступности), статус инвойса не считается ошибкой для покупателя — только даунтайм наблюдаемости для администратора/разработчика (три аудитории ошибок, §12 стандарта): алерт в cms/health для разработчика, «ожидает подтверждения» — для покупателя.

  • Дубль вебхука о поступлении транзакции → идемпотентность по tx_hash, второй вебхук не создаёт вторую запись и не дублирует событие CryptoPaymentConfirmed.

  • Смена курса провайдера (API оракула недоступно) → 200 + meta.degraded при запросе текущего курса (курс — взаимозаменяемые данные, не привязаны к конкретному оракулу) либо fallback-источник курса (вторичный провайдер); если оба недоступны — создание нового инвойса отклоняется с понятной ошибкой, ранее созданные инвойсы с уже зафиксированным курсом не затрагиваются.

    ⚠️ Противоречие (снято ниже): как и в pay-gateways-ru.md, конвенция ядра «200 + meta.degraded» формально относится к чтению взаимозаменяемого провайдера, а создание инвойса — мутация с денежным эффектом. Разрешение: запрос курса (чтение) подчиняется 200 + meta.degraded/fallback-источнику; попытка создать инвойс, когда ни один источник курса не отвечает — предметная ошибка (503 с code: rate_provider_unavailable), а не тихий успех с фиктивным курсом — иначе клиент оплачивает по курсу, который никто не подтверждал.

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

Что взятьПуть
Биллинг-домен, работа со статус-машиной платежаer/project/src/app/Domain/Billing/

Legacy-импорт: донор er хранит биллинг-домен с историей инвойсов/транзакций — применимо. Команда cms:crypto-payments:import-legacy --source=er --dry-run маппит старые записи инвойсов/транзакций на cms_crypto_payments_invoices/_transactions, идемпотентна по tx_hash (естественный внешний ключ транзакции повторный прогон не дублирует), отчёт расхождений — обязателен перед боевым прогоном.

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

  • [ ] Мок блокчейн-API: тест создания инвойса, накопления подтверждений, финализации оплаты
  • [ ] Курс инвойса не пересчитывается до истечения rate_ttl_minutes (тест фиксации)
  • [ ] Недоплата в пределах underpayment_tolerance_percent засчитывается как полная оплата
  • [ ] Переплата не завершает платёж автоматически — требует решения администратора
  • [ ] Опрос подтверждений идемпотентен — повторный опрос той же транзакции не дублирует событие
  • [ ] При выключении модуля неоплаченные инвойсы истекают без падения checkout
  • [ ] Права crypto-payments.manage разграничивают просмотр и ручное решение по инвойсу
  • [ ] Контрактный тест деградации: недоступность оракула курса → meta.degraded/fallback-источник при чтении курса, предметная ошибка (rate_provider_unavailable) при попытке создать инвойс без курса
  • [ ] Дубль вебхука о поступлении транзакции (тот же tx_hash) не создаёт вторую запись и не дублирует событие
  • [ ] AML kill-switch аварийно останавливает приём валюты без выключения модуля (тест на конкретную валюту, остальные не затронуты)
  • [ ] Контрактный набор cms-testing зелёный, пакет тестируется в testbench-изоляции
  • [ ] Feature-тест на каждый роут; тестовая БД только crypto_payments_test, migrate:fresh запрещён

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