# Sandbox-та төлемді симуляциялау

> simulate эндпоинті арқылы sandbox счётының күйін өзгерту: paid, failed, expired. Kaspi шақырылмайды, webhook нағыз төлемдегідей келеді. Сынау циклі және автоматты тест жазу.

## Қысқаша

Sandbox-та нақты Kaspi жоқ, сондықтан клиенттің төлеуін **өзіңіз симуляциялайсыз**:

```
POST /api/v1/invoices/{id}/simulate
{ "status": "paid" }
```

Осыдан кейін счёт нағыз төлемдегідей жүреді: күйі өзгереді, чек нөмірі беріледі, оқиғалар журналға жазылады және **webhook нақты жағдайдағыдай жіберіледі**. Интеграцияңызды толық циклмен, бір тиын тәуекелсіз тексеруге болады.

Бұл әдіс **тек sandbox-та** істейді. Live режимде `not_sandbox` (HTTP 403) қатесі келеді.

## Қандай күйлерге ауыстыруға болады

| `status` | Не болады |
|---|---|
| `paid` | Счёт төленген болып белгіленеді, чек нөмірі беріледі, `invoice.paid` жіберіледі |
| `failed` | Төлем өтпеген болып жабылады, `invoice.failed` жіберіледі |
| `expired` | Мерзімі өткен болып жабылады, `invoice.expired` жіберіледі |

Басқа мән берсеңіз, `invalid_status` (HTTP 422) келеді.

Ауыстыру тек **ашық** счётқа қолданылады (`new`, `pending`). Жабылған счётқа тағы симуляция жасасаңыз, `invoice_not_open` (HTTP 409) келеді.

**Бір ғана ерекшелік бар:** `cancelled` немесе `expired` күйдегі счётқа `paid` симуляциясын жасауға болады. Бұл — **кеш төлемді** әдейі жаңғырту: счёт жабылып қалған, ал ақша кейін келген жағдай. Ондайда оқиға `late: true` белгісімен келеді, дәл продакшендегідей. Өңдеуіңіз бұл жағдайды дұрыс қарсы алатынын осылай тексересіз: [Кеш келген төлем](/kb/late-payment).

## Не керек

- Ұйым **sandbox режимінде** тұруы керек (кабинеттен ауыстырасыз);
- кілт `qp_test_…` болуы керек;
- кілтте `invoices:write` құқығы болуы керек.

Sandbox-та Kaspi кассирі керек емес: [Sandbox пен нақты режимнің айырмашылығы](/kb/sandbox-vs-live).

**Kaspi мүлде шақырылмайды.** Ешқандай SMS, ешқандай нақты QR, ешқандай ақша қозғалысы жоқ. Sandbox счёттары тарифтің айлық лимитіне де кірмейді және сынақ мерзімін бастамайды.

## Қолмен жүргізу

Үш жол бар, үшеуі де бірдей нәтиже береді.

**API арқылы:**

```bash
curl -X POST https://api.qut.kz/api/v1/invoices/inv_…/simulate \
  -H 'X-API-Key: qp_test_…' \
  -H 'Content-Type: application/json' \
  -d '{"status":"paid"}'
```

Жауап — жаңартылған счёттың өзі: `status`, `paidAt`, `receiptNumber`, `receiptUrl` және басқа өрістер.

**Төлем бетінен:** sandbox счётының `payUrl` бетін ашсаңыз, онда төлеуді имитациялайтын батырма болады. Қолмен тексергенде ең жылдам жол.

**Кабинеттен:** Счёттар бөлімінде счёттың карточкасын ашыңыз — sandbox счётында симуляция әрекеті тұрады.

## Сынау циклі

Интеграцияны тексерудің қысқа реті:

1. **Счёт жасаңыз.** `POST /api/v1/invoices` — `qp_test_…` кілтімен. Жауаптағы `id` мен `payUrl`-ды сақтаңыз.
2. **Webhook адресіңіз тіркелгенін тексеріңіз.** Кабинет → Интеграциялар. Sandbox-та адрес талаптары жұмсақ, бірақ адрес қолжетімді болуы керек.
3. **Симуляция жасаңыз.** `POST /invoices/{id}/simulate` `{"status":"paid"}`.
4. **Webhook келгенін тексеріңіз.** Өз журналыңызда `invoice.paid` бар ма, қолтаңба тексерісінен өтті ме, жауабыңыз 2xx па.
5. **Счёттың күйін оқып шығыңыз.** `GET /invoices/{id}` — `status: "paid"`, `receiptNumber` бар.
6. **Қалған тармақтарды қайталаңыз:** жаңа счёт → `failed`, тағы біреуі → `expired`, тағы біреуін болдырып, сосын `paid` (кеш төлем).

Webhook келмесе, себебі әдетте симуляцияда емес: [Webhook келмей жатыр](/kb/webhook-not-arriving).

## Автоматты тест жазу

Симуляция — автотест үшін жасалған. Бір тесттің қаңқасы:

```javascript
const API = 'https://api.qut.kz/api/v1';
const H = { 'X-API-Key': process.env.QUTPAY_TEST_KEY, 'Content-Type': 'application/json' };

// 1. счёт
const inv = await (await fetch(`${API}/invoices`, {
  method: 'POST', headers: { ...H, 'Idempotency-Key': `test-${Date.now()}` },
  body: JSON.stringify({ amount: 100, description: 'Тест', externalOrderId: 'T-1' }),
})).json();

// 2. төлемді симуляциялау
await fetch(`${API}/invoices/${inv.id}/simulate`, {
  method: 'POST', headers: H, body: JSON.stringify({ status: 'paid' }),
});

// 3. нәтижені тексеру
const after = await (await fetch(`${API}/invoices/${inv.id}`, { headers: H })).json();
if (after.status !== 'paid') throw new Error(`күтілгені paid, келгені ${after.status}`);
```

Webhook-ты да тестке қосқыңыз келсе, екі тәсіл бар:

- **Тікелей тексеру.** Тестіңіз webhook қабылдағышын өзі көтереді (жергілікті сервер) және симуляциядан кейін оқиғаның келуін күтеді. Жеткізу бірден жүрмейтінін ескеріп, күту уақытын қойыңыз.
- **Жанама тексеру.** Webhook-ты бөлек тексеріп, тестте тек өз қабылдағышыңыздың логикасын сынайсыз: дайын payload-ты өз функцияңызға беріп, дұрыс өңдейтінін тексересіз. Бұл тұрақтырақ және жылдамырақ.

Тестті қайталанатын етудің екі шарты: әр жүгіруде **жаңа счёт** жасаңыз (бір счётты екі рет `paid` етуге болмайды) және `Idempotency-Key` мәнін әр жүгіруде өзгертіңіз.

## Қателер

| Қате | HTTP | Себебі |
|---|---|---|
| `not_sandbox` | 403 | Ұйым live режимінде немесе счёт live счёты |
| `invalid_status` | 422 | `status` — тек `paid`, `failed`, `expired` |
| `invoice_not_open` | 409 | Счёт бұрын жабылған (және `paid` кеш төлем жағдайы емес) |
| `invoice_not_found` | 404 | Идентификатор қате немесе счёт басқа ұйымдікі |
| `insufficient_scope` | 403 | Кілтте `invoices:write` жоқ |

Толық тізім: [Қателер каталогы](/kb/error-catalog).

## Жиі қойылатын сұрақтар

**Live счётты симуляциялауға бола ма?** Жоқ, ешқашан. Нақты режимде бұл эндпоинт `not_sandbox` қайтарады — бұл әдейі солай.

**Симуляциядан кейін webhook нақтысындай келе ме?** Иә, дәл сондай: сол оқиға атаулары, сол қолтаңба схемасы, 2xx емес жауапта сол қайталау тәртібі.

**Sandbox счёты чек алады ма?** Иә, чек нөмірі беріледі және чек беті жасалады, бірақ онда `SANDBOX` белгісі тұрады. Фискалды чек шықпайды.

**Қайтаруды да симуляциялауға бола ма?** Бөлек симуляция керек емес: sandbox-та `paid` күйге жеткен счётқа әдеттегі қайтару шақыруын жасай бересіз.

**Бұл тәсілді ЖИ-агентке тапсыруға бола ма?** Иә, дәл осы цикл агенттің өзін-өзі тексеруіне ыңғайлы: [ЖИ-агентке интеграцияны тапсыру](/kb/ai-agent-setup).
