# Места в зале

Билеты на конкретные места: тариф с местами, схема зала файлом CSV и заказ места по его подписи или вашему идентификатору.

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

## Как завести событие с местами {#setup}

1. [Создайте событие](https://budu.events/docs/events/#create) с тарифом `"seated": true`. Квоту такому тарифу не передают: она равна числу его мест.
2. Загрузите места файлом CSV: [`POST /v1/events/{id}/seats`](#import). Сначала файл можно проверить с `?dry_run=1`.
3. [Опубликуйте событие](https://budu.events/docs/events/#publish). Тариф с местами без загруженных мест опубликовать нельзя.

Места меняются только у черновика. Загрузка заменяет все места события целиком.

## Формат CSV {#csv}

```csv
sector,row,number,label,external_id,tariff
Партер,1,1,П-1-1,hall-0001,parter
Партер,1,2,,hall-0002,parter
"Балкон, левая сторона",1,1,Б-1-1,hall-0101,balcony
```

| Столбец | Обязательно | Описание |
|---|---|---|
| `sector` | да | Сектор, до 50 символов |
| `row` | да | Ряд, до 20 символов |
| `number` | да | Номер места, до 20 символов |
| `label` | нет | Подпись места, уникальна в событии, до 100 символов. Пусто: `sector-row-number`, во второй строке примера `Партер-1-2` |
| `external_id` | нет | Ваш идентификатор места, например из схемы зала. Уникален в событии и не совпадает ни с одной подписью |
| `tariff` | если тарифов с местами несколько | Код или название тарифа с местами |

- Первая строка — заголовок. Регистр и пробелы вокруг названий не важны, порядок столбцов любой. Другой столбец — ошибка.
- Разделитель `,` или `;` (Excel с русскими настройками сохраняет через `;`). Кавычки по RFC 4180, BOM и переводы строк Windows допускаются.
- Кодировка только UTF-8. В Excel сохраняйте как «CSV UTF-8».
- Пустые строки пропускаются. Управляющие символы внутри значений не допускаются.
- Порядок строк — порядок мест в зале: в нём места отдаются в списке и в остатках.
- До 50 000 мест и 5 МБ на файл.

## Загрузить места {#import}

`POST /v1/events/{id}/seats`

Тело запроса — сам файл, `Content-Type: text/csv`. Файл принимается целиком или не принимается совсем. `?dry_run=1` проверяет и считает места, ничего не записывая.

```bash Запрос
curl "https://api.budu.events/v1/events/0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60/seats?dry_run=1" \
  -H "Authorization: Bearer $BUDU_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @hall.csv
```

```json Ответ 200
{
  "total": 3,
  "per_tariff": [
    {"tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c73", "count": 2},
    {"tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c74", "count": 1}
  ],
  "errors": [],
  "sample": [
    {
      "id": "0193e0b3-0a11-7f00-8d2b-41c6e9a7b001",
      "tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c73",
      "sector": "Партер",
      "row": "1",
      "number": "1",
      "label": "П-1-1",
      "external_id": "hall-0001",
      "position": 1
    }
  ]
}
```

`sample` — первые 20 мест для предпросмотра. Если в файле есть ошибки, ответ будет `400 invalid_request`, ничего не запишется, а ошибки окажутся в `details.errors`:

```json Ответ 400
{
  "error": {
    "code": "invalid_request",
    "message": "В файле мест есть ошибки — места не загружены",
    "details": {
      "errors": [
        {"line": 3, "message": "Место «П-1-1» повторяется: строки 2 и 3"}
      ]
    }
  }
}
```

`line` — номер строки файла, заголовок — строка 1. `line: 0` относится к файлу целиком. В ответе до 100 ошибок.

`Idempotency-Key` здесь не нужен: повтор того же файла даёт тот же результат. После публикации загрузка отвечает `400` «Места можно загрузить только до публикации».

## Список мест {#list}

`GET /v1/events/{id}/seats`

| Параметр | Описание |
|---|---|
| `limit` | Мест на странице, от 1 до 5 000, по умолчанию 1 000 |
| `after` | `id` последнего места предыдущей страницы |
| `sector` | Только этот сектор |

```bash Запрос
curl "https://api.budu.events/v1/events/0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60/seats?limit=2" \
  -H "Authorization: Bearer $BUDU_KEY"
```

```json Ответ 200
{
  "seats": [
    {
      "id": "0193e0b3-0a11-7f00-8d2b-41c6e9a7b001",
      "tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c73",
      "sector": "Партер",
      "row": "1",
      "number": "1",
      "label": "П-1-1",
      "external_id": "hall-0001",
      "position": 1
    },
    {
      "id": "0193e0b3-0a11-7f00-8d2b-41c6e9a7b002",
      "tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c73",
      "sector": "Партер",
      "row": "1",
      "number": "2",
      "label": "Партер-1-2",
      "external_id": "hall-0002",
      "position": 2
    }
  ],
  "last": false,
  "sectors": ["Партер", "Балкон, левая сторона"]
}
```

Места идут в порядке файла. Следующая страница: `after=<id последнего места>`, пока `last` не станет `true`. `sectors` приходит только на первой странице.

## Состояния мест {#states}

[`GET /v1/events/{id}/availability?seats=1`](https://budu.events/docs/events/#availability) добавляет к остаткам страницу состояний мест: `seats: [{seat_id, tariff_id, sector, label, state}]` и `seats_last`. Страницы листаются так же, параметрами `seats_after` и `seats_limit`.

| `state` | Что значит |
|---|---|
| `free` | Свободно |
| `held` | В резерве у заказа. Освободится, если резерв отменят или он истечёт |
| `sold` | Продано. Снова станет `free`, если билет отменят с возвратом в продажу |

## Заказ места {#order}

В [`POST /v1/orders`](https://budu.events/docs/orders/#create) и [`POST /v1/reservations`](https://budu.events/docs/orders/#reserve) позиция с местом выглядит как `{"seat": "<ссылка>"}` вместо `{"tariff_id", "qty"}`:

```json
{
  "event_id": "0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60",
  "items": [
    {"seat": "П-1-1"},
    {"seat": "hall-0002"},
    {"tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c71", "qty": 2}
  ],
  "buyer": {
    "email": "anna@example.ru",
    "first_name": "Анна",
    "last_name": "Петрова"
  }
}
```

- Ссылка на место — его `id`, `external_id` или `label`. Мы проверяем в этом порядке.
- `qty` у места `0` или `1`. `tariff_id` можно не указывать, а если указан, он должен совпадать с тарифом места.
- Тариф с местами нельзя заказать количеством: `400` «Для этого тарифа выберите места».
- Место считается за один билет в лимите 20 на заказ и в `per_order_limit` тарифа.
- В заказе и билете место приходит объектом `seat: {id, sector, row, number, label}`. Гость видит «Сектор · Ряд · Место» на билете, в письме и в календаре.

Ошибки: `409 seat_taken`, если место в резерве у другого заказа или продано (`details.seat_id`, `details.seat`). `404 not_found`, если места нет в событии (`details.seat`). `409 event_not_on_sale` с «Продажа мест ещё не открыта», если после публикации прошло несколько секунд и места ещё загружаются в продажу.
