# Кеш келген төлем — счёт жабылған, ал ақша келді

> Болдырылған немесе мерзімі өткен счётқа ақша кешігіп келуі мүмкін. Ондайда invoice.paid оқиғасы late: true белгісімен келеді. Не істеу керек және кодта бұған қалай дайын болу керек.

## Қысқаша

Счёт `cancelled` немесе `expired` күйіне өтіп кеткен соң да, оған ақша кешігіп келуі мүмкін. Бұл қалыпты жағдай, сирек болғанымен, болады. Ондайда сізге `invoice.paid` оқиғасы **`late: true` белгісімен** келеді. Сіз екі жолдың бірін таңдайсыз: **қызметті беру** немесе **ақшаны қайтару**. Үшінші жол — елемеу — жарамайды: ақша мерчант шотында тұр, ал клиент тауарын күтіп отыр.

Ең бастысы — кодыңыз мұндай оқиғаны күте білсін. Көп интеграция `invoice.paid` оқиғасын «счёт ашық болса ғана» өңдейді және кеш төлемді үнсіз тастап кетеді.

## Неге бұлай болады

Счёт бізде де, Kaspi-де де жүреді, әрі екі жақтың уақыт есебі әрдайым бірдей болмайды.

- Клиент QR-ды сканерлеп, төлемді растау терезесінде ұзақ отырады. Біз счётты `expired` деп жабамыз, ал ол растауды сол күйі басады.
- Байланыс үзіліп, растау Kaspi жағына кешігіп жетеді.
- Сіз счётты болдырдыңыз, дәл сол сәтте клиент төлеп жатқан болады.
- Kaspi жағында өңдеу уақытша кідіреді.

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

## Қалай білесіз

| Белгі | Қайдан көресіз |
|---|---|
| `invoice.paid` оқиғасы `late: true` белгісімен | Webhook payload-ынан |
| Счёттың күйі жабық күйден `paid` күйіне өткен | `GET /api/v1/invoices/{id}` |
| Kaspi Pay-де транзакция бар, ал жүйеңізде тапсырыс жабылған | Kaspi шоты мен өз базаңызды салыстырғанда |

Webhook-ты тыңдамасаңыз, мұндай төлемді тек айдың соңында, есеп айырысқанда байқайсыз. Сондықтан `invoice.paid` оқиғасын әрқашан өңдеген жөн.

## Кодта дайын болу

Кеш төлемді бөлек оқиға деп қарамаңыз — бұл жай ғана `invoice.paid`, оның үстінде белгісі бар. Негізгі ереже: **төлемді өңдеуді счёттың алдыңғы күйіне байлап қоймаңыз**.

```js
app.post('/webhooks/qutpay', async (req, res) => {
  const event = req.headers['x-webhook-event'];
  const body = JSON.parse(req.rawBody); // қолтаңбаны тексергеннен кейін

  if (event === 'invoice.paid') {
    const orderId = body.invoice.externalOrderId;
    const order = await orders.find(orderId);

    // Идемпоттылық: бұл счёт бойынша бұрын өңдедік пе?
    if (order.paidInvoiceId === body.invoice.id) return res.sendStatus(200);

    if (body.late) {
      // Счёт жабық болатын, бірақ ақша келді
      if (order.status === 'cancelled') {
        await flagForReview(order, body.invoice); // адам шешеді
      } else {
        await fulfil(order, body.invoice); // қызметті береміз
      }
    } else {
      await fulfil(order, body.invoice);
    }
  }

  res.sendStatus(200);
});
```

Үш нәрсеге назар аударыңыз:

- **Идемпоттылық.** `(invoice.id, status)` жұбы бойынша бір рет қана өңдеңіз. 2xx емес жауап берсеңіз, оқиға 11 ретке дейін қайталанады.
- **Жабық тапсырысты автоматты ашпаңыз.** Тапсырыс болдырылған, тауар басқа біреуге сатылған болуы мүмкін. Ондай жағдайды адамға жіберген дұрыс.
- **Әрқашан 200 қайтарыңыз.** Ішкі логикаңыз шешім қабылдай алмаса да, оқиғаны қабылдап алып, өз кезегіңізге қойыңыз.

## Шешім қабылдау

| Жағдай | Дұрыс әрекет |
|---|---|
| Тапсырыс әлі орындалмаған, тауар бар | Қызметті беріңіз, тапсырысты `paid` деп белгілеңіз |
| Тауар қоймада жоқ, тапсырыс жабылған | Ақшаны қайтарыңыз, клиентке хабарлаңыз |
| Клиент екінші рет төлеп қойған | Артығын қайтарыңыз |
| Қызмет мерзімдік (жазылым, абонемент) | Мерзімді сол сәттен бастап ашыңыз |
| Іс-шара өтіп кеткен, билет жарамсыз | Ақшаны қайтарыңыз |

Ақшаны қайтару `POST /api/v1/invoices/{id}/refund` арқылы жасалады. Қайтару жауабы `refund_unknown` немесе `refund_pending_unknown` десе, **қайталап жібермеңіз** — алдымен счёт күйін оқыңыз, әйтпесе екі рет қайтарып жіберуіңіз мүмкін.

## Клиентпен қалай сөйлесу керек

Кеш төлем клиенттің кінәсі емес. Ол растау батырмасын басты, ақшасы шықты — оның көзқарасы бойынша бәрі дұрыс болды.

- Хабарламаны кешіктірмеңіз. Ақша келгенін көрген бойда жазыңыз.
- Егер қызметті бере алатын болсаңыз, жай ғана беріңіз — түсіндірудің қажеті жоқ.
- Бере алмасаңыз, қайтару жасалғанын және қашан келетінін айтыңыз.
- Клиентке техникалық мәтінді көрсетпеңіз. «Төлем кешігіп өңделді» деген жеткілікті.

## Қайталанбауы үшін

- **Счётты тым ерте болдырмаңыз.** Клиент әлі растау терезесінде отырған болуы мүмкін.
- **`expiresAt` өрісін пайдаланыңыз.** Мерзімді өзіңіз ойлап таппай, счёттың өз мерзіміне сүйеніңіз.
- **Тапсырысты счётпен қатар жаппаңыз.** Тапсырысты «төлем күтілуде» күйінде біраз ұстаған дұрыс.
- **Webhook-ты міндетті түрде тыңдаңыз.** Тек кабинетке қарап отырсаңыз, кеш төлемдер көзден таса қалады.

Егер счёттар мүлде төленбей жатса, себебі басқа: [Мәселе менде ме, Kaspi-де ме](/kb/is-it-us-or-kaspi).

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

**Кеш төлем жиі бола ма?** Жоқ, сирек. Бірақ ай сайын жүздеген счёт шығаратын болсаңыз, кездеседі. Сондықтан кодта дайын болған дұрыс.

**Ақша қайда түседі — жабық счёт болса да?** Тіке сіздің Kaspi шотыңызға. Счёттың күйі ақшаның қозғалысына әсер етпейді.

**`late: true` белгісі қай оқиғада келеді?** `invoice.paid` оқиғасында. Счёт бұрын `cancelled` немесе `expired` болған жағдайда.

**Мен оқиғаны елеместен қалдырсам, қайталана ма?** Иә, 2xx емес жауап берсеңіз 11 ретке дейін қайталанады. Бірақ 200 қайтарып, ішінде елемей тастасаңыз — оқиға жоғалады.

**Қайтару жасауға қанша уақыт бар?** Мерзім Kaspi жағында анықталады, сондықтан кешіктірмеген дұрыс. Қайтару өтпей жатса, счёттың күйін алдымен оқып алыңыз.
