Перейти к содержанию

Тестовый режим

Тестовый режим — тот же API, те же URL и тот же контракт, но с ключом test_… и провайдером-песочницей: деньги не двигаются, все объекты помечены test: true, тестовые и боевые данные не смешиваются в списках. Интеграцию нужно полностью отладить в тестовом режиме, затем сменить ключ на live_….

Что умеет песочница

Сценарии, которые воспроизводит песочница на чекауте и через simulate:

Сценарий Результат
Успешная оплата succeeded, событие payment.succeeded
Авторизация без списания waiting_for_capture для платежей с capture: false; затем capture (в том числе частичный) или cancel
Отказ банка canceled с cancellation_details.reason (insufficient_funds, issuer_declined, general_decline, …)
3-D Secure страница-имитация подтверждения: успех или отказ three_d_secure_failed
Истечение canceled, expired_on_confirmation (не оплачен) или expired_on_capture (не списан вовремя)
Таймаут провайдера ответ провайдера задерживается; платёж остаётся pending, KASSA дозапрашивает статус
Позднее уведомление уведомление провайдера приходит после истечения платежа — статус не меняется, расхождение фиксируется для сверки
Дубль уведомления повторное уведомление провайдера обрабатывается один раз, событие одно
СБП-QR QR-код с кнопкой «сканировать» на тестовом чекауте
Возврат полный и частичный, refund.succeeded

Исход выбирают три способа, и все три ведут в одну и ту же таблицу: POST /test/payments/{payment_id}/simulate, метаданные платежа и кнопки на странице-имитации банка (см. ниже).

Что из этого работает в текущей сборке

Список выше работает через API: песочница, POST /test/payments/{payment_id}/simulate, опрос GET /payments/{payment_id}, возвраты (POST /refunds, события refund.*) и доставка вебхуков с подписью. Шаг 4 типового автотеста ниже и пункты чек-листа «сделан возврат» и «подпись вебхука проверена» выполнимы целиком. Страница-имитация банка (3-D Secure, СБП-QR, кнопки исхода) готова и открывается браузером — раздел «Страница-имитация банка» ниже. Не готова сама страница оплаты /c/{token} (этап M1, задача 8): открыть адрес подтверждения можно, а витрины чекаута вокруг него пока нет. 501 not_implemented остаётся у операций поздних этапов — споров (M2) и платёжных ссылок (M5).

Исход по сумме сам не наступает — его нужно запросить

Таблица «сумма → исход» ниже выбирает исход при создании попытки у провайдера, но доводит платёж до него только явный триггер: simulate либо кнопка на странице-имитации банка. Сами, без триггера, наступают ровно два исхода — 01 (отказ банка) и 04 (таймаут провайдера): они происходят в ответе на обращение к провайдеру. Остальные (02, 03, 05, 06, 07 и «остальные — успешная оплата») приходят к нам уведомлением, а отправить его песочнице некому: плановой доставки без запроса в текущей сборке нет (KASSA, вопрос A14.12).

Когда создаётся попытка — зависит от confirmation.type, и это меняет рецепт. При type: qr попытка создаётся вместе с платежом: 01/04 видны сразу в ответе POST /payments, а simulate доступен с первой секунды. При type: redirect (значение по умолчанию) провайдера зовёт выбор способа оплаты на странице оплаты; до него у платежа нет операции у провайдера, и simulate отвечает 422 invalid_state с объяснением. Значит, у redirect-платежа сама по себе не наступает ни одна строка таблицы.

Практический вывод для автотеста: создавайте платёж с confirmation.type = qr и вызывайте simulate с нужным исходом — сумма задаст режим доставки (задержку и дубль), а simulate — судьбу платежа. Для redirect сначала нужен выбор способа на странице оплаты.

Детерминированные исходы без simulate

Карточные данные в Merchant API не передаются никогда — ни в тестовом режиме, ни в боевом, — поэтому «тестовой карты» у интеграции с API нет и быть не может. Тот же детерминизм задаётся двумя способами, доступными в тестовом режиме.

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

Последние две цифры Исход
01 отказ банка (payment.canceled; код отказа по умолчанию general_decline)
02 авторизация без списания → waiting_for_capture
03 непройденная 3-D Secure (three_d_secure_failed)
04 таймаут провайдера — платёж уходит следующему кандидату каскада
05 уведомление приходит с задержкой
06 уведомление приходит дважды (проверка идемпотентности обработчика)
07 платёж истекает по таймауту подтверждения
остальные успешная оплата

Метаданные платежа имеют приоритет над суммой и позволяют не подгонять копейки:

Ключ metadata Значение
sandbox_outcome succeed, decline, authorize, three_ds_fail, provider_timeout, late_webhook, duplicate_webhook, expire
sandbox_decline_code код отказа из справочника песочницы, например card_expired; неизвестный код заменяется общим отказом
sandbox_delay_seconds задержка уведомления в секундах, 0…3600 (по умолчанию 60 для late_webhook)

Опечатка в этих ключах не ошибка запроса: значение игнорируется, исход выбирается по сумме. Песочница — не то место, где стоит падать на опечатке в метаданных.

Страница-имитация банка

Тестовый платёж с confirmation.type = redirect уводит плательщика на страницу-имитацию банка — она живёт на домене API, а не на чекауте, и открывается обычным браузером.

Адрес Что там
{KASSA_SANDBOX_BANK_URL}/sandbox/bank/{provider_payment_id} получатель, сумма, кнопки исходов: «оплатить» и три отказа (недостаточно средств, карта истекла, эмитент отклонил)
.../sandbox/bank/{provider_payment_id}/3ds шаг 3-D Secure: поле «код из СМС». Код 123456 — успех, любой другой — отказ three_d_secure_failed. Второй попытки нет
.../sandbox/bank/{provider_payment_id}/leave уйти, не заплатив: исход не назначается, платёж закроется по своему сроку
{KASSA_SANDBOX_BANK_URL}/sandbox/sbp/{provider_payment_id} «приложение банка» для СБП; сюда же ведёт confirmation_data QR-кода, то есть его можно открыть по ссылке, а не только отсканировать

Адрес страницы приходит в confirmation.confirmation_url попытки, а у СБП — в confirmation.confirmation_data. Идентификатор операции подставлять руками не нужно.

Карточных полей на странице нет вовсе. Номер карты KASSA не принимает нигде и никогда — ни в боевом режиме, ни в тестовом: исход выбирается именованной кнопкой, а не номером. Расхождения между «картой» и «суммой» поэтому не возникает — таблица исходов одна, та же, что выше.

Кнопка возвращает плательщика раньше, чем исход применится. Нажатие ставит задачу, и исход доходит до платежа настоящим подписанным уведомлением — ровно тем же путём, что в бою. Поэтому 303 уводит плательщика на страницу результата до смены статуса: ваша страница «спасибо за покупку» обязана начинать с опроса GET /payments/{payment_id} или ждать события, а не считать возврат по return_url подтверждением оплаты. Настоящий банк ведёт себя так же.

По одной операции решение принимается один раз. Повторная отправка формы по уже завершённой операции отвечает 409 и страницей «операция уже обработана» — как и настоящий банк.

В боевом контуре этих адресов не существует: песочница не обслуживает live, а /sandbox/** закрыт на входящем прокси.

POST /test/payments/{payment_id}/simulate

Переводит тестовый платёж в нужный исход без участия плательщика — удобно для автотестов интеграции. Доступно только с тестовым ключом; с боевым ключом запрос отклоняется (403 forbidden).

curl -X POST https://api.kassa.example/v1/test/payments/pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3/simulate \
  -u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5e0f1a2b-3c4d-4e5f-9a8b-7c6d5e4f3a21" \
  -d '{ "outcome": "succeed", "delay_seconds": 0 }'

Ответ — 202 Accepted без тела: исход применяется асинхронно, результат придёт событием и будет виден в GET /payments/{payment_id}. В автотестах дождитесь события (или опрашивайте статус), а не полагайтесь на немедленное изменение.

Поля запроса:

Поле Тип Описание
outcome string, обязательно исход, см. таблицу ниже
delay_seconds integer 0–300 задержка применения исхода; удобно для проверки таймеров и состояния «ожидание»

Исходы (outcome):

outcome Из статуса Что происходит
succeed pending, waiting_for_capture оплата успешна → succeeded, событие payment.succeeded
authorize pending успешная авторизация: платёж с capture: falsewaiting_for_capture, событие payment.waiting_for_capture
decline pending отказ банка → canceled, party: payment_network, причина из справочника (например issuer_declined)
expire pending, waiting_for_capture истечение → canceled, expired_on_confirmation или expired_on_capture
three_ds_fail pending плательщик не прошёл 3-D Secure → canceled, three_d_secure_failed
provider_timeout pending провайдер не отвечает вовремя; платёж остаётся pending, статус уточняется опросом — проверьте, что ваша интеграция не считает таймаут отказом
late_webhook canceled (истёкший) уведомление провайдера об оплате приходит после истечения — статус не «воскресает», расхождение попадает в сверку
duplicate_webhook succeeded повторное уведомление провайдера — no-op, дублей событий нет

Несовместимый с текущим статусом исход → 422 invalid_state. Платёж с test: false или чужой платёж → 404 not_found.

Типовой автотест интеграции

  1. POST /payments с test_-ключом и confirmation.type = qr201, status: pending. Тип важен: при redirect попытка у провайдера появляется только после выбора способа на странице оплаты, и шаг 2 ответит 422 invalid_state.
  2. POST /test/payments/{payment_id}/simulate { "outcome": "succeed" }202.
  3. Дождаться payment.succeeded на тестовом адресе вебхука (проверить подпись по контрольному примеру) или GET /payments/{payment_id}status: succeeded.
  4. POST /refunds на часть суммы → 201; дождаться refund.succeeded.
  5. Повторить шаг 1 с тем же Idempotency-Key и телом → 200 и тот же объект; с другим телом → 409 idempotency_conflict.
  6. Второй платёж → simulate { "outcome": "decline" } → событие payment.canceled с cancellation_details.

Тестовые вебхуки

  • В кабинете на странице адреса вебхука есть кнопка «Отправить тестовое событие»: KASSA отправит подписанное событие выбранного типа с "test": true.
  • Журнал доставок (код ответа, время, число попыток) — в кабинете и в поле delivery объекта события (GET /events).
  • Проверить свою реализацию подписи без сети можно по контрольному примеру.

Чек-лист выхода в боевой режим

Кабинет отмечает пункты автоматически по фактам в тестовом режиме:

  • [ ] создан платёж;
  • [ ] обработано событие payment.succeeded (ответ 2xx);
  • [ ] обработано событие payment.canceled;
  • [ ] сделан возврат;
  • [ ] подпись вебхука проверена (хотя бы одна доставка с верной подписью на ваш адрес);
  • [ ] настроен return_urlпубличный адрес по https (в песочнице допустим и локальный, см. ниже); на боевом ключе локальный адрес будет отклонён 400 invalid_request.

Адрес возврата в песочнице

Тестовый ключ принимает return_url, указывающий на вашу машину: https://localhost:3000/return, https://192.168.1.10/return — разработка идёт локально, и требовать публичный домен раньше, чем появился сайт, было бы бессмысленно. Боевой ключ такие адреса отклоняет 400 invalid_request с parameter: confirmation.return_url.

Что не принимается ни в одном контуре: не-https (включая javascript: и data:), учётные данные в адресе (https://user:pass@…), пробелы и управляющие символы внутри адреса. Перед выкаткой на боевой ключ замените адрес возврата на публичный — это отдельный пункт чек-листа выше.

После проверки магазина выпустите live_-ключ и замените его в конфигурации. Пока магазин не активирован, запросы с live_-ключом отвечают 422 shop_not_live. Комиссия в песочнице (fee_amount) рассчитывается по демонстрационному тарифу и не равна боевой.