# Поздняя оплата — счёт закрыт, а деньги пришли

> На отменённый или просроченный счёт деньги могут прийти с опозданием. В этом случае событие invoice.paid приходит с меткой late: true. Что делать и как заранее подготовить к этому код.

## Коротко

Даже после того как счёт перешёл в `cancelled` или `expired`, деньги по нему могут прийти с опозданием. Это нормальная, хотя и редкая ситуация. В таком случае к вам приходит событие `invoice.paid` **с меткой `late: true`**. Дальше у вас два пути: **оказать услугу** или **вернуть деньги**. Третьего — проигнорировать — нет: деньги лежат на счёте мерчанта, а покупатель ждёт свой товар.

Главное, чтобы ваш код умел ждать такое событие. Многие интеграции обрабатывают `invoice.paid` только если счёт ещё открыт, и молча теряют поздние оплаты.

## Почему так бывает

Счёт живёт и у нас, и в Kaspi, и отсчёт времени с двух сторон не всегда совпадает.

- Покупатель отсканировал QR и долго сидит в окне подтверждения. Мы уже закрыли счёт как `expired`, а он всё-таки нажимает «подтвердить».
- Связь прервалась, и подтверждение дошло до Kaspi с опозданием.
- Вы отменили счёт ровно в тот момент, когда покупатель платил.
- Обработка на стороне Kaspi ненадолго задержалась.

**Kaspi не сообщает точное время оплаты.** В payload нет поля «в какую секунду покупатель подтвердил», поэтому измерить задержку по данным невозможно. Вы видите только факт: счёт был закрыт, деньги пришли.

## Как это увидеть

| Признак | Где видно |
|---|---|
| Событие `invoice.paid` с меткой `late: true` | В payload вебхука |
| Статус счёта перешёл из закрытого в `paid` | `GET /api/v1/invoices/{id}` |
| В Kaspi Pay транзакция есть, а в вашей системе заказ закрыт | При сверке счёта Kaspi со своей базой |

Если вебхуки не слушать, такую оплату вы заметите только в конце месяца при сверке. Поэтому событие `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.** Даже если внутренняя логика не может принять решение, примите событие и положите его в свою очередь.

## Как принять решение

| Ситуация | Правильное действие |
|---|---|
| Заказ ещё не выполнен, товар в наличии | Выдайте услугу, отметьте заказ оплаченным |
| Товара нет, заказ закрыт | Верните деньги и сообщите покупателю |
| Покупатель заплатил дважды | Верните лишнее |
| Услуга срочная (подписка, абонемент) | Откройте период с этого момента |
| Мероприятие прошло, билет недействителен | Верните деньги |

Возврат делается через `POST /api/v1/invoices/{id}/refund`. Если в ответ пришло `refund_unknown` или `refund_pending_unknown`, **не отправляйте повторно** — сначала прочитайте статус счёта, иначе можно вернуть деньги дважды.

## Как говорить с покупателем

Поздняя оплата не вина покупателя. Он нажал «подтвердить», деньги ушли — с его точки зрения всё прошло правильно.

- Не тяните с сообщением. Напишите, как только увидели приход.
- Если услугу можно оказать, просто окажите — объяснять ничего не нужно.
- Если нельзя, скажите, что сделан возврат и когда деньги придут.
- Не показывайте покупателю технический текст. Достаточно «оплата обработалась с задержкой».

## Чтобы это случалось реже

- **Не отменяйте счёт слишком рано.** Покупатель может всё ещё быть в окне подтверждения.
- **Используйте поле `expiresAt`.** Не придумывайте собственный срок, опирайтесь на срок самого счёта.
- **Не закрывайте заказ одновременно со счётом.** Лучше подержать его в статусе «ожидает оплату».
- **Обязательно слушайте вебхуки.** Если смотреть только в кабинет, поздние оплаты проходят мимо.

Если счета не оплачиваются вообще, причина другая: [Проблема у вас или у Kaspi](/kb/ru/is-it-us-or-kaspi).

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

**Часто ли бывают поздние оплаты?** Редко. Но при сотнях счетов в месяц вы с ними столкнётесь, поэтому код стоит подготовить заранее.

**Куда приходят деньги, если счёт закрыт?** Прямо на ваш счёт Kaspi. Статус счёта на движение денег не влияет.

**В каком событии приходит метка `late: true`?** В `invoice.paid`, если до этого счёт был `cancelled` или `expired`.

**Если я проигнорирую событие, оно повторится?** При ответе не 2xx — да, до 11 раз. Но если вернуть 200 и внутри ничего не сделать, событие потеряется.

**Сколько времени есть на возврат?** Срок определяется на стороне Kaspi, поэтому тянуть не стоит. Если возврат не проходит, сначала прочитайте статус счёта.
