# Возвраты

:::caution{title="Возможность в подготовке"}

Заявки на возврат через API ещё **не включены**. Ниже — как это будет работать: так вы можете
заранее заложить их в свою CRM. Адреса и поля могут уточниться к выпуску; когда заявки появятся
в [справочнике API](/api), они включены.

**Сейчас** вернуть деньги можно только через лагерь: сообщите лагерю номер брони, сумму и причину.
Возврат вы увидите в платежах брони (`GET /bookings/{id}/payments`) — платёж с `type: refund` и
отрицательной суммой — и событием `payment.refunded`.

:::

## Главное

- Возврат — **только по заявке**. Ваша CRM подаёт заявку, деньги **не двигаются**, пока её не
  одобрит сотрудник лагеря с правом «Одобрять возвраты». Одобрить заявку или «вернуть сразу» через
  API нельзя никогда.
- Возвращается **только оплата по счёту Kaspi, который выставила система лагеря**. Деньги уходят
  обратно тому, кто платил, через тот же Kaspi.
- Оплату другим способом (наличные, перевод на карту, банковский перевод) возвращает только лагерь
  вручную.
- Деньги возвращаются **с того же юрлица** (ИП или ТОО), которое их приняло. Деньги одной брони
  между юрлицами не делятся.
- Отмена брони сама по себе деньги не возвращает. Отменили бронь, по которой платили, — подайте
  заявку на возврат.

## Путь заявки

```
requested ──► approved ──► sending ──► done      деньги у плательщика
    │                          │
    │                          ├──► failed       Kaspi отказал, денег не ушло
    │                          │
    │                          └──► needs_check  Kaspi не ответил — лагерь проверяет вручную
    │
    ├──► rejected   лагерь отклонил (причина в reject_reason)
    └──► cancelled  заявку отозвали
```

| `state`       | Что значит                                                                         |
| ------------- | ---------------------------------------------------------------------------------- |
| `requested`   | Заявка подана и ждёт решения лагеря                                                |
| `approved`    | Лагерь одобрил, возврат в очереди                                                  |
| `sending`     | Возврат отправлен в Kaspi                                                          |
| `needs_check` | Kaspi не ответил, ушли ли деньги. Лагерь проверит вручную; **сама заявка не повторяется** — двойной возврат не исправить |
| `done`        | Деньги вернулись плательщику. Рядом появляется платёж-возврат (`refund_payment_id`) |
| `failed`      | Kaspi отказал, денег не ушло. Подайте новую заявку или обратитесь в лагерь          |
| `rejected`    | Лагерь отклонил заявку — причина в `reject_reason`                                 |
| `cancelled`   | Заявку отозвали                                                                    |

Список состояний может пополняться.

## Подать заявку

`POST /bookings/{id}/refund-requests` с `Idempotency-Key`, право `bookings`:

```sh
curl https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings/15230/refund-requests \
  -H "Authorization: Bearer $ZB_KEY" \
  -H "Idempotency-Key: 8e4b0d2a-c7f1-4a39-b6e5-0f2d9a7c3b18" \
  -H "Content-Type: application/json" \
  -d '{
    "payment_id": "9120",
    "amount": "50000",
    "reason": "Ребёнок заболел, семья отказалась от поездки"
  }'
```

| Поле         | Что это                                                                                   |
| ------------ | ----------------------------------------------------------------------------------------- |
| `payment_id` | Номер оплаты брони по счёту Kaspi (`id` из `GET /bookings/{id}/payments`)                 |
| `amount`     | Сколько вернуть: целые тенге строкой (`"50000"`). Не больше оплаты за вычетом прежних и ждущих возвратов по ней |
| `reason`     | Почему возвращаем — увидит сотрудник лагеря, который решает. От 1 до 500 знаков          |

Ответ `201`:

```json
{
  "id": "41",
  "booking_id": "15230",
  "payment_id": "9120",
  "invoice_id": "731",
  "amount": { "amount": "50000.00", "currency": "KZT" },
  "reason": "Ребёнок заболел, семья отказалась от поездки",
  "state": "requested",
  "reject_reason": null,
  "by_api": true,
  "created_at": "2027-06-02T08:12:40.551023Z",
  "decided_at": null,
  "finished_at": null,
  "refund_payment_id": null,
  "version": "2027-06-02T08:12:40.551023Z"
}
```

`by_api: true` — заявку подало ваше подключение (а не сотрудник лагеря).

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

## Следить за заявкой

- `GET /bookings/{id}/refund-requests` — заявки по брони;
- `GET /refund-requests/{id}` — одна заявка;
- события `refund_request.created`, `refund_request.approved`, `refund_request.rejected`,
  `refund_request.cancelled`, `refund_request.done`, `refund_request.failed`. В `object` события —
  заявка целиком.

Когда деньги ушли (`done`), приходит и событие `payment.refunded`: у брони появился платёж с
`type: refund` и **отрицательной** суммой, `paid` уменьшилось, `debt` вырос.

## Отказы

| Ответ                            | Что значит                                                                 |
| -------------------------------- | -------------------------------------------------------------------------- |
| `400 unknown_payment`            | Оплаты с таким номером нет в этой брони                                    |
| `400 refund_not_kaspi_invoice`   | Оплата пришла не по счёту Kaspi — её возвращает только лагерь              |
| `400 reason_required`            | Не указана причина                                                         |
| `400 bad_reason`                 | Причина длиннее 500 знаков                                                 |
| `400 bad_amount`                 | Сумма не по шаблону или не больше нуля                                     |
| `400 fractional_amount`          | Сумма с тиынами — Kaspi возвращает только целые тенге                      |
| `409 refund_exceeds_refundable`  | Сумма больше того, что по оплате ещё можно вернуть                         |
| `409 refund_no_cashdesk`         | У счёта оплаты нет кассы Kaspi — вернуть может только лагерь вручную       |
| `403 refund_approve_forbidden`   | Попытка одобрить возврат через API — его одобряет только лагерь            |
| `404 not_found`                  | Брони нет или она не вашего отдела                                         |
