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

UC-BKG-001. Инвестор инициирует бронирование с витрины

Назначение

Гость/инвестор жмёт «Забронировать» на карточке объекта недвижимости и через форму ФИО+телефон заводит воронку агента (клиент → лид → событие ленты BookingRequested) и намерение бронирования (LeadBookingIntentEntity, Source = InvestorAdded, ConfirmedByInvestor = true) на пару (LeadId, PropertyObjectId). Ключевое: заявка с витрины не создаёт ни DealEntity, ни BookingEntity и не ставит объект недвижимости в «Резерв» — намерение лишь фиксирует, что инвестор хочет этот объект. По INV-BKG-2 бронь требует сделку в статусе Application, а сделку создаёт агент/Менеджер ПИН позже: подтверждённое намерение инвестора + одобренная фиксация на пару (инвестор × застройщик) открывают создание сделки сразу в Application, бронь — уже из неё (UC-CRM-002UC-BKG-002). Это первая точка входа единого flow брони. Дом полей — lead.md, client.md, booking.md.

Metadata

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

Актор

РольPermissions / scopeTenantRecord-level
Гость (витрина)нет (публичный гейт Public; валидная реф-cookie referralUrl)null (ADR-0052)не применяется
Инвестор (витрина)авторизован; своя активностьnull (инвестор без tenant)своя заявка
System (привязка агента / lazy-reg)system-scopescope записи = AgencyCompanyId / компания ПИНне применяется

Предусловие

  • Карточка объекта недвижимости открыта на витрине (catalog, /lots/[id]); кнопка «Забронировать» доступна для объекта недвижимости в статусе «Свободен».
  • Форма заполнена: FirstName + LastName + PhoneNumber (обязательны), MiddleName опционален; PropertyObjectId известен из карточки и обязателен для бронирования.
  • chessboard доступен (валидность PropertyObjectId и статус «Свободен»); identity доступен (lazy-регистрация инвестора по телефону, RegistrationSource = LazyRegistration, identity/domain/user.md).

Триггер

ПолеЗначение
ИсточникUI-действие (карточка объекта недвижимости витрины) → вызов API
Триггерящий элементPOST /api/public/crm/booking-requestscreate-booking-request.md

Основной Поток

Основная нить — публичная заявка с витрины; общий хвост get-or-create клиента/лида совпадает с UC-CRM-001 (booking-ветка).

  1. Контекст и вход. На карточке объекта недвижимости кнопка «Забронировать» (активна при статусе «Свободен») открывает форму ФИО+телефон. Гейт публичный (аноним с реф-cookie либо инвестор). Модель запроса содержит ФИО, PhoneNumber, PropertyObjectId.
  2. Проверка входа. ФИО 1..128 не пустое, PhoneNumber — нормализация и формат E.164, PropertyObjectId задан → иначе VALIDATION_ERROR.
  3. Дедуп 24ч. По ленте лидов: существует ли событие BookingRequested с Payload.PropertyObjectId = PropertyObjectId у живого клиента с этим PhoneNumber за now − 24h при лиде Status = Active. Найдено → A1 (идемпотентный 200, IsDuplicate = true).
  4. Проверка объекта недвижимости в chessboard. Синхронный lookup по PropertyObjectId: не найден → CRM_PROPERTY_OBJECT_NOT_FOUND (404); статус ≠ «Свободен» (Резерв/Бронь/Продан) → CRM_PROPERTY_OBJECT_NOT_AVAILABLE (409, A3). Из ответа берём DeveloperCompanyId, ProjectId.
  5. Lazy-регистрация инвестора. get-or-create в identity по нормализованному телефону (RegistrationSource = LazyRegistration) → InvestorUserId; авторизованный инвестор с совпадающим телефоном — reuse. Недоступность → IDENTITY_UNAVAILABLE (503, ретрай).
  6. Привязка агента-владельца. Валидная реф-ссылка с владельцем-агентом → агент-владелец назначается исполнителем лида (участник Executor, проекция ExecutorAgentUserId) и его исторической атрибуцией (OriginAgentUserId, иммутабельно), TenantId = AgencyCompanyId; иначе (ссылки нет / ссылка инвестора) → TenantId = компания ПИН, исполнителем и атрибуцией становится случайный активный сотрудник ПИН (UC-2, A4).
  7. get-or-create клиента. Ищем живую карточку ClientEntity того же tenant по телефону (unique ux_clients_tenant_phone, INV-CLI-1); нет → создаётся новая (Source = ClientSource.BookingRequest, Status = Active, событие ClientCreated); есть с InvestorUserId = null → проставить (INV-CLI-3).
  8. get-or-create лида (INV-CRM-001). Ищем активный лид на пару (ExecutorAgentUserId, PrimaryInvestorUserId); найден → A2 (событие в существующий лид, lead-created не публикуется); нет → создаётся лид (Source = LeadSource.BookingRequest, ExpiresAt = now + 1 месяц, Wishes = LeadWishes.Empty, ReferralLinkId?, событие LeadCreated), клиенту проставляется ActiveLeadId (INV-CLI-2). Wishes пустой — публичная заявка; форму квалификации агента заполняют позже (INV-LEAD-5).
  9. Намерение бронирования. На пару (LeadId, PropertyObjectId) заводится (get-or-create по инварианту «одно активное намерение на пару») LeadBookingIntentEntity: Source = InvestorAdded, ConfirmedByInvestor = true, ConfirmedAt = now, Status = Active, ProjectId/DeveloperCompanyId из объекта, AddedByUserId = InvestorUserId, DealId = null (сделки ещё нет). Уже существует активное намерение на пару → идемпотентно переиспользуется. События лида BookingIntentAdded + BookingIntentConfirmed (Flow A: инвестор добавил — подтверждено сразу).
  10. Событие ленты. В ленту лида добавляется BookingRequested (LeadBookingRequestedPayload { PropertyObjectId, SelectionId?, DeveloperCompanyId }, INV-LEAD-2); Lead.LastEventAt / Client.LastActivityAt = now; событие LeadEventAdded.
  11. Ни сделка, ни бронь НЕ создаются. DealEntity/BookingEntity не создаются и booking-created.v1 не публикуется: по INV-BKG-2 (booking.md) бронь заводится только у сделки Status ∈ {Application, Booking}, а сделку агент создаёт на основании подтверждённого намерения и одобренной фиксации (UC-CRM-002). Объект недвижимости в chessboard остаётся «Свободен» — переход в «Резерв» произойдёт только при создании реальной брони из сделки в Application (UC-BKG-002, таймер ReserveDurationMinutes default 60).
  12. Согласованность и публикация. Клиент + лид + намерение + событие ленты фиксируются атомарно в одном согласованном изменении; при новом лиде — LeadCreatedEvent (топик pin.crm.lead-created.v1, AggregateId = lead.Id) через outbox.
  13. Результат. Возвращаются { RequestId = leadEvent.Id, ReceivedAt, IsDuplicate = false }. Идентификаторы лида/клиента наружу не возвращаются (внутренние данные агентства). Фронт: «Заявка принята, агент свяжется»; notifications по lead-created уведомляет агента-владельца.
примечание
Согласованность с identity и chessboard

Проверка объекта (шаг 4) и lazy-регистрация (шаг 5) — синхронные вызовы соседних сервисов до атомарного коммита CRM. Lazy-регистрация идемпотентна (get-or-create по нормализованному телефону): если коммит CRM откатится, созданный/переиспользованный профиль identity остаётся и переиспользуется при ретрае — «сирот»-пользователей не образуется, повторная заявка не плодит записи. Статус объекта из chessboard (шаг 4) — снимок на момент чтения; реальную бронь и её гонку с изменением статуса объекта отрабатывает уже UC-BKG-002 (INV-BKG-001), эта заявка объект не блокирует.

Диаграмма

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

A1. Дедуп 24ч (повторная заявка)

  1. Шаг 3 нашёл BookingRequested тем же телефоном на тот же PropertyObjectId за now − 24h.
  2. Новых записей нет; возвращается существующий RequestId, IsDuplicate = true, HTTP 200.
  3. lead-created не публикуется. Защищает от двойного submit/спама до lazy-регистрации.

Завершается идемпотентно.

A2. У пары уже есть активный лид (INV-CRM-001)

  1. Шаг 8 нашёл активный лид (Status = Active) на пару (ExecutorAgentUserId, PrimaryInvestorUserId).
  2. Новый LeadEntity не создаётся; событие BookingRequested пишется в существующую ленту, обновляется LastEventAt.
  3. LeadCreatedEvent не публикуется. Гонка ux_leads_active_pair → перечитать лид и завершиться идемпотентно (как UC-CRM-001 A1).

A3. Объект недвижимости не «Свободен»

  1. Шаг 4: статус объекта недвижимости — Резерв/Бронь/Продан.
  2. CRM_PROPERTY_OBJECT_NOT_AVAILABLE (409); клиент/лид/событие не создаются, воронка не заводится.
  3. Фронт показывает «Объект недоступен для бронирования».

A4. Реферальной привязки нет (UC-2)

  1. Шаг 6 не нашёл реф-ссылку/агента-владельца.
  2. TenantId = компания ПИН; исполнителем и атрибуцией лида (ExecutorAgentUserId/OriginAgentUserId) назначается случайный сотрудник ПИН.
  3. Далее — общий хвост; лид «живёт» в компании ПИН, уведомление уходит выбранному сотруднику.

Постусловие

  • Заведена воронка агента: ClientEntity (Source = BookingRequest, если не было) со Status = Active; LeadEntity (Status = Active, ExpiresAt = CreatedAt + 1 месяц) — кроме A2, где лид переиспользуется; LeadEventEntity(BookingRequested) в ленте; обновлён Lead.LastEventAt.
  • Заведено активное LeadBookingIntentEntity на пару (LeadId, PropertyObjectId): Source = InvestorAdded, ConfirmedByInvestor = true, Status = Active, DealId = nullкроме A2 и повторов, где активное намерение на пару переиспользуется идемпотентно.
  • Опубликован pin.crm.lead-created.v1только при создании нового лида (не в A1/A2).
  • DealEntity/BookingEntity не созданы, booking-created.v1 не публикуется; объект недвижимости в chessboard остаётся «Свободен» — сделку по подтверждённому намерению создаёт агент (при одобренной фиксации), переход в «Резерв» наступит только при старте реальной брони из сделки в Application (UC-BKG-002).
  • Агент-владелец получает уведомление «клиент запросил бронирование объекта недвижимости» (через notifications).

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
VALIDATION_ERROR400Пустое ФИО / неверный телефон / PropertyObjectId пустПоказать ошибки формы
CRM_PROPERTY_OBJECT_NOT_FOUND404PropertyObjectId не найден в chessboard«Объект не найден»
CRM_PROPERTY_OBJECT_NOT_AVAILABLE409Статус объекта недвижимости ≠ «Свободен» (A3)«Объект недоступен для бронирования»
CRM_CLIENT_PHONE_DUPLICATE409Телефон занят другой живой карточкой агентства (INV-CLI-1, гонка)Ретрай (перечитка карточки)
CRM_LEAD_ACTIVE_PAIR_EXISTS— (internal)Гонка ux_leads_active_pair при создании лидаПеречитать и завершиться идемпотентно (A2)
IDENTITY_UNAVAILABLE503identity недоступен при lazy-регистрацииРетрай позже
CHESSBOARD_UNAVAILABLE503chessboard недоступен при проверке объекта недвижимостиРетрай позже
Дедуп — не ошибка

Дедуп-повтор (A1) — не ошибка: HTTP 200, IsDuplicate = true (идемпотентный успех).

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

СобытиеКогда поднимаетсяSubscribers
ClientCreatedСоздание новой карточки клиента (Source = BookingRequest)внутренний диспетчер
LeadCreatedСоздание нового лида ([*] → Active)внутренний диспетчер → сборка LeadCreatedEvent
LeadEventAddedДобавление LeadEventEntity(BookingRequested) в лентувнутренний диспетчер
BookingIntentAddedСоздание LeadBookingIntentEntity (Source = InvestorAdded)внутренний диспетчер
BookingIntentConfirmedНамерение инвестора подтверждено сразу (ConfirmedByInvestor = true)внутренний диспетчер

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

СобытиеTopicКогда публикуетсяConsumers
LeadCreatedEventpin.crm.lead-created.v1Только при создании нового лида ([*] → Active); A1/A2 — no-opnotifications (уведомление агенту), referral (подтверждение привязки при Source = Referral)

Контракт/payload — lead-created.md (PII осознанно исключён, Source = BookingRequest). Не публикуются здесь: booking-created.v1 и прочие booking-события (booking-created.md) — их поднимает создание BookingEntity в UC-BKG-002. Публикация — через outbox, атомарно с созданием агрегата.

Acceptance Criteria

#КритерийПроверка
AC1Заявка «Забронировать» создаёт Client(Source=BookingRequest) + Lead(Source=BookingRequest) + событие BookingRequested c payload {PropertyObjectId, DeveloperCompanyId}Integration-тест create-booking-request; проверить 3 записи + payload
AC2DealEntity/BookingEntity не создаются, booking-created.v1 не публикуется, объект недвижимости в chessboard остаётся «Свободен»Тест: deals/bookings пусты, нет outbox booking-события, статус объекта недвижимости не менялся
AC2bЗаявка создаёт активное LeadBookingIntentEntity (Source = InvestorAdded, ConfirmedByInvestor = true, DealId = null); повтор на пару (LeadId, PropertyObjectId) не плодит второе намерениеТест: одна строка lead_booking_intents, инвариант активности пары
AC3Дедуп 24ч: повтор тем же телефоном+объектом недвижимости не создаёт второе событие, 200 IsDuplicate=trueТест A1: два вызова подряд, одна строка lead_events(BookingRequested)
AC4Объект недвижимости не «Свободен» → 409 CRM_PROPERTY_OBJECT_NOT_AVAILABLE, воронка не заводитсяТест A3: объект недвижимости в «Резерв» → нет клиента/лида
AC5У пары есть Active-лид → событие в существующий, lead-created не публикуетсяТест A2: одна строка leads, две — lead_events
AC6Нет реф-привязки → TenantId = ПИН, исполнитель/атрибуция (ExecutorAgentUserId/OriginAgentUserId) = сотрудник ПИНТест A4: проверить tenant/agent лида
AC7Клиент + лид + событие + outbox атомарны; сбой транзакции откатывает всёТест с инъекцией сбоя транзакции
AC8pin.crm.lead-created.v1 несёт Source = BookingRequestТест payload события в outbox

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

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

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