# Счёттың өмірлік циклі

> Счёттың барлық күйлері мен ауысулары, қай күйде қандай webhook оқиғасы келеді, қайсысы ашық және қайсысы төленген деп есептеледі, кеш келген төлемді қалай өңдеу керек.

## Қысқаша

Счёт мынандай жолмен жүреді:

```
new ──▶ pending ──┬──▶ paid ──┬──▶ refunded
                  │           └──▶ partially_refunded
                  ├──▶ cancelled
                  └──▶ expired
```

Кодыңызда екі топты ажыратыңыз: **ашық** счёттар — `new` және `pending`; **төленген** деп есептелетіндер — `paid`, `refunded`, `partially_refunded`. Қайтарылған счёт та төленген болып қалады: ақша келген, сосын қайтарылған.

Ең маңызды шекті жағдай — **кеш төлем**: жабылған счётқа ақша кейін де келуі мүмкін.

## Күйлер кестесі

| Күй | Мағынасы | Ашық па | Төленген деп есептеле ме |
|---|---|---|---|
| `new` | Счёт жасалды, әлі Kaspi-де күтуде | Иә | Жоқ |
| `pending` | Клиенттің төлеуін күтіп тұр | Иә | Жоқ |
| `paid` | Төленді | Жоқ | Иә |
| `cancelled` | Сіз болдырдыңыз | Жоқ | Жоқ |
| `expired` | Мерзімі өтті, төленбеді | Жоқ | Жоқ |
| `refunded` | Толық қайтарылды | Жоқ | Иә |
| `partially_refunded` | Ішінара қайтарылды | Жоқ | Иә |

Тізімді сүзгенде немесе есеп жасағанда осы екі топқа сүйеніңіз, әр күйді бөлек тізіп жазбаңыз — кейін жаңа күй қосылса, кодыңыз бұзылмайды.

## Ауысулар

| Қайдан | Қайда | Не болды |
|---|---|---|
| `new` | `pending` | Счёт Kaspi-ге тіркелді, клиент төлей алады |
| `pending` | `paid` | Клиент төледі |
| `pending` | `cancelled` | Сіз `POST /invoices/{id}/cancel` жібердіңіз |
| `pending` | `expired` | `expiresAt` өтті, ешкім төлемеді |
| `paid` | `refunded` | Толық сома қайтарылды |
| `paid` | `partially_refunded` | Сома бөлігі қайтарылды |
| `cancelled` / `expired` | `paid` | **Кеш төлем.** Ақша кешігіп келді |

Кері ауысу жоқ: `paid` счёт қайта `pending` болмайды, `expired` счёт өзінен өзі жанданбайды.

## Қай оқиға қай сәтте келеді

| Оқиға | Қашан | Дене ерекшелігі |
|---|---|---|
| `invoice.created` | Счёт жасалғанда | `id`, `externalOrderId`, `amount` |
| `invoice.pending` | Счёт төлеуге дайын болғанда | — |
| `invoice.paid` | Клиент төлегенде | `paidAt`, `receiptUrl` |
| `invoice.cancelled` | Болдырғанда | — |
| `invoice.expired` | Мерзімі өткенде | — |
| `invoice.failed` | Счёт өтпей қалғанда | — |
| `invoice.refunded` | Толық қайтарғанда | — |
| `invoice.partially_refunded` | Ішінара қайтарғанда | — |
| `invoice.status` | Күй өзгергенде жалпы оқиға | Барлық ауысуға бір арна керек болса |
| `invoice.lost` | Счёт Kaspi жағында жоғалғанда | — |

Оқиға тізімін кабинеттің **Интеграциялар** бөлімінде таңдайсыз. Барлығын қосудың қажеті жоқ: көбіне `invoice.paid` пен `invoice.refunded` жеткілікті. Баптау: [Webhook баптау](/kb/webhook-setup).

## Уақыттар

| Не | Қанша |
|---|---|
| QR-дың сканерлеу терезесі | Шамамен 3 минут, нақты уақыты `expiresAt` өрісінде |
| Төлемді тексеру циклі | Әр 3 секунд сайын |
| 3 минуттан жас счёт | Әр айналымда тексеріледі |
| 30 минутқа дейінгі счёт | 20 секунд сайын |
| Одан ескі счёт | 90 секунд сайын |
| Клиент төлегеннен webhook-қа дейін | Әдетте 5 секунд ішінде |

Кезек ең жаңа счёттан басталады, сондықтан жаңа ғана жасалған счёт бірінші тексеріледі.

**Kaspi төлем уақытын бермейді:** payload-та төлем сәтінің дәл уақыты жоқ, сондықтан «клиент растағаннан біздің білгенімізге дейін» аралығын Kaspi деректерінен өлшеу мүмкін емес.

Мерзім туралы толығырақ: [Счёттың жарамдылық мерзімі](/kb/invoice-expiry).

## Кеш төлем: ең маңызды шекті жағдай

`cancelled` немесе `expired` болып жабылған счётқа ақша кешігіп келуі мүмкін. Ондайда `invoice.paid` оқиғасы **`late: true`** белгісімен кейін де келеді.

Бұл қате емес және сирек болса да болады. Кодыңыз оны елемеуі тиіс емес — ақша шынымен түсті.

Не істеу керек:

1. Оқиғаны қабылдап, 200 қайтарыңыз.
2. Тапсырысты іздеп, оның қазіргі күйін қараңыз.
3. Екі жолдың бірін таңдаңыз: **қызметті беріңіз** (тауар бар, тапсырыс күші жойылмаған) немесе **ақшаны қайтарыңыз** ([Қайтару API](/kb/refunds-api)).
4. Клиентке хабарлаңыз. Үнсіз қалу ең жаман нұсқа.

```js
if (event === 'invoice.paid') {
  const order = await findOrder(invoice.externalOrderId);
  if (invoice.late && order.status === 'closed') {
    await notifyStaff(order, invoice);   // қолмен шешім керек
  } else {
    await fulfil(order, invoice);
  }
}
```

Толығырақ: [Кеш келген төлем](/kb/late-payment).

## Күйді қалай оқу керек

Екі арна бар, екеуі бірін-бірі алмастырмайды:

| Арна | Қашан қолдану |
|---|---|
| Webhook | Негізгі арна. Күй өзгергенде өзі келеді |
| `GET /api/v1/invoices/{id}` | Нақты бір счёттың қазіргі күйін білу керек болғанда |

Клиент құрылғының алдында тұрып нәтиже күтетін сценарийде (вендинг, турникет, шлагбаум) екеуін қатар жүргізіңіз: алғашқы 30 секундта секундына бір рет күйді сұрап, қайсысы бұрын келсе соны қабылдаңыз. Сондықтан өңдеуіңіз идемпотентті болуы керек. Толығырақ: [Webhook пен күйді сұрау](/kb/polling-vs-webhook).

`GET /api/v1/invoices/{id}` жауабында счёттың оқиғалары мен қайтарулары да келеді — журнал ретінде пайдалы.

## Идемпотентті өңдеу

Бір счёт бойынша бір оқиға бірнеше рет келуі мүмкін: желі үзілді, сіз 200 қайтара алмадыңыз, біз қайталадық. Сондықтан өңдеуді **`(invoice.id, status)` жұбы** бойынша бір рет орындаңыз.

Тәжірибеде бұл былай көрінеді: осы жұпты өз базаңызға бірегей индекспен жазып, жазылмаса (яғни бұрын болған) ештеңе істемейсіз. Толығырақ: [Идемпоттылық](/kb/idempotency).

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

**`refunded` счёт төленген деп есептеле ме?** Иә. Ақша келген, содан кейін қайтарылған. Есепте оны төленген топтан шығарып тастамаңыз — қайтару бөлек көрсеткіш.

**`pending` күйінде ұзақ тұрып қалса?** Бұл қалыпты: клиент әлі төлемеген. `expiresAt` өткенде счёт `expired` болады. Толығырақ: [Счёт pending күйінде тұрып қалды](/kb/invoice-stuck-pending).

**`new` күйін көрмеймін, бірден `pending` келеді.** Қалыпты жағдай: счёт Kaspi-ге тез тіркеледі, сондықтан `new` күйі көбіне көрінбей қалады.

**Болдырылған счётты қайта ашуға бола ма?** Жоқ. Жаңа счёт жасаңыз.

**Күй өзгерісін өткізіп алсам ше?** Webhook журналы кабинеттің **Интеграциялар** бөлімінде сақталады, ал счёттың қазіргі күйін әрқашан `GET /api/v1/invoices/{id}` арқылы оқи аласыз.
