UC-BKG-001. Инвестор инициирует бронирование с витрины
Назначение
Гость/инвестор жмёт «Забронировать» на карточке объекта недвижимости и через форму ФИО+телефон заводит
воронку агента (клиент → лид → событие ленты BookingRequested) и намерение бронирования
(LeadBookingIntentEntity, Source = InvestorAdded, ConfirmedByInvestor = true) на пару
(LeadId, PropertyObjectId). Ключевое: заявка с витрины не создаёт ни DealEntity, ни BookingEntity
и не ставит объект недвижимости в «Резерв» — намерение лишь фиксирует, что инвестор хочет этот объект. По
INV-BKG-2 бронь требует сделку в статусе Application, а сделку создаёт агент/Менеджер ПИН позже: подтверждённое
намерение инвестора + одобренная фиксация на пару (инвестор × застройщик) открывают создание сделки сразу в
Application, бронь — уже из неё (UC-CRM-002 →
UC-BKG-002). Это первая точка входа единого flow брони. Дом полей —
lead.md, client.md, booking.md.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-BKG-001 |
| Контекст | crm (см. docs/06-services/crm/) |
| Тип | основной |
| Статус | draft |
Актор
| Роль | Permissions / scope | Tenant | Record-level |
|---|---|---|---|
| Гость (витрина) | нет (публичный гейт Public; валидная реф-cookie referralUrl) | null (ADR-0052) | не применяется |
| Инвестор (витрина) | авторизован; своя активность | null (инвестор без tenant) | своя заявка |
| System (привязка агента / lazy-reg) | system-scope | scope записи = 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-requests — create-booking-request.md |
Основной Поток
Основная нить — публичная заявка с витрины; общий хвост get-or-create клиента/лида совпадает с UC-CRM-001 (booking-ветка).
- Контекст и вход. На карточке объекта недвижимости кнопка «Забронировать» (активна при статусе
«Свободен») открывает форму ФИО+телефон. Гейт публичный (аноним с реф-cookie либо инвестор). Модель
запроса содержит ФИО,
PhoneNumber,PropertyObjectId. - Проверка входа. ФИО
1..128не пустое,PhoneNumber— нормализация и формат E.164,PropertyObjectIdзадан → иначеVALIDATION_ERROR. - Дедуп 24ч. По ленте лидов: существует ли событие
BookingRequestedсPayload.PropertyObjectId = PropertyObjectIdу живого клиента с этимPhoneNumberзаnow − 24hпри лидеStatus = Active. Найдено → A1 (идемпотентный200,IsDuplicate = true). - Проверка объекта недвижимости в
chessboard. Синхронный lookup поPropertyObjectId: не найден →CRM_PROPERTY_OBJECT_NOT_FOUND(404); статус ≠ «Свободен» (Резерв/Бронь/Продан) →CRM_PROPERTY_OBJECT_NOT_AVAILABLE(409, A3). Из ответа берёмDeveloperCompanyId,ProjectId. - Lazy-регистрация инвестора. get-or-create в
identityпо нормализованному телефону (RegistrationSource = LazyRegistration) →InvestorUserId; авторизованный инвестор с совпадающим телефоном — reuse. Недоступность →IDENTITY_UNAVAILABLE(503, ретрай). - Привязка агента-владельца. Валидная реф-ссылка с владельцем-агентом → агент-владелец назначается
исполнителем лида (участник
Executor, проекцияExecutorAgentUserId) и его исторической атрибуцией (OriginAgentUserId, иммутабельно),TenantId = AgencyCompanyId; иначе (ссылки нет / ссылка инвестора) →TenantId= компания ПИН, исполнителем и атрибуцией становится случайный активный сотрудник ПИН (UC-2, A4). - get-or-create клиента. Ищем живую карточку
ClientEntityтого же tenant по телефону (uniqueux_clients_tenant_phone, INV-CLI-1); нет → создаётся новая (Source = ClientSource.BookingRequest,Status = Active, событиеClientCreated); есть сInvestorUserId = null→ проставить (INV-CLI-3). - 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). - Намерение бронирования. На пару (
LeadId,PropertyObjectId) заводится (get-or-create по инварианту «одно активное намерение на пару»)LeadBookingIntentEntity:Source = InvestorAdded,ConfirmedByInvestor = true,ConfirmedAt = now,Status = Active,ProjectId/DeveloperCompanyIdиз объекта,AddedByUserId = InvestorUserId,DealId = null(сделки ещё нет). Уже существует активное намерение на пару → идемпотентно переиспользуется. События лидаBookingIntentAdded+BookingIntentConfirmed(Flow A: инвестор добавил — подтверждено сразу). - Событие ленты. В ленту лида добавляется
BookingRequested(LeadBookingRequestedPayload { PropertyObjectId, SelectionId?, DeveloperCompanyId }, INV-LEAD-2);Lead.LastEventAt/Client.LastActivityAt = now; событиеLeadEventAdded. - Ни сделка, ни бронь НЕ создаются.
DealEntity/BookingEntityне создаются иbooking-created.v1не публикуется: по INV-BKG-2 (booking.md) бронь заводится только у сделкиStatus ∈ {Application, Booking}, а сделку агент создаёт на основании подтверждённого намерения и одобренной фиксации (UC-CRM-002). Объект недвижимости вchessboardостаётся «Свободен» — переход в «Резерв» произойдёт только при создании реальной брони из сделки вApplication(UC-BKG-002, таймерReserveDurationMinutesdefault 60). - Согласованность и публикация. Клиент + лид + намерение + событие ленты фиксируются атомарно в одном
согласованном изменении; при новом лиде —
LeadCreatedEvent(топикpin.crm.lead-created.v1,AggregateId = lead.Id) через outbox. - Результат. Возвращаются
{ 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ч (повторная заявка)
- Шаг 3 нашёл
BookingRequestedтем же телефоном на тот жеPropertyObjectIdзаnow − 24h. - Новых записей нет; возвращается существующий
RequestId,IsDuplicate = true, HTTP200. lead-createdне публикуется. Защищает от двойного submit/спама до lazy-регистрации.
Завершается идемпотентно.
A2. У пары уже есть активный лид (INV-CRM-001)
- Шаг 8 нашёл активный лид (
Status = Active) на пару (ExecutorAgentUserId,PrimaryInvestorUserId). - Новый
LeadEntityне создаётся; событиеBookingRequestedпишется в существующую ленту, обновляетсяLastEventAt. LeadCreatedEventне публикуется. Гонкаux_leads_active_pair→ перечитать лид и завершиться идемпотентно (как UC-CRM-001 A1).
A3. Объект недвижимости не «Свободен»
- Шаг 4: статус объекта недвижимости —
Резерв/Бронь/Продан. CRM_PROPERTY_OBJECT_NOT_AVAILABLE(409); клиент/лид/событие не создаются, воронка не заводится.- Фронт показывает «Объект недоступен для бронирования».
A4. Реферальной привязки нет (UC-2)
- Шаг 6 не нашёл реф-ссылку/агента-владельца.
TenantId= компания ПИН; исполнителем и атрибуцией лида (ExecutorAgentUserId/OriginAgentUserId) назначается случайный сотрудник ПИН.- Далее — общий хвост; лид «живёт» в компании ПИН, уведомление уходит выбранному сотруднику.
Постусловие
- Заведена воронка агента:
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_ERROR | 400 | Пустое ФИО / неверный телефон / PropertyObjectId пуст | Показать ошибки формы |
CRM_PROPERTY_OBJECT_NOT_FOUND | 404 | PropertyObjectId не найден в chessboard | «Объект не найден» |
CRM_PROPERTY_OBJECT_NOT_AVAILABLE | 409 | Статус объекта недвижимости ≠ «Свободен» (A3) | «Объект недоступен для бронирования» |
CRM_CLIENT_PHONE_DUPLICATE | 409 | Телефон занят другой живой карточкой агентства (INV-CLI-1, гонка) | Ретрай (перечитка карточки) |
CRM_LEAD_ACTIVE_PAIR_EXISTS | — (internal) | Гонка ux_leads_active_pair при создании лида | Перечитать и завершиться идемпотентно (A2) |
IDENTITY_UNAVAILABLE | 503 | identity недоступен при lazy-регистрации | Ретрай позже |
CHESSBOARD_UNAVAILABLE | 503 | chessboard недоступен при проверке объекта недвижимости | Ретрай позже |
Дедуп-повтор (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 |
|---|---|---|---|
LeadCreatedEvent | pin.crm.lead-created.v1 | Только при создании нового лида ([*] → Active); A1/A2 — no-op | notifications (уведомление агенту), 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 |
| AC2 | DealEntity/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 атомарны; сбой транзакции откатывает всё | Тест с инъекцией сбоя транзакции |
| AC8 | pin.crm.lead-created.v1 несёт Source = BookingRequest | Тест payload события в outbox |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (4): UC-BKG-002. Бронирование Резерв → Бронь, UC-CRM-001. Создание лида (заявка / реферал / вручную), UC-CRM-003. Движение сделки по канбан-доске, US-реестр блока «Сделка» (US-INV / US-AGT / US-DEV / US-MGR)
- Саги (1): Saga «Booking» (Резерв → Бронь → передача в продажу)
- scenarios (1): UC-OVR-011. Самозапись инвестора: от слота до исхода встречи и лида в воронке
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API: create-booking-request.md; продолжение flow (реальная бронь) — agent/create-booking.md.
- Смежные use-cases: UC-CRM-001 (общий хвост lead/client), UC-CRM-002 (сделка из лида), UC-BKG-002 (Резерв → Бронь).
- Domain: lead.md (
BookingRequested,LeadBookingRequestedPayload, INV-CRM-001), lead.md (LeadBookingIntentEntity,Source, инвариант активного намерения на пару), client.md (ClientSource.BookingRequest, INV-CLI-1/2/3), booking.md (INV-BKG-2 — почему заявка не создаёт бронь); lazy-регистрация — identity/domain/user.md. - Kafka: lead-created.md; booking-события — booking-created.md.
- Статус объекта недвижимости —
docs/06-services/chessboard/(консюмер booking-событий, будущий путь по конвенции). - US-реестр / README: user-stories-deal.md (US-INV-10); README контекста.
- Сквозной UC-3 —
docs/02-architecture/scenarios/(будущий путь по конвенции). - ADR: 0052 tenancy, 0056 simplicity-first.