POST /api/investor/crm/leads/{leadId}/booking-intents — Инвестор добавляет объект-намерение
Инвестор помечает объект недвижимости как «хочу купить/забронировать» в рамках своего лида. Намерение — основание для будущей сделки по объекту этого застройщика; когда есть подтверждённое намерение и одобренная фиксация для пары (инвестор, застройщик), сделка сразу заводится в развёрнутой стадии (Flow A). Дом полей — lead.md.
Назначение
Принять от инвестора объект (PropertyObjectId) в контексте его лида и завести активное намерение
LeadBookingIntentEntity со стороной инвестора (Source = InvestorAdded, ConfirmedByInvestor = true
сразу — инвестор сам выразил намерение). Намерение фиксирует объект и его застройщика, но не создаёт
сделку и не вовлекает застройщика. Если по этому объекту в лиде уже есть предложенное агентом
неподтверждённое намерение (Source = AgentProposed) — операция его подтверждает (эквивалент
confirm-booking-intent.md).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | investor (JWT инвестора) |
| Feature group | booking-intents |
| Статус | draft |
| Документ | docs/06-services/crm/api/investor/add-booking-intent.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Инвестор) |
| Auth policy / Permissions | crm.leads.participate (инвесторская сторона лида) |
| Scope (RBAC) | не применяется: инвестор — вне agency-tenant (кросс-tenant участник, ADR-0052) |
| Record-level | lead.PrimaryInvestorUserId = actor.UserId (инвестор — сторона лида); форсится сервером |
| Tenant isolation | намерение TenantId = lead.TenantId = AgencyCompanyId; инвестор кросс-tenant → изоляция record-level по лиду (scope-bypass agency-tenant с явным предикатом) |
| Audit | create (AddedByUserId = actor.UserId) |
Выполнить операцию может только инвестор — сторона лида (lead.PrimaryInvestorUserId = actor.UserId);
проверка форсится сервером, а не телом запроса. Инвестор — кросс-tenant участник (ADR-0052): scope-based
RBAC не применяется, изоляция — record-level по лиду (TenantId намерения = TenantId лида =
AgencyCompanyId агентства).
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/investor/crm/leads/{leadId}/booking-intents |
| Success status | 201 (намерение создано) / 200 (идемпотентный повтор или подтверждение существующего предложенного) |
| Idempotency header | не применяется (бизнес-дедуп по INV-INTENT-1: одно активное намерение на пару LeadId+PropertyObjectId) |
| Correlation | сквозной trace через заголовок traceparent |
Отдельного idempotency-ключа нет: повторный вызов с тем же PropertyObjectId — не ошибка, а идемпотентный
возврат существующей карточки (200) или подтверждение предложенного агентом намерения (200); новое
активное намерение по паре LeadId+PropertyObjectId создаётся только один раз (INV-INTENT-1;
CRM_BOOKING_INTENT_ALREADY_EXISTS — только при гонке одновременных запросов).
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
LeadId | route | Guid | Да | != Guid.Empty; существующий лид Status = Active, инвестор — его сторона | — |
PropertyObjectId | body | Guid | Да | != Guid.Empty; существующий объект недвижимости chessboard (шаг 2); из него берутся ProjectId, DeveloperCompanyId, статус | — |
ActorUserId | identity claims | Guid | Да | JWT инвестора; record-level = lead.PrimaryInvestorUserId (форсится сервером) | scope isolation |
Модель ответа BookingIntentCardModel
Проекция намерения (дельта дома lead.md).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
IntentId | Guid | Да | LeadBookingIntentEntity.Id | — | Идентификатор намерения. |
LeadId | Guid | Да | LeadId | — | Лид-контекст. |
PropertyObjectId | Guid | Да | PropertyObjectId | — | Объект намерения. |
ProjectId | Guid | Да | ProjectId (снимок из chessboard) | — | Проект объекта. |
DeveloperCompanyId | Guid | Да | DeveloperCompanyId (снимок из chessboard) | — | Застройщик объекта. |
Source | string (enum BookingIntentSource) | Да | Source | — | InvestorAdded / AgentProposed. |
ConfirmedByInvestor | bool | Да | ConfirmedByInvestor | — | true — инвестор подтвердил намерение. |
ConfirmedAt | DateTimeOffset? | Нет | ConfirmedAt | — | Момент подтверждения инвестором. |
Status | string (enum BookingIntentStatus) | Да | Status | — | Active / Cancelled. |
DealId | Guid? | Нет | DealId | — | Сделка, созданная по намерению (если уже есть). |
CreatedAt | DateTimeOffset | Да | CreatedAt | — | Момент добавления. |
Алгоритм
-
Контекст и доступ. Операцию выполняет инвестор (permission
crm.leads.participate);ActorUserId— из токена (из тела не принимается);LeadIdиз route,PropertyObjectIdиз body. -
Проверка входа и предусловий.
- формат:
LeadId/PropertyObjectId!= Guid.Empty→ иначеVALIDATION_FAILED; - лид существует → иначе
CRM_LEAD_NOT_FOUND; - инвестор — сторона лида (
lead.PrimaryInvestorUserId == ActorUserId) → иначеCRM_LEAD_FORBIDDEN; - лид активен (
Status = Active) → иначеCRM_LEAD_NOT_ACTIVE; - существование объекта: запрос в
chessboardпоPropertyObjectId→ нет →CRM_PROPERTY_OBJECT_NOT_FOUND; из ответа берёмProjectId,DeveloperCompanyId, статус объекта; - допустимость состояния: статус объекта =
Свободен; иначеCRM_PROPERTY_OBJECT_NOT_AVAILABLE— намерение на занятый/снятый объект бессмысленно.
- формат:
-
Что читаем. Активное намерение лида по паре (
LeadId,PropertyObjectId) — для дедупа (INV-INTENT-1). -
Бизнес-правила и переходы (INV-INTENT-1). Если по паре (
LeadId,PropertyObjectId) уже есть активное намерение:ConfirmedByInvestor = true→ идемпотентный повтор: вернуть существующую карточку (200), ничего не менять;Source = AgentProposed,ConfirmedByInvestor = false→ подтвердить:ConfirmedByInvestor = true,ConfirmedAt = now; в ленту лида добавитьLeadEventType.BookingIntentConfirmed; вернуть карточку (200).
Иначе создать новое намерение:
Source = InvestorAdded,ConfirmedByInvestor = true,ConfirmedAt = now,Status = Active, снимокProjectId/DeveloperCompanyIdизchessboard,TenantId = lead.TenantId,AddedByUserId = ActorUserId,DealId = null. В ленту лида добавитьLeadEventType.BookingIntentAdded(payloadLeadBookingIntentRefPayload { IntentId, PropertyObjectId, DeveloperCompanyId }) и обновитьLastEventAt. Маппинг ответа поле-в-поле — по таблице выше. -
Что меняется. Появляется (или подтверждается) запись намерения и новое событие ленты лида — в одном согласованном изменении.
-
Результат. Карточка намерения →
201(создано) /200(идемпотентный повтор или подтверждение предложенного). При гонке уникальной пары (INV-INTENT-1) —CRM_BOOKING_INTENT_ALREADY_EXISTS.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | none (застройщик в намерении не участвует; developer-facing события возникают только при переходе сделки в Application) |
| Внешние вызовы | chessboard lookup объекта (timeout 2 c): существование, статус, ProjectId/DeveloperCompanyId |
| Проекции / поиск | none |
| Уведомления | опосредованно: агент видит новое намерение в ленте лида (LastEventAt); прямого push нет |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | LeadId/PropertyObjectId == Guid.Empty | «Проверьте корректность запроса» | no retry |
UNAUTHORIZED | 401 | нет/невалидный JWT | «Требуется авторизация» | no retry |
CRM_LEAD_NOT_FOUND | 404 | лид не найден | «Лид не найден» | no retry |
CRM_LEAD_FORBIDDEN | 403 | инвестор не сторона лида | «Нет доступа к лиду» | no retry |
CRM_LEAD_NOT_ACTIVE | 409 | lead.Status != Active | «Лид неактивен» | no retry |
CRM_PROPERTY_OBJECT_NOT_FOUND | 404 | объект не найден в chessboard | «Объект не найден» | no retry |
CRM_PROPERTY_OBJECT_NOT_AVAILABLE | 409 | статус объекта не Свободен | «Объект недоступен» | no retry |
CRM_BOOKING_INTENT_ALREADY_EXISTS | 409 | гонка INV-INTENT-1 (конфликт уникальности активного намерения по паре) | «Повторите попытку» | retry (перечитать намерение) |
При гонке двух параллельных вызовов на одну и ту же пару (LeadId, PropertyObjectId) второй вызов может
получить 409 CRM_BOOKING_INTENT_ALREADY_EXISTS, если первый уже создал намерение к моменту проверки
дедупа. Клиенту следует перечитать карточку намерения, а не слепо повторять запрос.
Acceptance Criteria
- AC-1 (happy — новое намерение). Дано: активный лид инвестора, объект
Свободен, намерения по объекту нет. Когда:POST. Тогда:201, намерениеSource = InvestorAdded,ConfirmedByInvestor = true,ConfirmedAtзаполнен, в ленте лидаBookingIntentAdded. - AC-2 (идемпотентный повтор). Дано: у лида уже есть активное подтверждённое намерение по объекту. Когда:
повтор
POST. Тогда:200с существующей карточкой, новое намерение/событие не создаются. - AC-3 (подтверждение предложенного). Дано: по объекту есть активное
AgentProposed-намерение (ConfirmedByInvestor = false). Когда:POST. Тогда:200,ConfirmedByInvestor = true,ConfirmedAtзаполнен, в ленте лидаBookingIntentConfirmed. - AC-4 (объект недоступен). Дано: статус объекта не
Свободен. Когда:POST. Тогда:409 CRM_PROPERTY_OBJECT_NOT_AVAILABLE. - AC-5 (чужой лид). Дано:
lead.PrimaryInvestorUserId != actor. Когда:POST. Тогда:403 CRM_LEAD_FORBIDDEN. - AC-6 (неактивный лид). Дано:
lead.Status != Active. Когда:POST. Тогда:409 CRM_LEAD_NOT_ACTIVE.
Совместимость и наблюдаемость
- Route и тело стабильны; новые optional-поля
BookingIntentCardModel— additive; удаление/переименование полей или сужение enumBookingIntentSource/BookingIntentStatus— breaking (migration + rollout). - Logs:
booking-intent added/confirmed(IntentId,LeadId,PropertyObjectId,Source); metrics:crm_booking_intents_total{source,result}, latency p95; traces — сквозной trace черезtraceparent; dashboardpin-crm.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (5): DELETE /api/investor/crm/leads/{leadId}/booking-intents/{intentId} — Инвестор снимает объект-намерение, GET /api/agent/crm/leads/{leadId}/booking-intents — Список намерений лида, POST /api/agent/crm/leads/{leadId}/booking-intents — Агент предлагает объект клиенту, POST /api/investor/crm/leads/{leadId}/booking-intents/{intentId}/confirm — Инвестор подтверждает предложенное намерение, POST /api/public/crm/booking-requests — Заявка на бронирование объекта (витрина)
- Use Cases (1): UC-CRM-016. Инвестор добавляет намерения бронирования (много объектов)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей, INV-INTENT-1, enum-ы,
LeadBookingIntentRefPayload): lead.md; лид-контекст иLeadEventType— lead.md. - Смежные методы: confirm-booking-intent.md (Flow B), remove-booking-intent.md (снятие), propose-booking-intent.md (агент предлагает), list-booking-intents.md (список лида).
- Продолжение flow (гейт сделки): create-deal.md (Flow A — сразу
Application; Flow B —Draft); заявка с витрины — create-booking-request.md. - ADR: 0052 tenancy, 0071 company-ownership и участники; карта области — README.