Заменяет модель BOOKING/BID — переопределяет user_id/вводит
client_id, добавляет WAIT_PAYMENT/REJECTED, историю переходов и
разделение точек входа создания. Код под это ещё не написан — только дизайн-документы.
WAIT_PAYMENT → BOOKED закладывается как
системное действие по колбэку, сам шлюз не подключается.BID или WAIT_PAYMENT.housing.user_id), не через роли.| Поле Booking | Тип | Смысл |
|---|---|---|
user_id | int | Переопределяется: владелец брони, всегда = housing.user_id, проставляется бэкендом. |
client_id | int? | Новое. Реальный аккаунт клиента, если известен. |
reason_rejected | text? | Новое. Причина отмены. |
status | enum | Набор значений полностью заменяется. |
source | enum | Переосмысливается как «кто создал» (MANUAL/GUEST/OTA). |
ota_platform | enum? | Новое. Конкретная внешняя площадка (AVITO/SUTOCHNO/...), заполняется только при source === OTA. |
| id | Название | Блокирует даты в поиске |
|---|---|---|
BID | Заявка | Нет |
WAIT_PAYMENT | Ожидание оплаты | Нет |
BOOKED | Бронь | Да |
REJECTED | Отменена | Нет |
Старое значение BOOKING переименовывается в BOOKED везде по коду. Default в БД остаётся BID.
Старый вариант (MANUAL/AVITO/SUTOCHNO в одном enum) смешивал два разных вопроса — кто инициировал заявку и через какую конкретно внешнюю площадку она пришла. Разделено на два поля.
| id | Название | Смысл |
|---|---|---|
MANUAL | Вручную | Владелец создал сам в ЛК |
GUEST | От гостя | Гость создал через confirm (см. «Гостевой флоу») |
OTA | С площадки | Синхронизировано с внешней площадкой — какой именно, см. ota_platform |
Новый catalog-файл shared/catalogs/booking/ota-platform.ts, тот же паттерн createType():
| id | Название |
|---|---|
AVITO | Авито |
SUTOCHNO | Суточно |
Заполняется в Booking.ota_platform только когда source === OTA. Расширяется добавлением строки в этот список — не трогает BookingSourceType. Каждая площадка в будущем получит свой класс-адаптер синхронизации, подключаемый по значению ota_platform (например реестр Record<OtaPlatformType, OtaAdapter>) — сам интерфейс адаптера не проектируется сейчас.
booking_status_history
id
booking_id FK -> booking, CASCADE
from_status nullable enum (null для создания)
to_status enum
changed_by_user_id nullable int (null = системное действие)
reason nullable text
created_at timestamp
Пишется при каждом изменении статуса, включая создание. Экспонируется как Booking.history (join, только если запрошено — паттерн unavailable_reasons).
BID → WAIT_PAYMENT → BOOKED → REJECTED — последовательно вперёд, «назад» нет, кроме прыжка в REJECTED из любого статуса. Прыжок через ступень вперёд (BID → BOOKED) разрешён. REJECTED — терминальный.
| Переход | Владелец | Клиент | Система |
|---|---|---|---|
BID → WAIT_PAYMENT | ✓ | — | — |
BID → BOOKED | ✓ | — | — |
WAIT_PAYMENT → BOOKED | override | — | основной путь |
* → REJECTED | всегда* | пока BID/WAIT_PAYMENT | каскад |
*OTA-защита: если booking.source === BookingSourceType.OTA, владелец не может перевести бронь в REJECTED вообще — только сама OTA-интеграция.
Раньше правило было плоским — «владелец может всё, кроме OTA». На практике владелец различает три типа заявок на своём объекте по-разному:
| source | editBooking | Отмена (→ REJECTED) |
|---|---|---|
MANUAL | ✓ | ✓ |
GUEST | ✗ | ✓ |
OTA | ✗ | ✗ |
Прямое сравнение booking.source, без нового поля прав: editBooking — caller.id === housing.user_id AND source === MANUAL; отмена владельцем — caller.id === housing.user_id AND source !== OTA. Заявку, которую владелец завёл сам, он полностью контролирует; заявку от гостя не может подменить (исказило бы то, что гость реально запросил), но может отклонить; бронь с OTA не может тронуть вообще.
editBooking (даты, цена, контакты, заметка): только владелец, и только когда source === MANUAL (см. выше), в любом статусе кроме REJECTED. Поле status из BookingEditDto убирается — смена статуса только через отдельную мутацию.
Владелец не выбирает статус вручную — одно действие «Подтвердить» на заявке в BID:
BID → WAIT_PAYMENT. Клиент платит → система по колбэку сама переводит → BOOKED.
BID → BOOKED напрямую, WAIT_PAYMENT полностью пропускается.
BOOKED.housing_id все другие брони не BOOKED/REJECTED, чьи даты пересекаются (exclusive-end, как в checkRangeOverlap).REJECTED, reason_rejected = системный текст, changed_by_user_id = null.Должно идти в одной транзакции с переходом в BOOKED (SELECT ... FOR UPDATE на бронях объекта в диапазоне дат) — иначе гонка при параллельном подтверждении двух пересекающихся заявок.
| Мутация | Кто | Правило |
|---|---|---|
addBooking(data) | owner | Только свой объект (housing.user_id === caller.id — сейчас проверки нет, добавляется). Статус на выбор. source: MANUAL. |
addBid(data) | гость | Любой housing_id. Статус всегда BID. user_id = housing.user_id, client_id = caller, source: GUEST. |
changeBookingStatus(id, status, reason?) | owner/client | Единая точка для всех переходов — граф, OTA-блокировка, история, каскад. |
editBooking(id, data) | owner | Прочие поля. Только при source === MANUAL (см. «Права владельца по source»). status убран из DTO. |
deleteBooking | — | Убирается совсем — REJECTED с историей покрывает смысл. |
Backend сейчас вообще не проверяет владение housing_id при addBooking —
любой авторизованный пользователь может прямым GraphQL-запросом создать бронь на чужой объект. Для
гостя (addBid) это ожидаемо; для владельца, создающего вручную, — дыра, закрывается
проверкой из раздела «Мутации».
Форма выбора дат на самой странице объекта (лендинг) в этот флоу не входит и не меняется — важно только то, что происходит после перехода на confirm.vue.
Переход на confirm.vue → редактируемая форма, live-цена → «Оплатить» → addBooking(status: BOOKING) → список броней.
На confirm.vue гость попадает одним из трёх путей — только что зарегистрировался, залогинился существующим аккаунтом, или уже был авторизован. Во всех случаях: сразу, без формы, addBid с параметрами из query → редирект в чат (popup_show_booking). confirm.vue в текущем виде не нужен.
BookingService.getBookingInfo сейчас фильтрует status: BOOKING — фильтр убирается, чат открывается с момента BID, доступен и после REJECTED. Проверка допуска в bid-chat-service's chat.service.ts меняется:
housing.user_id === user.id || booking.client_id === user.id
bookings.user_id = :userId
OR housing.user_id = :userId
OR bookings.client_id = :userId
| Файл | Изменение |
|---|---|
shared/catalogs/booking/status.ts | Новый набор статусов |
shared/catalogs/booking/source.ts | Переопределить на MANUAL/GUEST/OTA |
shared/catalogs/booking/ota-platform.ts | Новый catalog: AVITO/SUTOCHNO |
backend/.../booking.entity.ts | client_id, reason_rejected, ota_platform |
backend/.../booking-status-history.entity.ts | Новая сущность + модуль/сервис |
backend/.../booking.service.ts | add/addBid/переход статуса/каскад/addUserFilter/проверка владения/проверка source в edit и отмене/убрать delete() |
backend/.../booking.resolver.ts | Новые мутации, убрать deleteBooking |
backend/.../booking.controller.ts | Снять фильтр по статусу в getBookingInfo |
bid-chat-service/.../chat.service.ts | Проверка допуска owner/client |
front/.../confirm.vue | Переписать: авто-создание без формы + редирект в чат |
front/.../add.vue, api/booking/delete.js | Убрать кнопку/хелпер удаления → действие «Отменить» |
shared/ui/booking/form/booking-status.vue | Заменить raw-статус-пикер на кнопки-действия |
backend/test/booking.e2e-spec.ts | Массовая правка BOOKING→BOOKED + новые кейсы |
Переименование BOOKING → BOOKED — 10–12 мест только в TS-сравнениях статуса; pricing.sql не затронут.
client_id при ручном создании — по умолчанию оставляем возможность передать.client_id в user-service — можно отложить.addBid/changeBookingStatus — рабочие.WAIT_PAYMENT → BOOKED — не решается сейчас.