# Оплата в вендинговом автомате без терминала

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

## Коротко

В вендинге схема простая: покупатель выбирает товар → контроллер автомата сообщает вашему серверу в облаке → сервер создаёт счёт в Qut Pay → QR появляется на экране автомата → покупатель сканирует его в Kaspi → к вам приходит вебхук `invoice.paid` → сервер отправляет автомату команду на выдачу. Одинаково работает для автоматов воды, кофе и снеков. QR создаётся **заново на каждую продажу** — напечатанная и висящая на корпусе картинка не подойдёт.

## Как это работает

| Шаг | Кто | Что делает |
|---|---|---|
| 1 | Покупатель | Выбирает товар на автомате (например, 5 литров воды) |
| 2 | Контроллер | Запрос на ваш сервер: номер автомата, ячейка, сумма |
| 3 | Ваш сервер | `POST /api/v1/invoices`, в `metadata` — номер автомата и ячейка |
| 4 | Ваш сервер | Возвращает контроллеру `qrImageUrl` (или `qrUrl`) и `expiresAt` |
| 5 | Автомат | Показывает на экране QR и таймер |
| 6 | Покупатель | Сканирует в приложении Kaspi и подтверждает |
| 7 | Qut Pay | Шлёт на сервер `invoice.paid` |
| 8 | Ваш сервер | Отправляет автомату команду: открыть ячейку / включить насос |
| 9 | Автомат | Выдаёт товар и возвращает экран в исходное состояние |

После подтверждения покупателем вебхук обычно приходит **в пределах пяти секунд**, так что человек может спокойно подождать у автомата.

## Какой метод API используется

Основной — `POST /api/v1/invoices` с `kind: "qr"`:

```
POST https://api.qut.kz/api/v1/invoices
X-API-Key: qp_live_…
Idempotency-Key: vm-014-1726300000

{
  "amount": 250,
  "kind": "qr",
  "description": "Автомат №14, вода 5 л",
  "externalOrderId": "vm-014-88231",
  "metadata": { "machine": "VM-014", "slot": "A2", "address": "Абая 10" }
}
```

Из ответа нужны: `id`, `qrImageUrl` (картинка на экран), `qrUrl`, `expiresAt` (для таймера).

Дополнительно:

- `GET /api/v1/invoices/{id}` — спросить статус, если вебхук задержался
- `POST /api/v1/invoices/{id}/cancel` — покупатель ушёл, счёт закрывается
- `POST /api/v1/invoices/{id}/refund` — товар не выдан, деньги возвращаются

## Кладите номер автомата в metadata

Это основа всего сценария. В `metadata` положите как минимум три вещи: **номер автомата**, **ячейку или код товара**, **адрес точки**. Когда придёт вебхук, вы сразу знаете, какому автомату слать команду, и не ищете это по своей базе.

В `externalOrderId` удобно собирать номер автомата и внутренний номер продажи: `vm-014-88231`. По нему потом легко и отчитаться, и разобрать спорную ситуацию.

Если нужна раздельная отчётность по точкам, заведите на каждую точку свой API-ключ — фильтровать счета по источнику станет проще.

## Что нужно со стороны железа

- **Контроллер со связью.** GSM-модем с SIM-картой или Wi-Fi-модуль, если на точке есть сеть. Без подключения схема не работает: счёт создаётся в облаке.
- **Экран.** Подойдёт любой, на котором можно показать QR. У части автоматов он уже есть, к остальным ставят небольшой дисплей.
- **Сервер в облаке.** Создаёт счета, принимает вебхуки, шлёт команды автоматам. API-ключ лежит только здесь, не в контроллере.
- **Канал команд.** Чаще всего MQTT: контроллер подписан на брокер, сервер публикует в его тему «открыть ячейку A2». Можно и по HTTP — контроллер сам опрашивает сервер, но MQTT быстрее и меньше нагружает связь.

**Не кладите API-ключ в контроллер.** Устройство внутри автомата можно вскрыть, а один ключ действует на всю сеть. Пусть контроллер ходит только на ваш сервер и только со своим устройственным токеном.

## Окно QR — главное ограничение

Окно сканирования — около **трёх минут**, его задаёт Kaspi. В вендинге отсюда следуют сразу два вывода.

**Постоянный QR на экране висеть не может.** Счёт создаётся на каждую продажу заново. Заранее напечатанная картинка перестанет работать уже на следующий день.

**Показывайте таймер.** Возьмите `expiresAt` и выведите на экран «QR действителен ещё 2:40». Когда время вышло, дайте кнопку «Показать заново» — контроллер запросит новый счёт, старый останется `expired`.

Если всё же нужен постоянный печатный код (например, дополнительная строка «оплатить здесь, если что-то пошло не так»), это должен быть не QR счёта, а **ссылка на оплату** вида `qut.kz/p/<slug>`: у неё нет срока жизни, и есть вариант с открытой суммой.

## На что обратить внимание

- **Идемпотентность.** Обрыв связи и повторная отправка запроса контроллером — обычное дело. Ставьте `Idempotency-Key`, иначе одному покупателю выставится два счёта.
- **Обработчик вебхука должен быть идемпотентным.** Пара `(invoice.id, status)` обрабатывается ровно один раз, иначе автомат выдаст две бутылки.
- **Если товар не выдан.** Деньги пришли, а ячейка не открылась (заклинило, товар кончился) — заложите автоматический `refund`. Это самый частый спор в вендинге.
- **Держите вебхук и опрос статуса вместе.** Покупатель стоит у автомата, ждать долго нельзя. Если вебхука нет 5-10 секунд, пусть контроллер спросит `GET /invoices/{id}`: [Оплата подтверждается медленно](/kb/ru/slow-payments).
- **Адрес вебхука должен быть открыт.** В бою принимается только `https` и настоящий домен, IP и адреса туннелей не подойдут. Адрес должен отвечать без авторизации, иначе доставка будет падать: [Вебхук не приходит](/kb/ru/webhook-not-arriving).
- **Поздняя оплата.** Если деньги придут на истёкший счёт, событие придёт с признаком `late: true`. Покупателя рядом уже нет — возвращайте деньги.
- **С ростом сети пересмотрите тариф.** Десять автоматов по 50 продаж в день — это 15 000 счетов в месяц: [Какой тариф выбрать](/kb/ru/tariff-choose).

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

**На автомате нет интернета — подойдёт?** Нет. Счёт создаётся в облаке, а автомат должен узнать о подтверждении оплаты. Самый дешёвый вариант — контроллер с GSM-модемом.

**Что будет, если один QR отсканируют несколько человек?** Один QR — это один счёт. Поэтому он годится ровно на одну продажу, и после каждой экран нужно возвращать в исходное состояние.

**У автомата нет экрана, только кнопки. Как быть?** Придётся поставить небольшой дисплей. Если показать QR негде, схема не работает.

**Покупатель отсканировал, и тут пропала связь. Где деньги?** Деньги приходят на ваш счёт в Kaspi, от автомата это не зависит. Если товар выдать не удалось — делайте `refund`. Поэтому записывайте в журнал каждую команду контроллеру.

**Можно один ключ на десять автоматов?** Можно, но лучше отдельный ключ на точку: и отчётность разделится, и при утечке вы удалите только один ключ, не трогая остальные.
