UC-CRM-016. Инвестор добавляет намерения бронирования (много объектов)
Назначение
Инвестор в рамках своего лида помечает объекты недвижимости как «хочу купить/забронировать» — заводит
намерения LeadBookingIntentEntity. Намерений у лида сколько угодно (по разным объектам одного или разных
застройщиков): инвестор постепенно собирает «список хотелок на объекты», добавляя их по одному. Намерение
инвестора подтверждено сразу (Source = InvestorAdded, ConfirmedByInvestor = true), но не создаёт
сделку и не вовлекает застройщика — оно лишь основание, из которого при одобренной фиксации для пары
«инвестор × застройщик» вырастает сделка. Дом полей — ../domain/lead.md (раздел
«Намерения бронирования»); здесь описан только поток добавления.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CRM-016 |
| Контекст | crm (см. docs/06-services/crm/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор — основной инвестор лида (InvestorPrimary) |
| Permissions / scope | crm.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 |
Основной Поток
- Контекст и доступ. Инвестор в своём активном лиде открывает объект застройщика и добавляет его в намерения. Каждое добавление — отдельный вызов; за сессию инвестор добавляет столько объектов, сколько рассматривает.
- Ввод. Запрос несёт
PropertyObjectIdвыбранного объекта. Модель запросаAddBookingIntentRequest { PropertyObjectId };LeadId— из маршрута,ActorUserId— из токена. - Проверка входа.
PropertyObjectIdзадан (!= Guid.Empty) — иначеVALIDATION_FAILED. - Проверки состояния, доступа и объекта.
- Лид существует — иначе
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(намерение на занятый/снятый объект бессмысленно).
- Лид существует — иначе
- Дедуп и переходы (INV-INTENT-1). По паре (
LeadId,PropertyObjectId):- активное намерение уже подтверждено (
ConfirmedByInvestor = true) → идемпотентный повтор: вернуть существующую карточку, ничего не менять; - активное намерение
AgentProposed(ConfirmedByInvestor = false) → подтвердить его (ConfirmedByInvestor = true,ConfirmedAt = now), в ленту —BookingIntentConfirmed; - активного намерения нет → создать новое.
- активное намерение уже подтверждено (
- Доменное решение (создание, INV-INTENT-3/5). Заводится намерение:
Source = InvestorAdded,ConfirmedByInvestor = true,ConfirmedAt = CreatedAt,Status = Active, снапшотыProjectId/DeveloperCompanyIdизchessboard(set-once, иммутабельны),TenantId = lead.TenantId,AddedByUserId = actor,DealId = null. - Что меняется. Создаётся (или подтверждается) намерение и добавляется запись ленты лида
LeadEventEntity(BookingIntentAdded)(payloadLeadBookingIntentRefPayload), обновляетсяLastEventAt— в одном согласованном изменении вместе с записью в outbox. - Публикация. Интеграционное событие
pin.crm.lead-booking-intent-changed.v1(ChangeKind = Added) — без вовлечения застройщика. - Результат. Возвращается карточка намерения (
201создано /200идемпотентный повтор или подтверждение предложенного). Объект появляется в списке намерений лида; повтор шагов 1–9 для следующего объекта пополняет список. - Асинхронно.
notificationsуведомляет исполнителя лида о новом намерении инвестора (сигнал завести фиксацию/сделку); застройщик о намерении не уведомляется.
Диаграмма
Альтернативные Потоки
A1. Подтверждение предложенного агентом объекта
- По объекту уже есть активное
AgentProposed-намерение (ConfirmedByInvestor = false). - Операция подтверждает его (
ConfirmedByInvestor = true,ConfirmedAt = now), в ленту —BookingIntentConfirmed, событиеChangeKind = Confirmed;200.
Эквивалентно UC-CRM-017 (подтверждение через .../confirm).
A2. Идемпотентный повтор
- По объекту уже есть активное подтверждённое намерение (
ConfirmedByInvestor = true). - Возвращается существующая карточка (
200), новое намерение/событие не создаются.
Повтор завершается идемпотентно: HTTP 200, состав намерений лида не меняется.
A3. Объект недоступен
- Статус объекта в
chessboardне «Свободен» (занят/снят/продан). CRM_PROPERTY_OBJECT_NOT_AVAILABLE(409); намерение не заводится.
Возврат: инвестор выбирает другой объект.
A4. Лид неактивен
- Лид
Expired/Archived. CRM_LEAD_NOT_ACTIVE(409).
Возврат: агент продлевает/реактивирует лид (INV-LEAD-3), затем повтор.
A5. Гонка уникальной пары
- Параллельно завелось второе намерение на ту же пару (INV-INTENT-1, unique partial).
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_FAILED | 400 | PropertyObjectId == Guid.Empty | Показать ошибку поля |
UNAUTHORIZED | 401 | Нет/невалидный JWT инвестора | Требуется авторизация |
CRM_LEAD_NOT_FOUND | 404 | Лид не найден | Сообщить, что лид не найден |
CRM_LEAD_FORBIDDEN | 403 | Актор — не основной инвестор лида | Скрыть действие |
CRM_LEAD_NOT_ACTIVE | 409 | Лид Expired/Archived (INV-INTENT-2) | Предложить продлить/реактивировать лид |
CRM_PROPERTY_OBJECT_NOT_FOUND | 404 | Объект не найден в chessboard | Сообщить, что объект не найден |
CRM_PROPERTY_OBJECT_NOT_AVAILABLE | 409 | Статус объекта не «Свободен» | Предложить выбрать другой объект |
CRM_INTENT_ACTIVE_OBJECT_EXISTS | 409 | Гонка INV-INTENT-1 (активное намерение на пару уже есть) | Перечитать намерения и повторить |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
LeadBookingIntentChanged | Создание/подтверждение намерения (не в no-op) | Внутренний диспетчер → запись LeadEventEntity(BookingIntentAdded/Confirmed) и сборка интеграционного события |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
LeadBookingIntentChangedEvent | pin.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; лид Expired → CRM_LEAD_NOT_ACTIVE; чужой инвестор → CRM_LEAD_FORBIDDEN | Негативные тесты состояния и доступа |
| AC7 | На добавлении застройщик не вовлекается: сделка не создаётся, developer-facing событий нет | Тест: publish только pin.crm.lead-booking-intent-changed.v1, нет pin.crm.deal-created.v1 |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-CRM-017. Flow B: агент предлагает объект → Draft-сделка → подтверждение инвестором → вовлечение застройщика, UC-CRM-018. Несколько сделок из одной одобренной фиксации (по разным объектам застройщика)
- События (1): Event Contract: pin.crm.lead-booking-intent-changed (LeadBookingIntentChangedEvent)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API метод: ../api/investor/add-booking-intent.md; снятие — ../api/investor/remove-booking-intent.md; список — ../api/agent/list-booking-intents.md.
- Domain entity: ../domain/lead.md (
LeadBookingIntentEntity, INV-INTENT-1..6, enumLeadBookingIntentSource/LeadBookingIntentStatus,LeadEventType.BookingIntentAdded), ../domain/deal.md (гейтDraft → Application). - Kafka событие: ../events/lead-booking-intent-changed.md.
- Смежные UC: UC-CRM-017-draft-deal-investor-confirmation.md (Flow B), UC-CRM-018-multiple-deals-from-one-fixation.md, UC-CRM-002-agent-creates-deal-from-lead.md.
- ADR: 0052 tenancy, 0071 crm-ownership-and-participant-model.