UC-BKG-002. Бронирование Резерв → Бронь
Назначение
Закрепление объекта за сделкой: агент из карточки сделки создаёт бронь BookingEntity в фазе Резерв
(короткая блокировка с таймером) и для бесплатной программы переводит её в Бронь до истечения
резерва. Дом полей — booking.md, условия — booking-program.md,
родитель — deal.md.
| Поле | Значение |
|---|
| ID | UC-BKG-002 |
| Контекст | crm (см. docs/06-services/crm/) |
| Тип | основной |
| Статус | draft |
Актор
| Роль | Permissions / scope | Tenant | Record-level |
|---|
| Агент (создание, подтверждение) | crm.bookings.manage | бронь TenantId = DeveloperCompanyId (ADR-0052) | актор — активный участник процесса (обычно Executor, Deal.ExecutorAgentUserId = actor) либо руководитель/админ компании-владельца (AgencyCompanyId) |
| Инвестор (создание с витрины / подтверждение) | crm.bookings.manage (инвесторская сторона) | null (инвестор без tenant) | участник сделки (PrimaryInvestorUserId = actor; допускается InvestorSecondary); вход с витрины — UC-BKG-001 |
| Менеджер застройщика (договор, платная программа) | crm.bookings.decide | TenantId = DeveloperCompanyId | участник-застройщик сделки (SellManager) |
| Менеджер ПИН | platform-scope (обход record-level) | кросс-tenant | не применяется |
Предусловие
- Существует сделка
DealEntity в статусе Application или Booking (INV-BKG-2), доступная актору.
Сделка в Draft (агент+инвестор без застройщика, до подтверждения намерения и одобренной фиксации) бронировать
нельзя — в ней ещё нет участника-застройщика (SellManager); сначала гейт Draft → Application
(UC-CRM-002), затем бронь.
- У застройщика объекта есть хотя бы одна живая программа бронирования (
BookingProgramEntity, INV-BPR-5).
- На объекте недвижимости нет активного процесса брони (
Reserved/AwaitingPayment/Booked), на сделке —
тоже (INV-BKG-001).
- Объект недвижимости и цена уже snapshot-нуты на сделке при её создании; синхронный вызов
chessboard не требуется.
Триггер
| Поле | Значение |
|---|
| Источник | UI-действие (карточка сделки /crm/deals/[id]) → вызов API |
| Триггерящий элемент | create-booking (Agent) → далее confirm-booking (Agent, бесплатная программа) |
Основной Поток
Happy path — бесплатная программа: Резерв (create-booking) → Бронь (confirm-booking). Платная программа,
отмена и истечение — в альтернативах A1–A3.
- Вход. Агент выбирает программу и вызывает
POST /api/agent/crm/deals/{dealId}/bookings
{ProgramId, RequisiteSetId?}. Целевой tenant брони = deal.TenantId; агентский scope — record-level.
- Проверки и инварианты. Сделка доступна актору и
Status ∈ {Application, Booking} (INV-BKG-2);
программа живая своего deal.TenantId; INV-BKG-001 — нет активной брони по PropertyObjectId и DealId
(иначе CRM_BOOKING_PROPERTY_OBJECT_BUSY / CRM_BOOKING_DEAL_ACTIVE_EXISTS).
- Snapshot условий.
ProgramTerms = BookingProgramTerms из программы (иммутабельный VO:
Name, IsPaid, PriceAmount?, PriceCurrencyId, ReserveDurationMinutes (default 60),
BookingDurationHours, RefundPolicy) — правка программы позже не влияет на бронь. RequisiteSetId (если
передан) сохраняется на брони как ссылка на шаблон реквизитов из profile — сами реквизиты/PII в CRM
не копируются; на бесплатной программе поле опционально и на переходы не влияет.
- Создание агрегата. Бронь:
Status = Reserved, InitiatedBy = Agent,
ReserveExpiresAt = now + ReserveDurationMinutes (1 час), PaymentStatus = IsPaid ? Pending : NotRequired.
Тем же изменением сделка переводится Application → Booking (денормализация StepGroup/StepId).
- Что меняется. Создаётся бронь; сделка обновляется (
BookingId, Status, StepId, StepGroup);
пишется таймлайн. Всё — в одном согласованном изменении вместе с записью в outbox; гонка ловится
уникальными индексами активной брони ux_bookings_active_property_object/ux_bookings_active_deal.
- Публикация.
BookingCreatedEvent → pin.crm.booking-created.v1 (chessboard объект недвижимости →
«Резерв» + таймер до ReserveExpiresAt; messenger карточка брони в чат сделки); DealStageChangedEvent
→ pin.crm.deal-stage-changed.v1 только при Application → Booking. Результат — Status = Reserved,
HTTP 201.
- Вход. До истечения резерва агент вызывает
POST /api/agent/crm/bookings/{bookingId}/confirm (тело
пустое); record-level по сделке.
- Guard INV-BKG-3.
ProgramTerms.IsPaid == false (платная — отказ CRM_BOOKING_PAYMENT_REQUIRED),
Status == Reserved, now <= ReserveExpiresAt. Переход Reserved → Booked: BookedAt = now,
BookingExpiresAt = BookedAt + BookingDurationHours.
- Что меняется и согласованность. Бронь обновляется в одном согласованном изменении вместе с outbox.
Гонка «worker истекает / агент подтверждает» разрешается оптимистичной блокировкой: проигравший ретраится
и видит
ReserveExpired → CRM_BOOKING_RESERVE_EXPIRED.
- Публикация.
BookingConfirmedEvent → pin.crm.booking-confirmed.v1 (chessboard объект недвижимости
→ «Бронь» + таймер до BookingExpiresAt; catalog витрина; notifications участникам). Результат —
Status = Booked, HTTP 200.
Диаграмма
Альтернативные Потоки
A1. Платная программа (Резерв → Ожидание оплаты → UC-BKG-004)
ProgramTerms.IsPaid = true: confirm-booking недоступен (CRM_BOOKING_PAYMENT_REQUIRED).
- Менеджер застройщика загружает договор — upload-booking-contract.md:
создаётся
DealDocumentEntity типа BookingContract (Status = Uploaded), бронь Reserved → AwaitingPayment,
PaymentStatus = Pending → DocumentStatusChangedEvent (pin.crm.document-status-changed.v1, notifications
инвестору «скачайте/подпишите/оплатите»). Статус объекта недвижимости остаётся «Резерв».
- Оплата (чек) → подтверждение менеджером (
ConfirmPayment, INV-BKG-4) → ConfirmBooking → Booked описаны в
UC-BKG-004; там же — BookingPaidEvent и переход к событию booking-confirmed.
Возвращается к фазе «Бронь» основного потока (шаг 10) после подтверждения оплаты.
A2. Отмена брони до истечения (cancel-booking)
- Клиент передумал / меняется объект недвижимости. Актор агентской стороны вызывает
cancel-booking.md с
CancelReason: отмена, переход из
Reserved/AwaitingPayment/Booked → Cancelled (INV-BKG-8).
BookingCancelledEvent → pin.crm.booking-cancelled.v1 (chessboard объект недвижимости → «Свободен»;
notifications). Сделка не переводится — остаётся Booking; возврат в Application/новая бронь —
решение менеджера.
Завершается терминально (Cancelled); освобождаются ux_bookings_active_property_object/ux_bookings_active_deal.
A3. Истечение таймера (worker, UC-BKG-003)
- Агент/менеджер не подтвердил вовремя. Фоновый polling-worker
booking-expiration (INV-BKG-5):
Reserved/AwaitingPayment при now > ReserveExpiresAt → ReserveExpired; Booked при
now > BookingExpiresAt → BookingExpired.
BookingExpiredEvent → pin.crm.booking-expired.v1 (chessboard объект недвижимости → «Свободен»;
messenger карточка «время истекло, подтвердите/продлите»). Детали — UC-BKG-003.
Постусловие
- Создан
BookingEntity (Status = Reserved) с иммутабельным snapshot ProgramTerms и ReserveExpiresAt.
- Сделка переведена
Application → Booking (при создании из Application); проставлен Deal.BookingId.
- Для бесплатной программы после confirm —
Status = Booked, BookedAt/BookingExpiresAt заполнены.
- Опубликованы
booking-created (всегда), deal-stage-changed (при смене статуса), booking-confirmed
(после подтверждения); статус объекта недвижимости в chessboard синхронизирован консюмером.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|
VALIDATION_FAILED | 400 | Пустой/невалидный DealId/ProgramId/BookingId | Показать ошибки формы |
CRM_DEAL_NOT_FOUND / CRM_DEAL_FORBIDDEN | 404 / 403 | Сделка не найдена / чужого агентства | Скрыть / нет доступа |
CRM_DEAL_INVALID_STATE | 409 | deal.Status ∉ {Application, Booking} (INV-BKG-2; в т.ч. Draft — сначала гейт Draft → Application) | Бронь недоступна в статусе сделки |
CRM_BOOKING_PROGRAM_NOT_FOUND | 404 | Программа архивирована / чужой tenant | Обновить список программ |
CRM_BOOKING_PROPERTY_OBJECT_BUSY | 409 | На объекте недвижимости активный процесс брони (INV-BKG-001) | Перечитать состояние объекта недвижимости |
CRM_BOOKING_DEAL_ACTIVE_EXISTS | 409 | У сделки уже есть активная бронь (INV-BKG-001) | Перечитать состояние сделки |
CRM_BOOKING_PAYMENT_REQUIRED | 409 | confirm-booking по платной программе (INV-BKG-3) | Дождаться оплаты (A1 → UC-BKG-004) |
CRM_BOOKING_RESERVE_EXPIRED | 409 | now > ReserveExpiresAt (в т.ч. гонка с worker) | Оформить бронь заново |
CRM_BOOKING_INVALID_STATE | 409 | Status ∉ {Reserved} при confirm (уже Booked/иной) | Перечитать статус брони |
CRM_BOOKING_ALREADY_TERMINAL | 409 | Действие над терминальной бронью (INV-BKG-8) | Бронь завершена |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|
BookingCreated | [*] → Reserved (create-booking) | внутренний диспетчер → запись таймлайна сделки |
DealStageChanged | Application → Booking | внутренний диспетчер → таймлайн + сборка DealStageChangedEvent |
BookingConfirmed | Reserved → Booked (confirm-booking) | внутренний диспетчер → таймлайн сделки |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|
BookingCreatedEvent | pin.crm.booking-created.v1 | [*] → Reserved (всегда) | chessboard (объект недвижимости → «Резерв» + таймер), messenger |
DealStageChangedEvent | pin.crm.deal-stage-changed.v1 | Только при Application → Booking | messenger, notifications |
BookingConfirmedEvent | pin.crm.booking-confirmed.v1 | → Booked (бесплатная — confirm; платная — UC-BKG-004) | chessboard (объект недвижимости → «Бронь» + таймер), catalog, notifications |
DocumentStatusChangedEvent | pin.crm.document-status-changed.v1 | A1: договор брони загружен (→ Uploaded) | notifications |
BookingCancelledEvent | pin.crm.booking-cancelled.v1 | A2: → Cancelled | chessboard (объект недвижимости → «Свободен»), notifications |
BookingExpiredEvent | pin.crm.booking-expired.v1 | A3: → ReserveExpired / BookingExpired (worker) | chessboard, messenger, notifications |
Контракты/payload — booking-created.md, booking-confirmed.md,
booking-cancelled.md, booking-expired.md,
document-status-changed.md; публикация — через outbox.
Acceptance Criteria
| # | Критерий | Проверка |
|---|
| AC1 | create-booking снимает snapshot ProgramTerms и создаёт бронь Reserved с ReserveExpiresAt = now + ReserveDurationMinutes (default 60) | Integration-тест: assert полей брони + booking-created в outbox |
| AC2 | При создании из Application сделка переходит в Booking, шлётся deal-stage-changed; из Booking — статус не меняется, событие не шлётся | Тест по двум исходным статусам сделки |
| AC3 | INV-BKG-001: вторая бронь на тот же PropertyObjectId/DealId → CRM_BOOKING_PROPERTY_OBJECT_BUSY / CRM_BOOKING_DEAL_ACTIVE_EXISTS | Тест гонки создания (ux_bookings_active_*) |
| AC4 | Бесплатная программа: confirm до ReserveExpiresAt → Booked, BookingExpiresAt заполнен, booking-confirmed опубликован | Тест confirm-booking |
| AC5 | Платная программа: confirm-booking → CRM_BOOKING_PAYMENT_REQUIRED; загрузка договора → AwaitingPayment (A1) | Тест ветки платной программы |
| AC6 | confirm после now > ReserveExpiresAt (worker истёк резерв) → CRM_BOOKING_RESERVE_EXPIRED, объект недвижимости «Свободен» | Тест гонки с booking-expiration |
| AC7 | cancel-booking из активного статуса → Cancelled, booking-cancelled, объект недвижимости освобождён; сделка остаётся Booking | Тест A2 |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные Документы
- API методы: create-booking.md, confirm-booking.md,
upload-booking-contract.md, cancel-booking.md;
точка входа с витрины — create-booking-request.md.
- Domain entities: booking.md (INV-BKG-001/2/3/8, статусы,
BookingProgramTerms),
booking-program.md, deal.md (переход при бронировании),
deal-timeline-event.md.
- Kafka events: booking-created.md, booking-confirmed.md,
deal-stage-changed.md, booking-cancelled.md,
booking-expired.md, document-status-changed.md.
- Соседние use cases: UC-BKG-001 (вход инвестора с витрины),
UC-BKG-003 (истечение), UC-BKG-004 (оплата платной брони).
- US-реестр / реестр UC: user-stories-deal.md; README контекста.
- ADR: 0052 tenancy,
0056 simplicity-first.