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

UC-CRM-016. Инвестор добавляет намерения бронирования (много объектов)

Назначение

Инвестор в рамках своего лида помечает объекты недвижимости как «хочу купить/забронировать» — заводит намерения LeadBookingIntentEntity. Намерений у лида сколько угодно (по разным объектам одного или разных застройщиков): инвестор постепенно собирает «список хотелок на объекты», добавляя их по одному. Намерение инвестора подтверждено сразу (Source = InvestorAdded, ConfirmedByInvestor = true), но не создаёт сделку и не вовлекает застройщика — оно лишь основание, из которого при одобренной фиксации для пары «инвестор × застройщик» вырастает сделка. Дом полей — ../domain/lead.md (раздел «Намерения бронирования»); здесь описан только поток добавления.

Metadata

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

Актор

ПолеЗначение
РольИнвестор — основной инвестор лида (InvestorPrimary)
Permissions / scopecrm.leads.participate (инвесторская сторона лида); scope не применяется — инвестор вне agency-tenant (кросс-tenant участник, ADR-0052)
TenantНамерение TenantId = lead.TenantId = AgencyCompanyId; инвестор кросс-tenant — изоляция record-level по лиду
Record-levelДоступ = lead.PrimaryInvestorUserId = actor (форсится сервером)

Предусловие

  • Лид существует и Status = Active (../domain/lead.md, INV-INTENT-2).
  • Актор — основной инвестор этого лида (InvestorPrimary).
  • Объект существует в витрине застройщика (chessboard/catalog) и доступен (статус «Свободен»).
  • По паре (LeadId, PropertyObjectId) нет другого активного намерения (INV-INTENT-1) — либо есть предложенное агентом неподтверждённое (AgentProposed), которое операция подтвердит.

Триггер

ПолеЗначение
ИсточникUI-действие → вызов API
Триггерящий элементКнопка «Хочу этот объект» / «Добавить в намерения» на карточке объекта в ЛК инвестора → POST /api/investor/crm/leads/{leadId}/booking-intents

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

  1. Контекст и доступ. Инвестор в своём активном лиде открывает объект застройщика и добавляет его в намерения. Каждое добавление — отдельный вызов; за сессию инвестор добавляет столько объектов, сколько рассматривает.
  2. Ввод. Запрос несёт PropertyObjectId выбранного объекта. Модель запроса AddBookingIntentRequest { PropertyObjectId }; LeadId — из маршрута, ActorUserId — из токена.
  3. Проверка входа. PropertyObjectId задан (!= Guid.Empty) — иначе VALIDATION_FAILED.
  4. Проверки состояния, доступа и объекта.
    • Лид существует — иначе CRM_LEAD_NOT_FOUND; актор — основной инвестор — иначе CRM_LEAD_FORBIDDEN.
    • Status = Active (INV-INTENT-2) — иначе CRM_LEAD_NOT_ACTIVE.
    • Объект существует в chessboard — иначе CRM_PROPERTY_OBJECT_NOT_FOUND; из ответа берутся снапшоты ProjectId, DeveloperCompanyId, статус.
    • Объект доступен (статус «Свободен») — иначе CRM_PROPERTY_OBJECT_NOT_AVAILABLE (намерение на занятый/снятый объект бессмысленно).
  5. Дедуп и переходы (INV-INTENT-1). По паре (LeadId, PropertyObjectId):
    • активное намерение уже подтверждено (ConfirmedByInvestor = true) → идемпотентный повтор: вернуть существующую карточку, ничего не менять;
    • активное намерение AgentProposed (ConfirmedByInvestor = false) → подтвердить его (ConfirmedByInvestor = true, ConfirmedAt = now), в ленту — BookingIntentConfirmed;
    • активного намерения нет → создать новое.
  6. Доменное решение (создание, INV-INTENT-3/5). Заводится намерение: Source = InvestorAdded, ConfirmedByInvestor = true, ConfirmedAt = CreatedAt, Status = Active, снапшоты ProjectId/DeveloperCompanyId из chessboard (set-once, иммутабельны), TenantId = lead.TenantId, AddedByUserId = actor, DealId = null.
  7. Что меняется. Создаётся (или подтверждается) намерение и добавляется запись ленты лида LeadEventEntity(BookingIntentAdded) (payload LeadBookingIntentRefPayload), обновляется LastEventAt — в одном согласованном изменении вместе с записью в outbox.
  8. Публикация. Интеграционное событие pin.crm.lead-booking-intent-changed.v1 (ChangeKind = Added) — без вовлечения застройщика.
  9. Результат. Возвращается карточка намерения (201 создано / 200 идемпотентный повтор или подтверждение предложенного). Объект появляется в списке намерений лида; повтор шагов 1–9 для следующего объекта пополняет список.
  10. Асинхронно. notifications уведомляет исполнителя лида о новом намерении инвестора (сигнал завести фиксацию/сделку); застройщик о намерении не уведомляется.

Диаграмма

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

A1. Подтверждение предложенного агентом объекта

  1. По объекту уже есть активное AgentProposed-намерение (ConfirmedByInvestor = false).
  2. Операция подтверждает его (ConfirmedByInvestor = true, ConfirmedAt = now), в ленту — BookingIntentConfirmed, событие ChangeKind = Confirmed; 200.
Эквивалентность операции подтверждения

Эквивалентно UC-CRM-017 (подтверждение через .../confirm).

A2. Идемпотентный повтор

  1. По объекту уже есть активное подтверждённое намерение (ConfirmedByInvestor = true).
  2. Возвращается существующая карточка (200), новое намерение/событие не создаются.
Не ошибка

Повтор завершается идемпотентно: HTTP 200, состав намерений лида не меняется.

A3. Объект недоступен

  1. Статус объекта в chessboard не «Свободен» (занят/снят/продан).
  2. CRM_PROPERTY_OBJECT_NOT_AVAILABLE (409); намерение не заводится.
Объект недоступен

Возврат: инвестор выбирает другой объект.

A4. Лид неактивен

  1. Лид Expired/Archived.
  2. CRM_LEAD_NOT_ACTIVE (409).
Лид неактивен

Возврат: агент продлевает/реактивирует лид (INV-LEAD-3), затем повтор.

A5. Гонка уникальной пары

  1. Параллельно завелось второе намерение на ту же пару (INV-INTENT-1, unique partial).
  2. CRM_INTENT_ACTIVE_OBJECT_EXISTS (409).
Гонка уникальной пары

Возврат: перечитать намерения лида и повторить.

Постусловие

  • Создано (или подтверждено) намерение Status = Active на пару (LeadId, PropertyObjectId); Source = InvestorAdded, ConfirmedByInvestor = true, ConfirmedAt заполнен; среди активных — ровно одно на объект (INV-INTENT-1).
  • В ленте лида — LeadEventEntity(BookingIntentAdded) (или BookingIntentConfirmed в A1); LastEventAt обновлён; лид остаётся Active.
  • Опубликован ровно один pin.crm.lead-booking-intent-changed.v1; сделка не создана, застройщик не вовлечён.
  • Объект — в списке намерений лида; повторное добавление других объектов расширяет список независимо.

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

КодHTTPУсловиеПоведение клиента
VALIDATION_FAILED400PropertyObjectId == Guid.EmptyПоказать ошибку поля
UNAUTHORIZED401Нет/невалидный JWT инвестораТребуется авторизация
CRM_LEAD_NOT_FOUND404Лид не найденСообщить, что лид не найден
CRM_LEAD_FORBIDDEN403Актор — не основной инвестор лидаСкрыть действие
CRM_LEAD_NOT_ACTIVE409Лид Expired/Archived (INV-INTENT-2)Предложить продлить/реактивировать лид
CRM_PROPERTY_OBJECT_NOT_FOUND404Объект не найден в chessboardСообщить, что объект не найден
CRM_PROPERTY_OBJECT_NOT_AVAILABLE409Статус объекта не «Свободен»Предложить выбрать другой объект
CRM_INTENT_ACTIVE_OBJECT_EXISTS409Гонка INV-INTENT-1 (активное намерение на пару уже есть)Перечитать намерения и повторить

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

СобытиеКогда поднимаетсяSubscribers
LeadBookingIntentChangedСоздание/подтверждение намерения (не в no-op)Внутренний диспетчер → запись LeadEventEntity(BookingIntentAdded/Confirmed) и сборка интеграционного события

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

СобытиеTopicКогда публикуетсяConsumers
LeadBookingIntentChangedEventpin.crm.lead-booking-intent-changed.v1Добавление намерения (ChangeKind = Added) / подтверждение предложенного (Confirmed), через outbox атомарноnotifications (уведомление исполнителю лида о намерении инвестора), messenger (по необходимости — карточка объекта в чате лида) — контракт ../events/lead-booking-intent-changed.md

Acceptance Criteria

#КритерийПроверка
AC1Инвестор добавляет объект в активный лид → намерение Active, Source = InvestorAdded, ConfirmedByInvestor = true, ConfirmedAt заполнен, в ленте BookingIntentAddedТест POST .../booking-intents → 201; проверить поля и ленту
AC2Инвестор добавляет несколько разных объектов → несколько активных намерений (по одному на объект), список пополняетсяТест: N вызовов по разным PropertyObjectId → N активных намерений
AC3Повтор на уже подтверждённый объект → 200, состав намерений без изменений, событие не публикуетсяТест идемпотентного no-op
AC4Объект с активным AgentProposed-намерением → добавление подтверждает его (ConfirmedByInvestor = true), в ленте BookingIntentConfirmedТест A1
AC5Второе намерение на тот же объект → CRM_INTENT_ACTIVE_OBJECT_EXISTS (409)Негативный тест INV-INTENT-1
AC6Объект не «Свободен» → CRM_PROPERTY_OBJECT_NOT_AVAILABLE; лид ExpiredCRM_LEAD_NOT_ACTIVE; чужой инвестор → CRM_LEAD_FORBIDDENНегативные тесты состояния и доступа
AC7На добавлении застройщик не вовлекается: сделка не создаётся, developer-facing событий нетТест: publish только pin.crm.lead-booking-intent-changed.v1, нет pin.crm.deal-created.v1

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

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

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