# Таксопарк: сбор платежей с водителей

> Массовое выставление счетов по списку водителей, защита от дублей через идемпотентность, разбор ошибок по каждому элементу отдельно и автоматизация ежедневного сбора.

## Коротко

Ежедневный или еженедельный сбор в таксопарке — это много однотипных счетов. Метод `POST /api/v1/invoices/bulk` создаёт от 1 до 100 счетов одним запросом: если передать номер водителя, счёт придёт push-уведомлением в его приложение Kaspi. Чтобы повторная отправка списка не наплодила дублей, используется `Idempotency-Key`. Ошибки приходят по каждому элементу отдельно — если один упал, остальные всё равно создадутся.

## Сценарий

В парке 80 водителей. Каждый платит ежедневную аренду или комиссию. Как это выглядело раньше: водитель приезжает в парк, отдаёт наличные диспетчеру или переводит со своей карты, диспетчер отмечает в таблице, а в конце месяца никто точно не знает, кто сколько заплатил.

Что нужно: утром счета уходят сразу всем 80 водителям, каждому на телефон, в кабинете видно кто оплатил, отчёт собирается сам.

## Схема работы по шагам

1. **Подготовка списка.** Ваша система (своя база, таблица, диспетчерская программа) формирует список водителей, которые должны заплатить сегодня: телефон, сумма, внутренний номер.
2. **Один запрос.** `POST /api/v1/invoices/bulk` — в теле массив счетов, 1-100 элементов. Если больше, разбиваете на пакеты.
3. **Идемпотентность.** К запросу добавляете заголовок `Idempotency-Key` с устойчивым значением, привязанным к дате (например «парк-сбор-2026-09-14»). Даже если задача запустится повторно, счета не выставятся дважды.
4. **Разбор ответа.** По каждому элементу приходит результат. Успешные записываете в базу, проблемные выводите в журнал.
5. **Push водителю.** Если выбран `kind: "phone"`, счёт приходит уведомлением в приложение Kaspi. Водитель открывает его и подтверждает.
6. **Вебхук.** При оплате приходит `invoice.paid` с `externalOrderId` — по нему находите водителя и закрываете его задолженность.
7. **Отчёт.** В конце дня фильтруете в кабинете или выгружаете CSV: кто заплатил, кто нет.

## Какие методы API нужны

| Что делает | Метод |
|---|---|
| Счета по списку | `POST /api/v1/invoices/bulk` (1-100) |
| Счёт отдельному водителю | `POST /api/v1/invoices` |
| Проверить статус | `GET /api/v1/invoices/{id}` |
| Список за период | `GET /api/v1/invoices` |
| Водитель не вышел на смену | `POST /api/v1/invoices/{id}/cancel` |
| Вернуть лишнее | `POST /api/v1/invoices/{id}/refund` |
| Узнать об оплате | Вебхук `invoice.paid`, результат массовой отправки `invoice.bulk` |

## Идемпотентность — то, что спасает от дублей

Ежедневный сбор — автоматическая задача, а автоматические задачи повторяются: cron запустился дважды, оборвалась сеть и запрос ушёл снова, диспетчер нажал кнопку два раза. Без идемпотентности каждому водителю уходит два счёта, телефон звонит дважды, а кто-то платит оба раза.

Решение — заголовок `Idempotency-Key`. Значение должно быть устойчивым: собирайте его из даты и номера пакета, а не из случайного числа. При повторе с тем же ключом новый счёт не создаётся, возвращается прежний (HTTP 200, `idempotentReplay: true`).

Обработка вебхука тоже должна быть идемпотентной: проверяйте пару `(invoice.id, status)`, потому что при ответе не 2xx мы повторим доставку 11 раз. Подробнее: [Идемпотентность](/kb/ru/idempotency).

Если дубли всё-таки пошли, есть порядок экстренной остановки: [Счета дублируются](/kb/ru/duplicate-invoices).

## Разбор ошибок по каждому элементу

В методе `bulk` **каждый элемент проверяется сам по себе**. Пакет не падает целиком: из 80 счетов создадутся 77, а 3 вернут ошибку.

Частые причины:

| Причина | Что делать |
|---|---|
| Формат телефона не `7XXXXXXXXXX` | Почистить номера в базе |
| Сумма ноль или отрицательная | Проверить логику расчёта |
| У счёта на телефон есть тиыны | Округлить до целых тенге |
| Слишком длинное описание | У счёта на телефон 60 символов, у QR — 100 |
| Исчерпан месячный лимит | Поднять тариф или разнести сбор |
| Сработала суточная защита | Разнести пакеты по времени |

Сопоставьте строки ответа с порядком своего списка, проблемные поставьте в отдельную очередь и отправьте после исправления. Весь пакет заново отправлять не нужно — даже с ключом идемпотентности это лишняя нагрузка.

Месячный лимит даёт ошибку `tariff_limit_reached`, суточная защита — `tariff_daily_burst`. Это разные вещи: суточное число не бизнес-лимит, а предохранитель от интеграции, ушедшей в цикл. Подробнее: [Достигнут лимит — что делать](/kb/ru/tariff-limit-hit).

## Автоматизация ежедневного сбора

Устойчивый порядок работы:

1. Утром в заданное время запускается задача.
2. Формируется список водителей, которые платят сегодня (вышедшие на смену, с задолженностью).
3. Список делится на пакеты не более 100 элементов.
4. Каждый пакет отправляется через `bulk`, `Idempotency-Key` — дата и номер пакета.
5. Между пакетами ставится пауза в несколько секунд — не отправляйте сотни запросов залпом.
6. Результат пишется в базу, проблемные элементы попадают в очередь повторной отправки.
7. Ближе к вечеру уходит вторая волна напоминаний тем, кто не заплатил.

Сначала прогоните весь цикл на ключе песочницы (`qp_test_…`): реальные деньги не двигаются, оплату вы ставите сами через `simulate`.

## Отчётность и выгрузка

Расставляйте пометки в момент создания счёта, тогда собрать отчёт будет просто:

- `externalOrderId` — внутренний номер водителя или строка вида «дата + водитель».
- `metadata` — номер машины, смена, диспетчер, подразделение парка.
- `description` — текст, который видит водитель: «Аренда, 14 сентября».

В разделе **Счета** кабинета фильтруете по периоду и статусу и выгружаете CSV. Этот же файл отдаёте в бухгалтерию. Подробнее: [Экспорт CSV и отчётность](/kb/ru/csv-export).

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

## Особые замечания

- **Номер водителя должен быть зарегистрирован в Kaspi.** Человеку без приложения Kaspi счёт на телефон не придёт — дайте ему QR или ссылку `payUrl`.
- **Номер кассира виден водителю** в уведомлении о счёте. Это нормальная работа Kaspi.
- **Если водитель не заплатил**, счёт уйдёт в `expired`. Предусмотрите в своей системе перенос задолженности на следующий день.
- **Поздние оплаты случаются.** Если деньги придут по просроченному счёту, событие `invoice.paid` придёт с признаком `late: true` — пересчитайте баланс или сделайте возврат.
- **Заранее посчитайте месячный лимит.** 80 водителей × 30 дней = 2 400 счетов, плюс повторные отправки.

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

**Сколько счетов можно отправить одним запросом?** От 1 до 100. Больше — разбивайте на пакеты.

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

**Если задача запустится дважды, будут дубли?** При корректном `Idempotency-Key` — нет. Значение ключа должно быть устойчивым и привязанным к дате.

**Если один элемент пакета упал, остальные создадутся?** Да. Каждый элемент проверяется отдельно, остальные обрабатываются как обычно.

**Куда приходят деньги?** Напрямую на Kaspi-счёт вашего парка. У нас они не задерживаются, процент с транзакций мы не берём — оплата сервиса это месячная подписка.
