Интеграция оплаты: YooKassa¶
Что оплачивается¶
Есть два независимых продукта:
- подписка KarmaCode — доступ к глубокому разбору, маршруту, прогнозам и трекеру;
- консультация практика — разовая покупка поверх подписки с комиссией платформы.
Они не должны использовать одну «универсальную» запись без типа: различаются продавец, чек, возврат, entitlement и юридическая схема.
Тарифы подписки¶
Домен и API сразу поддерживают несколько Product и несколько версий Price на продукт. В MVP seed создаёт один
активный месячный тариф в RUB без trial; его точная сумма является конфигурацией каталога, а не константой в коде.
Одновременно может продаваться только явно активная версия цены, но старые orders/subscriptions продолжают ссылаться
на ту версию, по которой были созданы. Так добавление второго тарифа не потребует переделки заказов, entitlement или
истории платежей.
Adapter и источник истины¶
billing обращается к PaymentProvider interface. Для YooKassa MVP разумно использовать небольшой HTTP adapter на
встроенном fetch, сверенный с их автоматически обновляемой OpenAPI-спецификацией; официального Node.js SDK в списке
серверных SDK нет, а случайный community SDK не должен определять доменную модель.
Состояние в нашей БД — операционная проекция, но факт проведения/возврата подтверждает YooKassa. Страница return_url
никогда не выдаёт доступ сама: она показывает processing, пока webhook или проверочный GET не подтвердит статус.
Первичная подписка¶
- Клиент выбирает активную версию тарифа; backend создаёт
OrderиPaymentAttemptс неизменяемыми суммой, валютой RUB, описанием и receipt lines. - Backend формирует UUID idempotency key, сохраняет его до вызова и делает
POST /v3/paymentsсcapture: true, redirect confirmation, metadata внутренних ID иsave_payment_method: true. - YooKassa возвращает provider payment ID и confirmation URL; секретный ключ никогда не уходит клиенту.
- После
payment.succeededbackend делает контрольный GET объекта, сравнивает shop, status, amount, currency и metadata, сохраняет только providerpayment_method.idи признакsaved, активирует subscription/entitlement. - Пользователь заранее видит период, сумму, правила автопродления и способ отмены; согласие фиксируется отдельно.
Официальный сценарий сохранения способа описан в документации YooKassa. Номер карты и CVC backend не принимает и не хранит.
Продление¶
Отдельный worker выбирает подписки с currentPeriodEnd в окне продления:
- атомарно создаёт один invoice на период и payment attempt с уникальным внутренним ключом;
- создаёт платёж с сохранённым
payment_method_id, без пользовательского redirect; - при успехе сдвигает период ровно от прежней границы и создаёт entitlement;
- при отказе ставит
past_due, уведомляет и повторяет по утверждённому графику; - после grace period ставит
expired, но не удаляет историю; cancelAtPeriodEndпрекращает новые списания, сохраняя доступ до оплаченной даты.
Повтор задачи не может создать второй invoice/платёж за тот же период. Смена цены создаёт новую price version; уже созданный invoice не пересчитывается.
Webhooks¶
Endpoint: POST /api/v1/webhooks/yookassa, без пользовательской auth, но с provider verification.
- HTTPS на 443/8443 и TLS 1.2+;
- проверка текущего объекта через authenticated GET; allowlist IP может быть дополнительным, но не единственным контролем;
- события минимум
payment.waiting_for_capture,payment.succeeded,payment.canceled,refund.succeeded; - дедупликация по provider/object/status и hash payload, monotonic state transition;
- raw payload и время сохраняются в
ProviderEvent, секретные/избыточные платёжные поля редактируются; - ответ 200 только после durable commit; повторная доставка безопасна;
- неизвестное валидное событие сохраняется и отвечает 200, невалидное — 4xx;
- тяжёлая downstream-работа (AI/уведомление) создаётся через outbox.
YooKassa повторяет не подтверждённые уведомления до 24 часов и рекомендует проверять статус или source IP: официальные webhooks.
Идемпотентность и ошибки¶
Для каждого POST/DELETE в YooKassa отправляется стабильный Idempotence-Key до 64 символов. Провайдер гарантирует
его обработку в пределах 24 часов, поэтому наша БД дополнительно хранит вечную бизнес-уникальность операции. При
network timeout повторяем тот же payload с тем же ключом либо делаем GET, но не создаём новый платёж.
Официальное описание: формат API и Idempotence-Key.
Сумма — Decimal(12,2) либо целые копейки с ISO currency. number/float для финансовых вычислений запрещён.
Консультации, комиссия и выплаты¶
До внешнего пилота надо выбрать договорную модель:
- Split payments: YooKassa распределяет один платёж продавцам и удерживает platform fee. Платформа должна получить соответствующий статус, а практики — подключиться как магазины. Это ближе к требованию «автосплит»: официальное описание.
- Безопасная сделка: деньги удерживаются до оказания услуги и затем выплачиваются физлицу; подходит не всем статусам и имеет отдельные лимиты/идентификацию.
- Платформа — продавец: обычный приём и отдельные расчёты с практиком. Возможность зависит от договоров, налогов, кассы и сути услуги; это не техническое решение команды разработки.
Для внутреннего тестирования принимается первичный режим «платформа — продавец + ручной settlement» без автоматической выплаты практику. Его нельзя выдавать за split: до внешнего пилота схема должна получить письменное подтверждение бизнеса/юриста и отражение в оферте. Внутренний ledger в любом случае хранит gross, provider fee, platform fee, practitioner payable, refund allocation и settlement status. Решение включено в pre-pilot gate.
Возвраты и спорные случаи¶
- возврат создаёт отдельный
Refundс собственным idempotency key и reason; - payment status не переписывается в «refunded»: агрегируется сумма полных/частичных refund;
- доступ по подписке меняется только по утверждённой refund policy;
- отмена практиком: предлагаемый PRD default — 100% возврат + отдельный bonus credit;
- no-show клиента и запрос в течение 48 часов — policy, а не hardcode; версия policy сохраняется с заказом;
- приложение не предоставляет ручную операцию
mark succeededили удаление финансовой истории; - ежедневная reconciliation сравнивает provider objects/реестры с внутренним ledger и поднимает расхождения в техническом мониторинге; административного API в MVP нет.
Для split правила распределения возврата отличаются, см. документацию YooKassa.
Минимальные таблицы¶
Product, Price, Order, PaymentAttempt, ProviderEvent, PaymentMethodReference, Subscription, Entitlement,
Refund, LedgerEntry, а после выбора модели — PractitionerSettlement/Transfer.
Приёмочные сценарии¶
- успешная первичная подписка и сохранённый метод;
- пользователь вернулся раньше webhook — доступа ещё нет, затем он появляется;
- одинаковый create request/webhook доставлен 2–10 раз — одна операция и один entitlement;
- timeout после POST — retry не списывает второй раз;
- успешное и неуспешное продление, grace, cancel-at-period-end;
- полное и частичное возмещение;
- неверная сумма/metadata в webhook блокирует transition и создаёт alert;
- консультация: комиссия, отмена практиком, no-show и settlement после оказания;
- reconciliation находит искусственно созданное расхождение.
Перед внешним пилотом нужны магазин YooKassa, подключённые автоплатежи, HTTPS webhook, политика чеков по 54-ФЗ, оферта, согласие на регулярные списания и утверждённая юридическая схема практиков. Полный перечень — в pre-pilot gate.