# Изменения брони

Что можно сделать с бронью после оформления: изменить данные, отменить, восстановить, перенести на
другую смену, поселить детей вместе, получить путёвку. Для всех изменений нужно право `bookings`,
для чтения — `read`.

| Нужно                       | Запрос                                 |
| --------------------------- | -------------------------------------- |
| Изменить данные             | `PATCH /bookings/{id}`                 |
| Отменить                    | `POST /bookings/{id}/cancel`           |
| Восстановить отменённую     | `POST /bookings/{id}/restore`          |
| Перенести на другую смену   | `POST /bookings/{id}/move`             |
| Поселить вместе             | `POST /bookings/{id}/together`         |
| История изменений           | `GET /bookings/{id}/history`           |
| Путёвка и её PDF            | `GET /bookings/{id}/voucher`, `GET /bookings/{id}/voucher.pdf` |
| Найти по номеру вашей CRM   | `GET /bookings/by-external-id/{external_id}` |

## Изменить бронь

`PATCH /bookings/{id}` — передайте **только то, что меняется**. Чего нет в теле — не меняется.
`Idempotency-Key` для `PATCH` не нужен: повтор той же правки даёт тот же результат.

```sh
curl -X PATCH https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings/15230 \
  -H "Authorization: Bearer $ZB_KEY" \
  -H 'If-Match: "2027-05-14T10:02:45.918201Z"' \
  -H "Content-Type: application/json" \
  -d '{
    "payer_phone": "8 702 000 00 02",
    "needs_transfer": false,
    "comment": "Платит бабушка, трансфер не нужен — привезут сами"
  }'
```

Ответ `200` — бронь целиком, уже с изменениями и новой `version`.

Что можно менять:

| Поле                     | Как                                                                                   |
| ------------------------ | ------------------------------------------------------------------------------------- |
| `child.full_name`, `child.birth_date`, `child.city_id` | ФИО, дата рождения, город ребёнка                          |
| `parent.full_name`, `parent.phone` | ФИО и телефон родителя                                                      |
| `payer_phone`            | Телефон плательщика. Пустая строка `""` — платит родитель                              |
| `needs_transfer`, `transfer_point_id` | Трансфер и точка посадки                                                  |
| `discount`               | Скидка (`amount`, `note`) — в пределах `max_discount` смены                           |
| `comment`                | Комментарий                                                                           |
| `external_id`            | Не передано — не менять; строка — заменить; `null` — стереть                          |
| `metadata`               | Заменяет ваши метки **целиком** (передайте все пары, которые должны остаться)         |
| `partner_manager`        | Кто продал. Пустая строка — снять                                                     |

Что важно:

- **Деньги пересчитываются сами.** Изменили трансфер или скидку — меняются `total` и `debt`, а
  живой счёт Kaspi на старую сумму лагерь гасит и выставляет новый (см.
  [Счета Kaspi](/partners/kaspi-invoices#изменение-брони-и-счёт)).
- **ФИО или телефон у брони без даты рождения.** Если у брони (старой, оформленной ещё в CRM
  лагеря) нет даты рождения ребёнка, сменить ФИО ребёнка, ФИО или телефон родителя можно только
  вместе с датой рождения — иначе `400 birth_date_required`.
- **Ребёнок перешёл в другой отдел** (`contacts_hidden: true`) — телефоны родителя и плательщика
  менять нельзя (`403 contacts_hidden`): контакты меняет отдел, который ведёт ребёнка сейчас.
- Неизвестное поле в теле — `422 invalid_input`.

### If-Match: не затереть чужую правку

Бронь может одновременно править ваш менеджер, другой менеджер отдела и сотрудник лагеря. Чтобы
ваша правка не легла поверх чужой, передайте в `If-Match` значение `version` брони, которую вы
показывали менеджеру. Если бронь с тех пор изменили — ответ `412 version_mismatch`: перечитайте
бронь, покажите менеджеру новые данные и повторите. Без `If-Match` правка применяется к текущей
версии.

## Отменить бронь

`POST /bookings/{id}/cancel` с `Idempotency-Key`. **Причина обязательна.** Можно передать `If-Match`.

```sh
curl https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings/15230/cancel \
  -H "Authorization: Bearer $ZB_KEY" \
  -H "Idempotency-Key: 2d7e9c41-6a0b-4f85-b3e2-1c8f5a9d0e67" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Семья передумала" }'
```

Ответ `200` — бронь с `status: cancelled` и `cancelled_reason`. Придёт событие `booking.cancelled`.

Что происходит при отмене:

- место возвращается в квоту вашего отдела;
- неоплаченный счёт Kaspi гасится;
- **внесённые деньги автоматически не возвращаются.** Вернуть их родителю можно заявкой на возврат
  (возможность в подготовке, см. [Возвраты](/partners/refunds)) или через лагерь.

Отказы: `400 reason_required` — нет причины; `409 already_cancelled` — уже отменена;
`409 already_checked_in` — ребёнок уже заехал в лагерь, такую бронь не отменить (обратитесь в
лагерь); `412 version_mismatch` — бронь изменили после вашего чтения.

## Восстановить отменённую бронь

`POST /bookings/{id}/restore` с `Idempotency-Key`, без тела. Подходит и для брони, снятой по
неоплате (`booking.hold_expired`), когда семья всё-таки решила ехать.

```sh
curl -X POST https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings/15230/restore \
  -H "Authorization: Bearer $ZB_KEY" \
  -H "Idempotency-Key: 7f0a3e58-9c2d-4b16-a4e7-5d1b8c3f2a90"
```

Бронь снова занимает место в квоте (если оно ещё есть), снова **держит его 24 часа** до
минимального платежа, и плательщику уходит счёт Kaspi. Ответ `200` — бронь, событие
`booking.restored`.

Отказы: `409 not_cancelled` — бронь не отменена; `409 quota_exhausted` — места закончились;
`409 sales_closed` — продажи закрыты; `409 duplicate_child` — ребёнка уже записали на эту смену
другой бронью.

## Перенести на другую смену

`POST /bookings/{id}/move` с `Idempotency-Key`. Работает как кнопка лагеря «Остаётся на следующую
смену»:

```sh
curl https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings/15230/move \
  -H "Authorization: Bearer $ZB_KEY" \
  -H "Idempotency-Key: c3b8a1f6-4e2d-49a7-8051-e6d9f2b7c014" \
  -H "Content-Type: application/json" \
  -d '{ "shift_id": "13" }'
```

Ответ `201` — **новая** бронь на смену 13 (событие `booking.created`).

:::caution{title="Перенос — это новая бронь, а не перемещение старой"}

- Новая бронь оформляется **по цене новой смены**, **без прежней скидки**, с удержанием места на
  24 часа и счётом Kaspi на минимальный платёж.
- **Прежняя бронь остаётся как есть** — со своим местом, счетами и платежами. Деньги на новую бронь
  не переезжают.
- Если прежнее место больше не нужно, **отмените прежнюю бронь отдельно**. Что делать с уже
  внесёнными по ней деньгами — решите с лагерем.

:::

Отказы: `409 same_shift` — бронь уже на этой смене; `409 shift_started` — смена уже началась;
`409 quota_exhausted`, `sales_closed`, `duplicate_child` — как при оформлении; `403 contacts_hidden`
— ребёнка ведёт другой отдел, переносит он.

## Поселить вместе

Просьба лагерю поселить детей в один отряд (друзья, братья и сёстры). Все брони — ваши,
действующие и на одной смене, не больше 50.

```sh
curl https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings/15230/together \
  -H "Authorization: Bearer $ZB_KEY" \
  -H "Idempotency-Key: 5a9d2f73-b1e8-4c06-9f3a-8e7c1d4b6a25" \
  -H "Content-Type: application/json" \
  -d '{ "booking_ids": ["15231", "15240"], "note": "Одна компания, просили родители" }'
```

Ответ `201`:

```json
{ "id": "318", "booking_ids": ["15230", "15231", "15240"] }
```

Можно сразу при оформлении — поле `together_with` в `POST /bookings`, или `together: true` в
`POST /bookings/batch` для детей одной семьи. Номер чужой брони — отказ `400 unknown_booking`.

## Путёвка

- `GET /bookings/{id}/voucher` — статус путёвки: `none` — не сформирована, `generated` —
  сформирована, `sent` — отправлена семье.
- Скачать PDF — проще всего по ссылке `pdf_url` из `GET /bookings/{id}/voucher`: ключ к ней не
  нужен, её можно сразу отдать браузеру или мессенджеру, файл придёт с именем ребёнка. Ссылка живёт
  10 минут (`pdf_url_expires_at`) — не храните её, за новой снова вызовите `GET /bookings/{id}/voucher`.
- Если `pdf_url` равен `null` (путёвка не выпущена или файл для вашего отдела собирается заново),
  скачайте `GET /bookings/{id}/voucher.pdf` с ключом — тяжёлый запрос, не чаще раза в секунду.

```json
{
  "booking_id": "1042",
  "number": "05-012",
  "status": "generated",
  "pdf_available": true,
  "pdf_url": "https://hb.kz-ast.bizmrg.com/…/05-012.pdf?X-Amz-Signature=…",
  "pdf_url_expires_at": "2026-11-04T10:10:00Z"
}
```
- События `voucher.ready` и `voucher.sent`.

Выпустить путёвку и отметить её отправленной через ключ API нельзя (`403 voucher_head_only`) — это
делает руководитель вашего отдела или лагерь.

## История брони

`GET /bookings/{id}/history` — кто и когда что менял в брони, порциями. Удобно, когда менеджер
спрашивает «почему изменилась сумма».

## Заезд и отъезд

Когда ребёнок приехал в лагерь и уехал, видно в брони — поле `checkin` (`checked_in_at`,
`checked_out_at`) — и приходит событиями `child.checked_in`, `child.checked_out`. Через API отметить
заезд нельзя: это делает лагерь на месте.
