docs(spec): POST bookings occurrence_start. Refs EventHub/EventHubBack#67
This commit is contained in:
+58
-15
@@ -19,10 +19,20 @@ EventHub — платформа для управления событиями
|
|||||||
- Гибкое подтверждение заявок: `auto` | `manual` | `{timeout, N}` (секунды) — детали §2.1.2 / §2.3
|
- Гибкое подтверждение заявок: `auto` | `manual` | `{timeout, N}` (секунды) — детали §2.1.2 / §2.3
|
||||||
- Теги календаря, рейтинг (средняя оценка, количество голосов)
|
- Теги календаря, рейтинг (средняя оценка, количество голосов)
|
||||||
- После успешной верификации email (`POST /v1/verify`, статус пользователя → `active`) система
|
- После успешной верификации email (`POST /v1/verify`, статус пользователя → `active`) система
|
||||||
идемпотентно создаёт дефолтный **personal**-календарь владельцу (`logic_calendar:ensure_default_calendar/1`):
|
идемпотентно нормализует **единственный personal** владельца
|
||||||
название — `nickname` или «Мой календарь», `confirmation=manual`. Повторный вызов не создаёт дубликат,
|
(`logic_calendar:ensure_default_calendar/1` → `normalize_owner_personals/1`):
|
||||||
если у пользователя уже есть active personal-календарь. Существующим пользователям без календаря
|
- title в БД — **`Default`** (EN); UI — i18n «По умолчанию»;
|
||||||
backfill не выполняется.
|
- `confirmation=manual`;
|
||||||
|
- если personal нет — создаёт; если есть — оставляет **самый ранний** (`created_at`), title → `Default`;
|
||||||
|
- лишние personal: с событиями/специалистами → `type=commercial` (system path, без требования
|
||||||
|
subscription); пустые → soft-delete.
|
||||||
|
- Повторный вызов идемпотентен. Миграция `20260730200000_single_default_personal` —
|
||||||
|
`backfill_single_personal/0` для всех владельцев.
|
||||||
|
- `POST /v1/calendars` с `type=personal` при уже существующем personal → **409**
|
||||||
|
`{error: "personal_exists"}`.
|
||||||
|
- `DELETE` единственного personal → **403** `{error: "default_calendar"}`.
|
||||||
|
- `PUT` `personal → commercial` на единственном personal → **403**
|
||||||
|
`{error: "default_calendar"}` (новый бизнес — отдельный create commercial).
|
||||||
|
|
||||||
**Новые поля (задача #12):**
|
**Новые поля (задача #12):**
|
||||||
- `short_name` — короткое уникальное имя для API и поиска
|
- `short_name` — короткое уникальное имя для API и поиска
|
||||||
@@ -105,6 +115,10 @@ Trial стартует **только** явным `POST /v1/subscription` с `a
|
|||||||
|
|
||||||
- `POST /v1/events/:id/bookings` только если календарь события commercial, `booking_open=true`,
|
- `POST /v1/events/:id/bookings` только если календарь события commercial, `booking_open=true`,
|
||||||
событие `active`, есть свободная вместимость.
|
событие `active`, есть свободная вместимость.
|
||||||
|
- Для **recurring** master в теле обязательно `occurrence_start` (ISO8601). Без поля → `400`
|
||||||
|
(`occurrence_start required`). Невалидное или отменённое вхождение → `400`.
|
||||||
|
Back материализует instance (`is_instance=true`, `master_id`) и вешает booking на его `id`.
|
||||||
|
Для `single` / уже материализованного instance тело опционально, `occurrence_start` игнорируется.
|
||||||
- **Pending занимает capacity** наравне с confirmed (защита от overbook при auto/timeout).
|
- **Pending занимает capacity** наравне с confirmed (защита от overbook при auto/timeout).
|
||||||
- Capacity: число booking со статусом `pending` | `confirmed`; `cancelled` и `expired` не считаются.
|
- Capacity: число booking со статусом `pending` | `confirmed`; `cancelled` и `expired` не считаются.
|
||||||
- Политика `confirmation` календаря при создании booking:
|
- Политика `confirmation` календаря при создании booking:
|
||||||
@@ -123,12 +137,26 @@ Trial стартует **только** явным `POST /v1/subscription` с `a
|
|||||||
|
|
||||||
#### Специалисты
|
#### Специалисты
|
||||||
|
|
||||||
- Специалист = существующий `user`, привязанный к commercial-календарю после **принятия приглашения**.
|
- Специалист = существующий `user`, привязанный к commercial-календарю
|
||||||
|
(`calendar_specialist`, `status` active|inactive).
|
||||||
- **Не путать** с `calendar_share` (права read/write/admin — фаза 2).
|
- **Не путать** с `calendar_share` (права read/write/admin — фаза 2).
|
||||||
|
|
||||||
|
##### Owner как specialist (одиночки)
|
||||||
|
|
||||||
|
- При **create** `commercial` (и при системном upgrade personal→commercial) бэкенд
|
||||||
|
**идемпотентно** создаёт строку `calendar_specialist` на `owner_id`:
|
||||||
|
`status=active`, `name` из nickname (иначе email), `specialization=[]`.
|
||||||
|
- Owner-строка **неудаляема**: `DELETE /v1/calendars/:id/specialists/:owner_id` →
|
||||||
|
`403` (`Owner specialist cannot be removed` / `owner_specialist_protected`).
|
||||||
|
- «Убрать себя из специалистов» = `PUT` с `status=inactive` (продуктовая галочка);
|
||||||
|
снова включить — `status=active`. Редактируются также `name` / `specialization`.
|
||||||
|
- Invite себе **не** требуется. Миграций/backfill старых календарей нет (wipe БД
|
||||||
|
на стендах при деплое).
|
||||||
|
|
||||||
##### Приглашение (`specialist_invite`)
|
##### Приглашение (`specialist_invite`)
|
||||||
|
|
||||||
Владелец commercial **не** вводит сырой `user_id` в продуктовом UI. Добавление — через invite:
|
Владелец commercial **не** вводит сырой `user_id` в продуктовом UI для **других**
|
||||||
|
специалистов. Добавление команды — через invite:
|
||||||
|
|
||||||
| Канал | Как |
|
| Канал | Как |
|
||||||
|-------|-----|
|
|-------|-----|
|
||||||
@@ -172,7 +200,10 @@ Trial стартует **только** явным `POST /v1/subscription` с `a
|
|||||||
- `POST /v1/calendars/:id/specialist-invites` — тело: `{ user_id }` **или** `{ email }`,
|
- `POST /v1/calendars/:id/specialist-invites` — тело: `{ user_id }` **или** `{ email }`,
|
||||||
опционально `name`, `specialization`
|
опционально `name`, `specialization`
|
||||||
- `DELETE /v1/calendars/:id/specialist-invites/:invite_id` — отмена pending (`cancelled`)
|
- `DELETE /v1/calendars/:id/specialist-invites/:invite_id` — отмена pending (`cancelled`)
|
||||||
- `PUT/DELETE /v1/calendars/:id/specialists/:user_id` — deactivate / remove уже принятого
|
- `PUT /v1/calendars/:id/specialists/:user_id` — update `name` / `specialization` / `status`
|
||||||
|
(в т.ч. owner)
|
||||||
|
- `DELETE /v1/calendars/:id/specialists/:user_id` — remove принятого; для
|
||||||
|
`user_id = owner_id` → **403** (см. Owner как specialist)
|
||||||
|
|
||||||
Invitee:
|
Invitee:
|
||||||
|
|
||||||
@@ -223,12 +254,11 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
|
|||||||
- Возвращать как одиночные, так и сгенерированные вхождения в едином списке.
|
- Возвращать как одиночные, так и сгенерированные вхождения в едином списке.
|
||||||
|
|
||||||
#### 2.2.3. Материализация при записи участника
|
#### 2.2.3. Материализация при записи участника
|
||||||
При записи участника на конкретное вхождение повторяющегося события:
|
`POST /v1/events/:id/bookings` на мастер серии:
|
||||||
- Система материализует (создаёт) физическую запись события для этого вхождения, если оно ещё
|
- в теле JSON: `occurrence_start` — время вхождения;
|
||||||
не было материализовано (например, для хранения количества записавшихся).
|
- если instance с этим `start_time` ещё нет — создаётся (`is_instance=true`, `master_id`);
|
||||||
- Материализованное событие имеет `is_instance = true` и ссылается на `master_id`.
|
- `booking.event_id` — id материализованного вхождения, не master.
|
||||||
- Запись участника (`booking`) всегда привязывается к конкретному экземпляру (материализованному
|
Одиночные события бронируются как раньше (тело может быть пустым `{}`).
|
||||||
или одиночному событию).
|
|
||||||
|
|
||||||
#### 2.2.4. Изменение и удаление серий
|
#### 2.2.4. Изменение и удаление серий
|
||||||
- При редактировании мастера можно применить изменения ко всем будущим экземплярам или создать
|
- При редактировании мастера можно применить изменения ко всем будущим экземплярам или создать
|
||||||
@@ -510,14 +540,27 @@ src/
|
|||||||
- `POST /v1/specialist-invites/:id/accept` | `…/decline` — ответ invitee.
|
- `POST /v1/specialist-invites/:id/accept` | `…/decline` — ответ invitee.
|
||||||
- `POST /v1/specialist-invites/accept` `{ token }` — accept по email deep-link.
|
- `POST /v1/specialist-invites/accept` `{ token }` — accept по email deep-link.
|
||||||
- `GET /v1/calendars/:calendar_id/events` — события календаря.
|
- `GET /v1/calendars/:calendar_id/events` — события календаря.
|
||||||
|
В JSON каждого события commercial-календаря — опциональное поле
|
||||||
|
`booking_occupancy`: `"free"` | `"pending"` | `"confirmed"`.
|
||||||
|
Считается по **active** bookings события (`pending` | `confirmed`);
|
||||||
|
`cancelled` / `expired` **не** дают занятость. Приоритет агрегата:
|
||||||
|
`confirmed` > `pending` > `free` (если есть хотя бы один confirmed →
|
||||||
|
`"confirmed"`; иначе если есть pending → `"pending"`; иначе `"free"`).
|
||||||
|
Для personal: поле можно omit или всегда `"free"`.
|
||||||
|
Virtual occurrences (expand списка): occupancy по **event id в ответе**
|
||||||
|
(материализованный instance, если он уже есть и его id отдан; иначе —
|
||||||
|
тот id, с которым Back отдаёт вхождение — обычно master / шаблон —
|
||||||
|
bookings смотрятся по этому id).
|
||||||
- `POST /v1/calendars/:calendar_id/events` — создать событие (тело может включать
|
- `POST /v1/calendars/:calendar_id/events` — создать событие (тело может включать
|
||||||
опциональный `specialist_id`; invalid → `400`).
|
опциональный `specialist_id`; invalid → `400`).
|
||||||
- `GET /v1/events/:id` — событие.
|
- `GET /v1/events/:id` — событие (тот же контракт `booking_occupancy`, что
|
||||||
|
у списка events выше).
|
||||||
- `PUT /v1/events/:id` — обновить событие (в т.ч. `specialist_id`; invalid → `400`).
|
- `PUT /v1/events/:id` — обновить событие (в т.ч. `specialist_id`; invalid → `400`).
|
||||||
- `DELETE /v1/events/:id` — удалить событие.
|
- `DELETE /v1/events/:id` — удалить событие.
|
||||||
- `GET /v1/events/:id/occurrences` — вхождения повторяющегося события.
|
- `GET /v1/events/:id/occurrences` — вхождения повторяющегося события.
|
||||||
- `DELETE /v1/events/:id/occurrences/:start_time` — отменить вхождение серии.
|
- `DELETE /v1/events/:id/occurrences/:start_time` — отменить вхождение серии.
|
||||||
- `POST /v1/events/:id/bookings` — запись на событие.
|
- `POST /v1/events/:id/bookings` — запись на событие. Recurring master: тело
|
||||||
|
`{ "occurrence_start": "<ISO8601>" }` обязательно; booking на материализованный instance.
|
||||||
- `GET /v1/events/:id/bookings` — список бронирований события (владелец).
|
- `GET /v1/events/:id/bookings` — список бронирований события (владелец).
|
||||||
- `GET /v1/bookings/:id` — статус бронирования.
|
- `GET /v1/bookings/:id` — статус бронирования.
|
||||||
- `PUT /v1/bookings/:id` — подтвердить/отклонить (`confirm`|`decline`); владелец —
|
- `PUT /v1/bookings/:id` — подтвердить/отклонить (`confirm`|`decline`); владелец —
|
||||||
|
|||||||
Reference in New Issue
Block a user