Перейти к основному содержимому

UC-BKG-002. Бронирование Резерв → Бронь

Назначение

Закрепление объекта за сделкой: агент из карточки сделки создаёт бронь BookingEntity в фазе Резерв (короткая блокировка с таймером) и для бесплатной программы переводит её в Бронь до истечения резерва. Дом полей — booking.md, условия — booking-program.md, родитель — deal.md.

Metadata

ПолеЗначение
IDUC-BKG-002
Контекстcrm (см. docs/06-services/crm/)
Типосновной
Статусdraft

Актор

РольPermissions / scopeTenantRecord-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.decideTenantId = 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.

Фаза 1. Создание брони (Резерв) — create-booking.md

  1. Вход. Агент выбирает программу и вызывает POST /api/agent/crm/deals/{dealId}/bookings {ProgramId, RequisiteSetId?}. Целевой tenant брони = deal.TenantId; агентский scope — record-level.
  2. Проверки и инварианты. Сделка доступна актору и Status ∈ {Application, Booking} (INV-BKG-2); программа живая своего deal.TenantId; INV-BKG-001 — нет активной брони по PropertyObjectId и DealId (иначе CRM_BOOKING_PROPERTY_OBJECT_BUSY / CRM_BOOKING_DEAL_ACTIVE_EXISTS).
  3. Snapshot условий. ProgramTerms = BookingProgramTerms из программы (иммутабельный VO: Name, IsPaid, PriceAmount?, PriceCurrencyId, ReserveDurationMinutes (default 60), BookingDurationHours, RefundPolicy) — правка программы позже не влияет на бронь. RequisiteSetId (если передан) сохраняется на брони как ссылка на шаблон реквизитов из profile — сами реквизиты/PII в CRM не копируются; на бесплатной программе поле опционально и на переходы не влияет.
  4. Создание агрегата. Бронь: Status = Reserved, InitiatedBy = Agent, ReserveExpiresAt = now + ReserveDurationMinutes (1 час), PaymentStatus = IsPaid ? Pending : NotRequired. Тем же изменением сделка переводится Application → Booking (денормализация StepGroup/StepId).
  5. Что меняется. Создаётся бронь; сделка обновляется (BookingId, Status, StepId, StepGroup); пишется таймлайн. Всё — в одном согласованном изменении вместе с записью в outbox; гонка ловится уникальными индексами активной брони ux_bookings_active_property_object/ux_bookings_active_deal.
  6. Публикация. BookingCreatedEventpin.crm.booking-created.v1 (chessboard объект недвижимости → «Резерв» + таймер до ReserveExpiresAt; messenger карточка брони в чат сделки); DealStageChangedEventpin.crm.deal-stage-changed.v1 только при Application → Booking. Результат — Status = Reserved, HTTP 201.

Фаза 2. Подтверждение брони (Бронь) — confirm-booking.md

  1. Вход. До истечения резерва агент вызывает POST /api/agent/crm/bookings/{bookingId}/confirm (тело пустое); record-level по сделке.
  2. Guard INV-BKG-3. ProgramTerms.IsPaid == false (платная — отказ CRM_BOOKING_PAYMENT_REQUIRED), Status == Reserved, now <= ReserveExpiresAt. Переход Reserved → Booked: BookedAt = now, BookingExpiresAt = BookedAt + BookingDurationHours.
  3. Что меняется и согласованность. Бронь обновляется в одном согласованном изменении вместе с outbox. Гонка «worker истекает / агент подтверждает» разрешается оптимистичной блокировкой: проигравший ретраится и видит ReserveExpiredCRM_BOOKING_RESERVE_EXPIRED.
  4. Публикация. BookingConfirmedEventpin.crm.booking-confirmed.v1 (chessboard объект недвижимости → «Бронь» + таймер до BookingExpiresAt; catalog витрина; notifications участникам). Результат — Status = Booked, HTTP 200.

Диаграмма

Альтернативные Потоки

A1. Платная программа (Резерв → Ожидание оплаты → UC-BKG-004)

  1. ProgramTerms.IsPaid = true: confirm-booking недоступен (CRM_BOOKING_PAYMENT_REQUIRED).
  2. Менеджер застройщика загружает договор — upload-booking-contract.md: создаётся DealDocumentEntity типа BookingContract (Status = Uploaded), бронь Reserved → AwaitingPayment, PaymentStatus = PendingDocumentStatusChangedEvent (pin.crm.document-status-changed.v1, notifications инвестору «скачайте/подпишите/оплатите»). Статус объекта недвижимости остаётся «Резерв».
  3. Оплата (чек) → подтверждение менеджером (ConfirmPayment, INV-BKG-4) → ConfirmBooking → Booked описаны в UC-BKG-004; там же — BookingPaidEvent и переход к событию booking-confirmed.

Возвращается к фазе «Бронь» основного потока (шаг 10) после подтверждения оплаты.

A2. Отмена брони до истечения (cancel-booking)

  1. Клиент передумал / меняется объект недвижимости. Актор агентской стороны вызывает cancel-booking.md с CancelReason: отмена, переход из Reserved/AwaitingPayment/BookedCancelled (INV-BKG-8).
  2. BookingCancelledEventpin.crm.booking-cancelled.v1 (chessboard объект недвижимости → «Свободен»; notifications). Сделка не переводится — остаётся Booking; возврат в Application/новая бронь — решение менеджера.

Завершается терминально (Cancelled); освобождаются ux_bookings_active_property_object/ux_bookings_active_deal.

A3. Истечение таймера (worker, UC-BKG-003)

  1. Агент/менеджер не подтвердил вовремя. Фоновый polling-worker booking-expiration (INV-BKG-5): Reserved/AwaitingPayment при now > ReserveExpiresAtReserveExpired; Booked при now > BookingExpiresAtBookingExpired.
  2. BookingExpiredEventpin.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_FAILED400Пустой/невалидный DealId/ProgramId/BookingIdПоказать ошибки формы
CRM_DEAL_NOT_FOUND / CRM_DEAL_FORBIDDEN404 / 403Сделка не найдена / чужого агентстваСкрыть / нет доступа
CRM_DEAL_INVALID_STATE409deal.Status ∉ {Application, Booking} (INV-BKG-2; в т.ч. Draft — сначала гейт Draft → Application)Бронь недоступна в статусе сделки
CRM_BOOKING_PROGRAM_NOT_FOUND404Программа архивирована / чужой tenantОбновить список программ
CRM_BOOKING_PROPERTY_OBJECT_BUSY409На объекте недвижимости активный процесс брони (INV-BKG-001)Перечитать состояние объекта недвижимости
CRM_BOOKING_DEAL_ACTIVE_EXISTS409У сделки уже есть активная бронь (INV-BKG-001)Перечитать состояние сделки
CRM_BOOKING_PAYMENT_REQUIRED409confirm-booking по платной программе (INV-BKG-3)Дождаться оплаты (A1 → UC-BKG-004)
CRM_BOOKING_RESERVE_EXPIRED409now > ReserveExpiresAt (в т.ч. гонка с worker)Оформить бронь заново
CRM_BOOKING_INVALID_STATE409Status ∉ {Reserved} при confirm (уже Booked/иной)Перечитать статус брони
CRM_BOOKING_ALREADY_TERMINAL409Действие над терминальной бронью (INV-BKG-8)Бронь завершена

Доменные События

СобытиеКогда поднимаетсяSubscribers
BookingCreated[*] → Reserved (create-booking)внутренний диспетчер → запись таймлайна сделки
DealStageChangedApplication → Bookingвнутренний диспетчер → таймлайн + сборка DealStageChangedEvent
BookingConfirmedReserved → Booked (confirm-booking)внутренний диспетчер → таймлайн сделки

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
BookingCreatedEventpin.crm.booking-created.v1[*] → Reserved (всегда)chessboard (объект недвижимости → «Резерв» + таймер), messenger
DealStageChangedEventpin.crm.deal-stage-changed.v1Только при Application → Bookingmessenger, notifications
BookingConfirmedEventpin.crm.booking-confirmed.v1→ Booked (бесплатная — confirm; платная — UC-BKG-004)chessboard (объект недвижимости → «Бронь» + таймер), catalog, notifications
DocumentStatusChangedEventpin.crm.document-status-changed.v1A1: договор брони загружен (→ Uploaded)notifications
BookingCancelledEventpin.crm.booking-cancelled.v1A2: → Cancelledchessboard (объект недвижимости → «Свободен»), notifications
BookingExpiredEventpin.crm.booking-expired.v1A3: → 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

#КритерийПроверка
AC1create-booking снимает snapshot ProgramTerms и создаёт бронь Reserved с ReserveExpiresAt = now + ReserveDurationMinutes (default 60)Integration-тест: assert полей брони + booking-created в outbox
AC2При создании из Application сделка переходит в Booking, шлётся deal-stage-changed; из Booking — статус не меняется, событие не шлётсяТест по двум исходным статусам сделки
AC3INV-BKG-001: вторая бронь на тот же PropertyObjectId/DealIdCRM_BOOKING_PROPERTY_OBJECT_BUSY / CRM_BOOKING_DEAL_ACTIVE_EXISTSТест гонки создания (ux_bookings_active_*)
AC4Бесплатная программа: confirm до ReserveExpiresAtBooked, BookingExpiresAt заполнен, booking-confirmed опубликованТест confirm-booking
AC5Платная программа: confirm-bookingCRM_BOOKING_PAYMENT_REQUIRED; загрузка договора → AwaitingPayment (A1)Тест ветки платной программы
AC6confirm после now > ReserveExpiresAt (worker истёк резерв) → CRM_BOOKING_RESERVE_EXPIRED, объект недвижимости «Свободен»Тест гонки с booking-expiration
AC7cancel-booking из активного статуса → Cancelled, booking-cancelled, объект недвижимости освобождён; сделка остаётся BookingТест A2

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные Документы