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

UC-OVR-011. Самозапись инвестора: от слота до исхода встречи и лида в воронке

Назначение

Сквозной сценарий самозаписи: инвестор (или аноним, регистрируемый по телефону на лету) выбирает на витрине свободный слот агента и бронирует показ/консультацию — в calendar возникает запись (appointment), а её жизненный цикл (подтверждение агентом / перенос / отмена / просрочка / исход «состоялась» или «не пришёл») событийно проецируется в crm как лента лида и активный лид в воронке конкретного агента, откуда лид доводится до сделки. Сценарий связывает calendar (владелец слотов, записей, календарных событий и напоминаний), crm (воронка: клиент, лид, лента), identity (lazy-регистрация) и notifications (уведомления сторон). Объединяет сервисные UC самозаписи и жизненного цикла записи (UC-CAL-003, UC-CAL-004, UC-CAL-005, UC-CAL-006) и является слотовым входом воронки из UC-OVR-001.

Metadata

ПолеЗначение
Статусdraft
Owning teamcalendar
Primary capabilityself-booking-to-lead
Risk levelmedium

Scope И Участники

УчастникРоль
Инвестор / ГостьАктор: выбирает и бронирует свободный слот; отменяет/переносит свою запись (в пределах cutoff)
Агент (или консультант ПИН)Владелец слота: подтверждает/отклоняет Pending-запись, отменяет запись, фиксирует исход встречи
calendarВладелец слотов, записей, календарных событий и напоминаний; издатель жизненного цикла записи
identity (internal)Lazy-регистрация анонима по телефону → InvestorUserId
crmConsumer записей: get-or-create клиента/лида пары «агент ↔ инвестор» + append-only лента лида; довод лида до сделки
notificationsУведомления агенту (новая запись) и инвестору (статусы записи)
System (worker calendar)Таймаут решения по Pending-записи (авто-Expired)

Preconditions

  • Агент опубликовал слоты самозаписи (UC-CAL-002); интересующий слот Available, до его начала больше MinLeadTimeMinutes.
  • Витрина слотов доступна анонимно; у анонима есть телефон для lazy-регистрации, авторизованный инвестор действует под своим JWT; identity доступен.
  • Привязка агента задаётся выбранным слотом (AgentUserId/TenantId из записи) — правила реферальной привязки здесь не применяются: агента инвестор выбрал сам.

Основной Сценарий

  1. Выбор и бронирование слота (calendar). Инвестор открывает витрину свободных слотов агента (фильтры: агент, проект, объект, вид встречи Showing/Consultation, диапазон дат) и бронирует слот, указав телефон и ФИО (аноним) и опционально комментарий. Для анонима выполняется lazy-регистрация по телефону в identity. В одном согласованном изменении: слот Available → Booked (условный переход — выигрывает ровно один запрос), создаётся запись (Confirmed при AutoConfirm слота, иначе Pending с дедлайном решения агента PendingExpiresAt) со снапшотом времени/вида, календарное событие агента (Planned) и напоминания; публикуется pin.calendar.appointment-created.v1 (при auto-confirm — также appointment-confirmed) — UC-CAL-003.
  2. Проекция в воронку (crm). По appointment-created crm (consumer записей календаря) делает get-or-create карточки клиента и активного лида пары «агент ↔ инвестор» (ФИО/телефон — из собственной display-реплики identity), пишет в ленту лида факт записи (ShowingAppointmentCreated/ ConsultationAppointmentCreated с комментарием и привязкой к проекту/объекту); при создании нового лида агент, изначально приведший запись, становится и Origin, и Executor, публикуется pin.crm.lead-created.v1. notifications уведомляет агента о новой записи — calendar-appointments.md.
  3. Решение агента по Pending-записи (calendar). Для слота без auto-confirm агент до PendingExpiresAt подтверждает встречу (Pending → Confirmed, слот остаётся Booked) или отклоняет с причиной (Pending → Declined, слот освобождается/истекает, событие Cancelled, напоминания гасятся); непринятое вовремя решение закрывает запись worker-ом (Pending → Expired). По каждому переходу публикуется appointment-confirmed / appointment-cancelledUC-CAL-004. crm дописывает в ленту лида AppointmentConfirmed либо AppointmentCancelled (с причиной DeclinedByAgent/Expired).
  4. Отмена/перенос инвестором (calendar, опционально). До cutoff инвестор отменяет запись (Cancelled(Investor), каскад освобождения слота и события) или переносит её — атомарная пара «отмена текущей + бронирование нового слота» со взаимными ссылками RescheduledFrom/To; агент может отменить запись со своей стороны с причиной. По переходам публикуются appointment-cancelled (+ appointment-created при переносе) — UC-CAL-005. crm отражает отмену/перенос в ленте (перенос помечается как продолжение, новый лид не создаётся).
  5. Исход встречи (calendar). После наступления времени агент фиксирует исход подтверждённой записи — Completed (встреча состоялась) или NoShow (клиент не пришёл); запись терминальна, слот Archived, календарное событие Completed, напоминания погашены; публикуется pin.calendar.appointment-completed.v1 с исходом — UC-CAL-006. crm дописывает в ленту AppointmentCompleted/AppointmentNoShow.
  6. Интерес к объекту → намерение бронирования (crm). Если инвестор на витрине/по ходу показа выражает интерес к конкретному объекту (жмёт «Забронировать» — UC-BKG-001), в лиде заводится LeadBookingIntentEntity на пару (LeadId, PropertyObjectId): Source = InvestorAdded, ConfirmedByInvestor = true (инвестор сам инициировал). Если объект предлагает агент по итогам встречи — Source = AgentProposed, ConfirmedByInvestor = false до подтверждения инвестором. Намерение — не сделка и не бронь: оно лишь фиксирует желаемый объект.
  7. Довод лида до сделки через фиксацию (crm). Активный лид в воронке агента с лентой встреч и намерениями работается дальше. Создание сделки для объекта требует двух условий: подтверждённое инвестором намерение (ConfirmedByInvestor = true) и одобренная фиксация на пару (инвестор × застройщик объекта) — фиксация авторизует агента работать с клиентом по проектам застройщика. Оба выполнены → сделка создаётся сразу в Application (Flow A). Если агент создаёт сделку по объекту, ещё не подтверждённому инвестором, — она заводится в Draft (агент+инвестор, без застройщика, скрыта из канбана застройщика); после подтверждения намерения инвестором и одобренной фиксации срабатывает гейт Draft → Application (Flow B). Состоявшийся показ — основание для квалификации, фиксации и сделки (UC-OVR-005); NoShow — сигнал повторного контакта. Слотовый и no-slot входы сходятся в одном клиенте/лиде (get-or-create по телефону), поэтому повторные касания того же инвестора не плодят вторую воронку.

Sequence

Постусловие

После полного прохода сценария гарантированно:

  • в calendar слот прошёл Available → Booked → Archived (или вернулся в Available/Expired при отмене/отклонении/просрочке), запись достигла терминального статуса (Completed/NoShow/Cancelled/ Declined/Expired) со снапшотом времени/вида, календарное событие агента отражает факт (Completed) или отмену (Cancelled), непосланные напоминания погашены (Skipped);
  • аноним получил учётную запись инвестора (lazy-регистрация идемпотентна по телефону); повторное бронирование тем же телефоном использует существующего пользователя;
  • в crm существует ровно один активный Lead пары «агент ↔ инвестор» с Origin = Executor при создании, Source = Appointment, привязкой к компании выбранного агента; лента лида несёт упорядоченную по времени цепочку фактов записи (создание → подтверждение/перенос/отмена → исход), включая комментарий инвестора и привязку к проекту/объекту; для нового лида опубликовано pin.crm.lead-created.v1 ровно один раз;
  • перенос отражён как продолжение (взаимные ссылки RescheduledFrom/To), второй лид не создан;
  • агент и инвестор уведомлены на ключевых переходах; лид доступен для доведения до сделки (UC-OVR-005).

Промежуточные состояния, наблюдаемые пользователями: (1) запись создана — «ожидает решения агента» (Pending) или «встреча подтверждена» (Confirmed); лид у агента появляется асинхронно (< 1 мин); (2) решение агента — запись подтверждена/отклонена/просрочена; (3) после времени — исход зафиксирован, запись терминальна.

Альтернативные Сценарии И Отказы

СитуацияПоведение
Гонка за один слотУсловный переход Available → Booked выигрывает ровно один запрос; проигравший получает SLOT_ALREADY_BOOKED, вторая запись не создаётся даже частично
Антиспам-лимиты инвестораНе более 1 активной записи на пару «инвестор + агент + вид» и ≤ 3 активных суммарно; превышение → APPOINTMENT_DUPLICATE (дубль-детект окна DUPLICATE_PHONE no-slot-входа здесь не применяется — антиспам живёт в календаре)
Слот стал недоступен между просмотром и бронированиемCAL_SLOT_NOT_AVAILABLE; до начала меньше MinLeadTimeMinutesCAL_SLOT_LEAD_TIME_VIOLATION; витрина обновляется
identity недоступен при lazy-регистрации анонимаIDENTITY_UNAVAILABLE (503); бронирование отклонено целиком, слот остаётся Available; витрина работает read-only
Агент не принял решение до PendingExpiresAtWorker переводит запись Pending → Expired (слот освобождён/истёк, событие Cancelled); повторный проход — no-op; лента лида получает AppointmentCancelled (Expired)
Отмена инвестором после cutoffAPPOINTMENT_CUTOFF_PASSED; самостоятельная отмена невозможна — отменить может агент (без ограничения cutoff, до фиксации исхода)
Перенос: новый слот занят/истёкSLOT_ALREADY_BOOKED/CAL_SLOT_NOT_AVAILABLE: ничего не меняется, старая запись и её слот не тронуты (всё-или-ничего)
Фиксация исхода раньше времениAPPOINTMENT_NOT_STARTED (now < StartAt); до начала возможна отмена, но не исход
Повторная фиксация исхода / решение по неактуальной записиAPPOINTMENT_INVALID_STATE: Completed/NoShow терминальны и иммутабельны; ошибочный исход исправляется организационно (новая запись), не переписыванием
Гонка «агент ↔ инвестор ↔ worker»Побеждает первая зафиксированная мутация (оптимистическая блокировка), проигравший получает APPOINTMENT_INVALID_STATE; каскад на слот/событие выполняется ровно один раз
display-реплика identity в crm отстаётConsumer записей — CRM_CALENDAR_SYNC_USER_REPLICA_NOT_FOUND (retriable): лид создаётся после прихода user-upserted; порядок между сервисами не гарантирован
Событие обработано crm раньше appointment-created (out-of-order топиков)Контракты самодостаточны: get-or-create по данным пришедшего события, лента дополняется задним числом по времени факта без дублей клиента/лида
Инвестору не подошёл ни один слотNo-slot вход «время обсудим» (UC-OVR-001); оба входа сходятся в одном клиенте/лиде по телефону

Согласованность И Идемпотентность

  • Событийная интеграция без общих таблиц. calendar — единственный владелец слотов, записей, календарных событий и напоминаний; crm ведёт только воронку (клиент, лид, лента) и узнаёт о записи исключительно по событиям — синхронно в calendar не ходит, статус записи не реплицирует (лента append-only).
  • Атомарность в calendar. Каждая мутация записи (создание, подтверждение, отмена, перенос, исход) каскадно и атомарно затрагивает слот, календарное событие и напоминания; интеграционное событие уходит только после фиксации перехода. Ключ-идентификатор записи упорядочивает её переходы на партиции; кросс-топик порядок между appointment-* не гарантирован — контракты самодостаточны.
  • Идемпотентность воронки. Consumer записей дедуплицирует по идентификатору сообщения и по паре «запись + тип ленты» (не более одной записи ленты на факт); get-or-create клиента/лида идемпотентен по unique-индексам (пара «tenant + инвестор», «tenant + телефон», активный лид на пару). Полный replay всех топиков безопасен — сходится к той же воронке без дублей.
  • Единство воронки. Инвариант «один активный лид на пару агент ↔ инвестор» сводит слотовый и no-slot входы, а также повторные записи, к одному лиду; lead-created публикуется только на новый лид (повторные касания — без интеграционного события).
  • Окно eventual-consistency. Запись в календаре агента видна сразу; лид в crm и уведомления — асинхронно (< 1 мин штатно). В этом окне инвестор уже видит подтверждение записи, агент видит её в календаре, а карточка лида в CRM догоняется — это лаг проекции, не ошибка.
  • Иммутабельные снапшоты и атрибуция. Время/вид записи фиксируются снапшотом (архивация/правка батча слотов их не меняет); агент, приведший запись, фиксируется в лиде как Origin и начальный Executor — историческая атрибуция переживает последующую передачу лида.

Acceptance Criteria (Given/When/Then)

  • Happy (слот → лид → исход). Дано: агент опубликовал слоты, активного лида пары нет. Когда: инвестор бронирует свободный слот, агент подтверждает запись и фиксирует Completed. Тогда: слот прошёл Booked → Archived, запись Confirmed → Completed, в воронке создан активный лид (Source = Appointment, Origin = Executor) с лентой «создана → подтверждена → состоялась», агент и инвестор уведомлены.
  • Самозапись анонима. Дано: аноним с новым телефоном. Когда: бронирует слот. Тогда: lazy-регистрация создаёт инвестора, запись привязана к нему; повторное бронирование тем же телефоном использует существующего пользователя и тот же лид.
  • Решение агента и таймаут. Дано: Pending-запись без auto-confirm. Когда: агент подтверждает / отклоняет / не отвечает до PendingExpiresAt. Тогда: запись Confirmed / Declined / Expired с корректным каскадом слота и события; лента лида получает соответствующий факт; повторный проход worker-а не дублирует событий.
  • Отмена и перенос. Дано: активная запись. Когда: инвестор отменяет (в пределах cutoff) или переносит на другой слот. Тогда: отмена атомарно освобождает слот и событие; перенос — атомарная пара со ссылками RescheduledFrom/To, число активных записей не растёт, в ленте перенос помечен как продолжение (новый лид не создан); занятость нового слота откатывает всю пару.
  • Исход и терминальность. Дано: Confirmed-запись, now ≥ StartAt. Когда: агент фиксирует NoShow. Тогда: запись NoShow (терминальна), слот Archived, событие Completed, лента лида получает AppointmentNoShow как сигнал повторного контакта; повторная фиксация → APPOINTMENT_INVALID_STATE.
  • Единая воронка. Дано: инвестор записался на слот, затем оставил no-slot заявку тем же телефоном. Когда: оба входа обработаны. Тогда: клиент и активный лид — одни и те же, второй лид не создаётся.

Source Of Truth И Данные

ДанныеВладелецConsistency rule
Слот / запись / календарное событие / напоминанияcalendarАтомарный каскад; снапшот времени/вида иммутабелен; переходы — только событиями
UserEntity инвестораidentityLazy-регистрация идемпотентна по телефону
Client / Lead / лента лидаcrmGet-or-create по unique-индексам; один активный лид на пару; лента append-only
Привязка агента (tenant лида/клиента)calendar (через событие)Из записи (компания выбранного агента); правила реферала не применяются

Контракты И Зависимости

Наблюдаемость

  • critical metrics: конверсия «слот → запись → состоявшаяся встреча»; доля SLOT_ALREADY_BOOKED (интенсивность гонок), NoShow (качество записей), просроченных Pending (неотвечающие агенты); лаг CRM-проекции и уведомлений (< 1 мин).
  • trace path: витрина → calendar (бронирование/жизненный цикл) → шина событий → crm (воронка) / notifications; корреляция сквозного трейса из метаданных событий.
  • alerts: рост зависших Pending; лаг consumer записей > 5 мин; рост CRM_CALENDAR_SYNC_USER_REPLICA_NOT_FOUND (рассинхрон с синком identity); DLQ consumer записей > 0.

Риски И Guardrails

  • Риск: двойное бронирование одного слота. Control: условный переход Available → Booked — выигрывает ровно один запрос; на слот не более одной активной записи.
  • Риск: две воронки на одного инвестора (слотовый + no-slot вход). Control: get-or-create по телефону, один активный лид на пару агент ↔ инвестор.
  • Риск: PII формы записи в логах/событиях. Control: в payload appointment-* PII нет (ФИО/телефон — из реплики identity), телефон маской +7***NNNN, комментарий — класс Internal, в логи не пишется.
  • Риск: потеря/переупорядочивание фактов ленты при out-of-order доставке. Control: самодостаточные контракты, business-guard «запись + тип ленты», упорядочивание ленты по времени факта.

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

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