# Как протестировать интеграцию

> Сценарии, которые нужно прогнать в песочнице: успешная оплата, отмена, истечение срока, полный и частичный возврат, поздняя оплата. Как тестировать вебхуки, какие граничные случаи проверить и как оформить всё это в автотесты.

## Коротко

Песочница (ключ `qp_test_…`) — полноценный режим, в котором не двигаются реальные деньги и не вызывается Kaspi. Оплату вы «проводите» сами через `POST /api/v1/invoices/{id}/simulate`. Кассир Kaspi для песочницы не нужен, а счета песочницы не расходуют лимит тарифа.

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

## Подготовка

1. Переключите режим организации в песочницу
2. Создайте ключ `qp_test_…` и выдайте нужные scope
3. Добавьте адрес вебхука и сохраните секрет (показывается один раз)

Чем режимы отличаются: [Чем песочница отличается от боевого режима](/kb/ru/sandbox-vs-live).

## Шесть основных сценариев

Эндпоинт `simulate` принимает три статуса: `paid`, `failed`, `expired`. Отмена делается отдельным эндпоинтом, возврат — тоже.

| № | Сценарий | Как воспроизвести | Что проверяете |
|---|---|---|---|
| 1 | Успешная оплата | Создать счёт → `simulate {"status":"paid"}` | Пришёл `invoice.paid`, заказ помечен оплаченным, товар/услуга выданы |
| 2 | Отмена | Создать счёт → `POST /invoices/{id}/cancel` | Статус `cancelled`, пришёл `invoice.cancelled`, заказ закрыт |
| 3 | Истечение срока | Создать счёт → `simulate {"status":"expired"}` | Статус `expired`, пришёл `invoice.expired`, резерв товара снят |
| 4 | Полный возврат | Оплаченный счёт → `POST /invoices/{id}/refund` без суммы | Статус `refunded`, пришли `refund.done` и `invoice.refunded` |
| 5 | Частичный возврат | Оплаченный счёт → `refund {"amount": половина}` | Статус `partially_refunded`, остаток посчитан верно |
| 6 | Поздняя оплата | Закрыть счёт через `cancel` или `simulate expired`, затем `simulate {"status":"paid"}` | Событие `invoice.paid` пришло с признаком **`late: true`** — видно, как система обрабатывает оплату уже закрытого заказа |

Шестой сценарий — самый важный. В жизни клиент сканирует QR, думает и подтверждает оплату уже после того, как счёт закрылся. Что делает ваша система: выдаёт услугу или автоматически возвращает деньги? Решите заранее, иначе первый такой случай придётся разбирать руками. Подробно: [Поздняя оплата](/kb/ru/late-payment).

Полное описание симуляции: [Симуляция оплаты в песочнице](/kb/ru/sandbox-simulate).

## Как тестировать вебхуки

Вебхук — самая хрупкая часть интеграции, потому что это входящий запрос на ваш сервер. Есть три подхода.

**1. Локальный сервер плюс туннель.** Удобно во время разработки: туннель (вроде ngrok) выставляет ваш локальный порт наружу. В песочнице это работает.

**Но в боевом режиме туннельный адрес не принимается** — вы получите `webhook_url_tunnel_forbidden`. В проде допустим только постоянный домен на `https`. Используйте туннель на этапе разработки и не оставляйте его как боевой адрес.

**2. Внешний приёмник (вроде webhook.site).** Хорош, чтобы своими глазами увидеть заголовки, тело и подпись. Но **не кладите туда свой секрет** — пользуйтесь только секретом песочницы и только для просмотра.

**3. Офлайн-проверка подписи.** Самый надёжный вариант. Сохраните пришедшее тело в файл и напишите юнит-тест: посчитайте HMAC-SHA256 от строки `timestamp + "." + rawBody` и сравните с `X-Webhook-Signature`.

Что обязано делать ваше обработчик вебхука:

- Подпись не сошлась — вернуть 401 и ничего не обрабатывать
- `X-Webhook-Timestamp` старше 5 минут — не принимать
- Подпись верна — **сначала вернуть 200**, тяжёлую работу делать после
- Та же пара `(invoice.id, status)` пришла повторно — не обрабатывать второй раз

Если вы не отвечаете 2xx, доставка повторяется **11 раз** (с нарастающей паузой от 10 секунд до 1 часа). При длительной череде ошибок адрес временно ставится на паузу. Поэтому порядок «ответить 200, обработать потом» имеет значение.

Детали: [Безопасность вебхуков](/kb/ru/webhook-security), [Настройка вебхуков](/kb/ru/webhook-setup).

## Граничные случаи

Когда основные сценарии пройдены, проверьте вот это — именно оно валит интеграции в проде.

| Что проверяем | Как | Ожидаемый результат |
|---|---|---|
| Дубль запроса | Создайте счёт дважды с одним `Idempotency-Key` | Новый счёт не создаётся, HTTP 200 и `idempotentReplay: true` |
| Один заказ дважды | Попробуйте два счёта с одним `externalOrderId` | Решите заранее, как обрабатываете это на своей стороне |
| Неверная сумма | `0`, отрицательное число, строка, тиыны в счёте `phone` | `invalid_amount`, `amount_too_small`, `amount_must_be_whole_tenge` |
| Отсутствующий телефон | `kind: "phone"` без `customer.phone` или с неверным форматом | `invalid_phone` |
| Несуществующий счёт | `GET` по выдуманному `id` | `invoice_not_found`, 404 |
| Закрытый счёт | Попробуйте отменить оплаченный счёт | `invoice_not_open`, 409 |
| Возврат больше суммы | Вернуть больше, чем оплачено | `invalid_refund_amount` |
| Лимиты | Создайте много счетов в песочнице | Счета песочницы не идут в месячный лимит, но частота запросов может дать 429 |
| Битый ключ | Запрос заведомо испорченным ключом | `unauthorized`, 401 |
| Нехватка прав | Возврат ключом без `refunds:write` | `insufficient_scope`, 403 |

Все коды: [Каталог ошибок](/kb/ru/error-catalog).

## Автотесты

Ручная проверка хороша один раз, но повторять её после каждой правки никто не будет. Песочница подходит для автотестов: реальных денег нет, Kaspi не вызывается, лимит не расходуется.

Типовой цикл одного теста:

1. `POST /api/v1/invoices` — создать счёт, получить `id`
2. `POST /api/v1/invoices/{id}/simulate` — `{"status":"paid"}`
3. Дождаться вебхука (или прочитать `GET /api/v1/invoices/{id}` напрямую)
4. Убедиться, что статус заказа в вашей базе изменился
5. Прибраться за собой

Практические советы:

- **Не зависайте в ожидании вебхука.** Поставьте таймаут (например, 30 секунд), по истечении прочитайте статус через `GET /api/v1/invoices/{id}` и завершите тест по нему
- **Каждый тест создаёт свой счёт.** Тесты, опирающиеся на общий счёт, ломают друг друга
- **Генерируйте `Idempotency-Key` случайно**, иначе второй прогон вернёт старый счёт и тест «пройдёт» вхолостую
- **Тестируйте и ошибочные сценарии.** Счастливый путь проверяют все, а падает интеграция на ошибках
- **В CI используйте только ключ песочницы.** Боевому ключу в тестовом окружении делать нечего

Примеры автотестового цикла есть в документации SDK: [Node.js SDK](/kb/ru/sdk-node), [PHP SDK](/kb/ru/sdk-php), [Python SDK](/kb/ru/sdk-python).

## При переходе в боевой режим

Пройденные тесты песочницы не гарантируют, что в проде всё хорошо. Дополнительно проверьте:

- Ключ заменён на `qp_live_…` (забывают чаще всего)
- Адрес вебхука подключён и для боевого режима
- Кассир активен
- Первый реальный платёж — **на минимальную сумму**, со своего телефона

Полный список: [Чек-лист выхода в прод](/kb/ru/going-live-checklist).

## Вопросы и ответы

**QR в песочнице настоящий?** Нет. Картинка генерируется, но приложение Kaspi её не отсканирует. Оплата проводится через `simulate`.

**Счета песочницы расходуют лимит тарифа?** Нет. И в месячный лимит не попадают, и семидневный пробный период не запускают — он начинается с первого **боевого** счёта.

**Можно ли выставить через `simulate` статус `cancelled`?** Нет, принимаются только `paid`, `failed`, `expired`. Для отмены есть `POST /invoices/{id}/cancel`.

**Что будет, если вызвать `simulate` боевым ключом?** Ошибка `not_sandbox`, 403. Симуляция доступна только в песочнице.

**Нужен ли кассир для тестов?** Для песочницы — нет. Кассир Kaspi требуется только для боевых счетов.
