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

POST /api/agent/crm/leads/{leadId}/booking-intents — Агент предлагает объект клиенту

Агент предлагает инвестору конкретный объект в рамках лида: заводит намерение со стороны агента (Source = AgentProposed), которое ждёт подтверждения инвестором (Flow B). До подтверждения намерение активно, но ConfirmedByInvestor = false — сделку по нему можно завести только в статусе Draft, застройщик не вовлекается. Дом полей — lead.md.

Назначение

Принять от агента объект (PropertyObjectId) в контексте лида и завести намерение LeadBookingIntentEntity со стороной агента (Source = AgentProposed, ConfirmedByInvestor = false). Это предложение инвестору поработать по объекту; инвестор подтверждает его позже (confirm-booking-intent.md). Намерение не создаёт сделку и не вовлекает застройщика.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areaagent (JWT агента)
Feature groupbooking-intents
Статусdraft
Документdocs/06-services/crm/api/agent/propose-booking-intent.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Агент); Operator (Менеджер ПИН — platform-scope, от имени агента)
Auth policy / Permissionscrm.leads.manage (агентская сторона лида)
Scope (RBAC)организация (агентство): record-level по лиду
Record-levelАктор — активный участник лида (обычно Executor) либо руководитель компании-владельца (AgencyCompanyId = lead.TenantId) / Менеджер ПИН
Tenant isolationлид читается в tenant актора (AgencyCompanyId); намерение TenantId = lead.TenantId = AgencyCompanyId
Auditcreate (AddedByUserId = actor.UserId)
Доступ

Выполнить операцию может только активный участник лида со стороны агентства (обычно Executor) либо руководитель компании-владельца лида (AgencyCompanyId = lead.TenantId); либо Менеджер ПИН — от имени агента, в рамках platform-scope. Намерение создаётся в tenant агентства (TenantId = lead.TenantId).

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/agent/crm/leads/{leadId}/booking-intents
Success status201 (намерение создано)
Idempotency headerне применяется (бизнес-дедуп по INV-INTENT-1: одно активное намерение на пару LeadId+PropertyObjectId)
Correlationсквозной trace через заголовок traceparent
подсказка

Отдельного idempotency-ключа нет: повторное предложение того же объекта по тому же лиду блокируется бизнес-инвариантом INV-INTENT-1 — активное намерение на пару LeadId+PropertyObjectId уже одно (CRM_BOOKING_INTENT_ALREADY_EXISTS).

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
LeadIdrouteGuidДа!= Guid.Empty; существующий лид Status = Active, принадлежит агентству актора
PropertyObjectIdbodyGuidДа!= Guid.Empty; существующий объект недвижимости chessboard (шаг 2); из него берутся ProjectId, DeveloperCompanyId, статус
ActorUserIdidentity claimsGuidДаJWT агента; = lead.ExecutorAgentUserId (кроме подачи Менеджером ПИН)
ActorAgencyCompanyIdidentity claimsGuidДаtenant агентства актора — record-level по лидуTenant isolation

Модель ответа BookingIntentCardModel

Проекция созданного намерения (дельта дома lead.md); полная таблица полей — add-booking-intent.md. Значимые для операции значения: Source = AgentProposed, ConfirmedByInvestor = false, ConfirmedAt = null.

Алгоритм

  1. Контекст и доступ. Операцию выполняет агент (permission crm.leads.manage) в tenant своего агентства; либо Менеджер ПИН от имени агента (platform-scope).
  2. Проверка входа и предусловий.
    • формат: LeadId/PropertyObjectId != Guid.Empty → иначе VALIDATION_FAILED;
    • лид существует → иначе CRM_LEAD_NOT_FOUND;
    • лид принадлежит агентству актора и агент — активный участник лида → иначе CRM_LEAD_FORBIDDEN;
    • лид активен (Status = Active) → иначе CRM_LEAD_NOT_ACTIVE;
    • существование объекта: запрос в chessboard по PropertyObjectId → нет → CRM_PROPERTY_OBJECT_NOT_FOUND; из ответа берём ProjectId, DeveloperCompanyId, статус объекта;
    • допустимость состояния: статус объекта = Свободен; иначе CRM_PROPERTY_OBJECT_NOT_AVAILABLE.
  3. Что читаем. Активное намерение лида по паре (LeadId, PropertyObjectId) — для дедупа (INV-INTENT-1).
  4. Бизнес-правила и переходы (INV-INTENT-1). Если по паре (LeadId, PropertyObjectId) уже есть активное намерение (любой стороны) — CRM_BOOKING_INTENT_ALREADY_EXISTS (повторное предложение бессмысленно; если инвестор уже добавил объект — намерение уже подтверждено). Иначе создать намерение: Source = AgentProposed, ConfirmedByInvestor = false, ConfirmedAt = null, Status = Active, снимок ProjectId/DeveloperCompanyId из chessboard, TenantId = lead.TenantId, AddedByUserId = ActorUserId, DealId = null. В ленту лида добавить LeadEventType.BookingIntentAdded (payload LeadBookingIntentRefPayload { IntentId, PropertyObjectId, DeveloperCompanyId }) и обновить LastEventAt.
  5. Что меняется. Появляется запись намерения и новое событие ленты лида — в одном согласованном изменении.
  6. Результат. Карточка намерения → 201. При гонке уникальной пары (INV-INTENT-1) — CRM_BOOKING_INTENT_ALREADY_EXISTS.

Побочные эффекты

ТипДетали
Интеграционные событияnone (застройщик не вовлекается)
Внешние вызовыchessboard lookup объекта (timeout 2 c): существование, статус, ProjectId/DeveloperCompanyId
Проекции / поискnone
Уведомленияинвестор видит предложение в ленте лида (LastEventAt); push инвестору — best-effort через notifications по активности лида

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400LeadId/PropertyObjectId == Guid.Empty«Проверьте корректность запроса»no retry
UNAUTHORIZED401нет/невалидный JWT или нет crm.leads.manage«Требуется авторизация»no retry
CRM_LEAD_NOT_FOUND404лид не найден«Лид не найден»no retry
CRM_LEAD_FORBIDDEN403лид другого агентства/агента«Нет доступа к лиду»no retry
CRM_LEAD_NOT_ACTIVE409lead.Status != Active«Лид неактивен — создайте или продлите лид»no retry
CRM_PROPERTY_OBJECT_NOT_FOUND404объект не найден в chessboard«Объект не найден»no retry
CRM_PROPERTY_OBJECT_NOT_AVAILABLE409статус объекта не Свободен«Объект недоступен»no retry
CRM_BOOKING_INTENT_ALREADY_EXISTS409активное намерение по паре (LeadId, PropertyObjectId) уже есть (INV-INTENT-1)«По этому объекту уже есть намерение»no retry (перечитать намерения)
Ошибки

При гонке на создание намерения по одной и той же паре (LeadId, PropertyObjectId) побеждает первый запрос; второй получает 409 CRM_BOOKING_INTENT_ALREADY_EXISTS. Это не повод для retry — клиенту следует перечитать список намерений лида (list-booking-intents.md).

Acceptance Criteria

  • AC-1 (happy — предложение). Дано: активный лид агентства, объект Свободен, намерения по объекту нет. Когда: POST. Тогда: 201, намерение Source = AgentProposed, ConfirmedByInvestor = false, в ленте лида BookingIntentAdded.
  • AC-2 (дубль намерения). Дано: по объекту уже есть активное намерение. Когда: POST. Тогда: 409 CRM_BOOKING_INTENT_ALREADY_EXISTS.
  • AC-3 (объект недоступен). Дано: статус объекта не Свободен. Когда: POST. Тогда: 409 CRM_PROPERTY_OBJECT_NOT_AVAILABLE.
  • AC-4 (чужой лид). Дано: лид другого агентства/агента. Когда: POST. Тогда: 403 CRM_LEAD_FORBIDDEN.
  • AC-5 (неактивный лид). Дано: lead.Status != Active. Когда: POST. Тогда: 409 CRM_LEAD_NOT_ACTIVE.

Совместимость и наблюдаемость

  • Route и тело стабильны; поля ответа additive; сужение enum намерения — breaking (migration + rollout).
  • Logs: booking-intent proposed (IntentId, LeadId, PropertyObjectId); metrics: crm_booking_intents_total{source=proposed,result}, latency p95; traces — сквозной trace через traceparent; dashboard pin-crm.

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

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

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