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

POST /api/agent/crm/leads/{leadId}/property-object-context — Контекст инвестора по объектам недвижимости

Пакетная композиция «в контексте инвестора лида»: по списку PropertyObjectId агент получает для основного инвестора лида его поведенческий и личный контекст — сколько раз и как долго инвестор смотрел объект, лежит ли объект в избранном (у агента и/или у инвестора) и в какие подборки инвестора он входит. Метод ничего не считает сам: он резолвит инвестора из лида, проверяет право агента на активный лид и делегированно дозагружает три internal-источника (просмотры, избранное, подборки), сшивая их по PropertyObjectId. Каталог остаётся без персонализации (платформенная проекция, ADR-0056 Часть D) — обогащение живёт здесь, в аутентифицированном CRM-слое, и накладывается фронтом оверлеем поверх карточек каталога/избранного/подборок.

Назначение

Дать агенту на карточке лида и в его рабочих списках объектов (каталог/шахматка/избранное/подборки) увидеть, как этот объект соотносится с интересами его клиента-инвестора: смотрел ли инвестор объект и насколько активно, отмечен ли объект в избранном самим агентом и/или инвестором (независимые флаги, 3-state), в какие подборки инвестора он уже добавлен. Фронт агента вызывает метод параллельно с публичными каталогом/избранным/подборками и накладывает вернувшийся контекст оверлеем на уже отрисованные карточки — без изменения самих платформенных проекций.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areaagent (JWT агента/куратора/руководителя агентства/Менеджера ПИН)
Feature groupleads
Статусdraft
Документdocs/06-services/crm/api/agent/get-property-object-investor-context.md

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

ПроверкаЗначение
Actor typeUser (агент-исполнитель / куратор лида) / User (руководитель агентства) / Operator(Admin) (Менеджер ПИН, platform-scope)
Auth policy / Permissionscrm.leads.view
Scope (RBAC)организация (агентство): TenantId = AgencyCompanyId; руководитель — весь свой TenantId; Менеджер ПИН — платформа
Record-levelдоступ = актор ∈ активные участники агентской роли лида (ExecutorAgentUserId = actor / членство) или руководитель компании-владельца или Менеджер ПИН; иначе CRM_LEAD_FORBIDDEN
Tenant isolationTenantId в каждом запросе; lead.TenantId обязан совпасть
Auditread-sensitive: доступ агента к активности целевого инвестора (actor, PrimaryInvestorUserId, без PII)
Доступ

Авторизация — делегированная (паттерн UC-ACT-004): право «активный лид агента на инвестора» проверяет сам crm перед вызовом internal-источников; источники (user-activity/favorites/selections) доверяют переданному userId и повторно его не перепроверяют.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/agent/crm/leads/{leadId}/property-object-context
Success status200 (контекст собран; частичная деградация источников не роняет ответ)
Idempotency headerне применяется (безопасное чтение, оформленное POST из-за батч-тела)
Correlationсквозной trace через заголовок traceparent

Метод — чтение (состояние не меняется); POST выбран из-за списочного тела (PropertyObjectIds не помещается в query безопасно). Кешируется только на клиенте на время сессии карточки.

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired; = AgencyCompanyIdTenant isolation
ActorUserIdidentity claimsGuidДаRequiredRecord-level (членство)
LeadIdrouteGuidДа!= Guid.Empty; существующий лид в tenant
PropertyObjectIdsbodyList<Guid>ДаRequired; 1..200 элементов; каждый != Guid.Empty; дубликаты схлопываются

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

Обёртка + один элемент на каждый запрошенный (после дедупликации) PropertyObjectId — включая объекты без активности/избранного/подборок (пустой контекст), чтобы фронт мог наложить оверлей строго 1:1 по id.

ПолеТипОбязательностьИсточникMaskingОписание
LeadIdGuidДаlead.IdЛид, в контексте которого собран ответ.
PrimaryInvestorUserIdGuidДаlead.PrimaryInvestorUserIdИнвестор, чей контекст отражён (основной инвестор лида).
ItemsList<PropertyObjectInvestorContextModel>ДакомпозицияПо одному элементу на запрошенный объект.

PropertyObjectInvestorContextModel

ПолеТипОбязательностьИсточникMaskingОписание
PropertyObjectIdGuidДавходОбъект недвижимости из запроса.
InvestorViewsInvestorViewsModelДаD1 (user-activity)Агрегат просмотров инвестора по объекту; при отсутствии просмотров — нули/null.
FavoritedByAgentboolДаD2 (favorites)Объект в избранном самого агента-актора (UserId = ActorUserId).
FavoritedByInvestorboolДаD2 (favorites)Объект в избранном инвестора (UserId = PrimaryInvestorUserId). Независим от FavoritedByAgent.
InInvestorSelectionsList<SelectionRef>ДаD3 (selections)Подборки инвестора, в которые входит объект; пусто — если ни в одну.

InvestorViewsModel

ПолеТипОбязательностьИсточникОписание
CountintДаD1Число закрытых просмотров объекта инвестором (0 — не смотрел).
TotalDurationSecondslongДаD1Суммарная длительность закрытых просмотров, сек (0 — не смотрел).
LastViewedAtDateTimeOffset?НетD1Когда инвестор смотрел объект в последний раз; null — не смотрел.

SelectionRef

ПолеТипОбязательностьИсточникОписание
SelectionIdGuidДаD3Идентификатор подборки инвестора.
TitlestringДаD3Название подборки (для подписи оверлея).
3-state избранного

FavoritedByAgent и FavoritedByInvestor — вывод из владельца строки, не отдельная сущность: вычисляются D2 как наличие строки Favorite соответствующего владельца (UserId = ActorUserId / UserId = PrimaryInvestorUserId); нет строки → флаг false. Флаги независимы: объект может быть у обоих, у одного или ни у кого. Личная заметка (Note) владельца никогда не отдаётся кросс-владельца — метод её не запрашивает и не возвращает.

Алгоритм

  1. Контекст и доступ. Операцию выполняет участник агентской роли лида / руководитель агентства / Менеджер ПИН (permission crm.leads.view) в рамках TenantId.
  2. Проверка входа и предусловий.
    • формат: LeadId != Guid.Empty; PropertyObjectIds1..200 непустых Guid; иначе VALIDATION_FAILED;
    • лид существует в TenantId — иначе CRM_LEAD_NOT_FOUND;
    • record-level: актор ∈ активные участники агентской роли лида (ExecutorAgentUserId = actor / членство) или руководитель компании-владельца или Менеджер ПИН — иначе CRM_LEAD_FORBIDDEN.
  3. Резолв инвестора. Из лида берётся производная PrimaryInvestorUserId (основной инвестор пары «исполнитель ↔ основной инвестор», INV-CRM-001; дом — lead.md). Инвариант INV-LEAD-7 гарантирует ровно одного основного инвестора у активного лида.
  4. Нормализация запроса. PropertyObjectIds дедуплицируются; каждому сохранённому id соответствует ровно один выходной элемент (даже если контекст пуст).
  5. Делегированная пакетная дозагрузка (параллельно). По доверенным userId (record-level уже проверен на шаге 2) crm вызывает три internal-источника, каждый — батчем по PropertyObjectIds:
    • D1 — просмотры инвестора: user-activity по (PrimaryInvestorUserId, PropertyObjectIds){ PropertyObjectId, Count, TotalDurationSeconds, LastViewedAt } (только закрытые просмотры; открытые не входят);
    • D2 — статус избранного: favorites по (UserIds = [ActorUserId, PrimaryInvestorUserId], PropertyObjectIds) → для каждого объекта наличие строки на агента и на инвестора (Note не запрашивается);
    • D3 — подборки инвестора: selections по (PrimaryInvestorUserId, PropertyObjectIds) → для каждого объекта список подборок инвестора (SelectionId, Title), в которые он входит.
  6. Сшивка по PropertyObjectId. Результаты трёх источников мёржатся по ключу объекта: InvestorViews — из D1 (нет записи → Count = 0, TotalDurationSeconds = 0, LastViewedAt = null); FavoritedByAgent/ FavoritedByInvestor — из D2 (нет строки → false); InInvestorSelections — из D3 (нет вхождений → пустой список).
  7. Деградация источника. Если internal-источник недоступен, его секция отдаётся «пустой» (просмотры — нули, флаги — false, подборки — пусто), а операция всё равно возвращает 200 — фронт накладывает оверлей по тем данным, что пришли (graceful degradation, как в UC-ACT-004 A5). Полная недоступность лида/CRM — не деградация, а отказ (CRM_LEAD_*).
  8. Результат. PropertyObjectInvestorContextListModel с элементом на каждый запрошенный объект → HTTP 200. Свежесть eventual: закрытие просмотров происходит polling-циклом worker-а user-activity (10–15 мин, ADR-0056), поэтому только что просмотренный объект может ещё не отразиться в InvestorViews; избранное/подборки — актуальны на момент чтения соответствующих источников.

Диаграмма

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

ТипДетали
Интеграционные событияnone (read-only)
Внешние вызовыm2m internal: user-activity (D1), favorites (D2), selections (D3) — параллельно, с таймаутом; деградация секции при недоступности
Проекции / поискне затрагиваются; каталог/избранное/подборки остаются платформенными проекциями без персонализации
Уведомленияnone

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400LeadId = Guid.Empty; PropertyObjectIds пуст / > 200 / содержит Guid.Empty«Проверьте корректность полей»no retry
CRM_LEAD_NOT_FOUND404лид не найден по LeadId в tenant«Лид не найден»no retry
CRM_LEAD_FORBIDDEN403актор не участник агентской роли / не руководитель / не Менеджер ПИН«Недостаточно прав на просмотр лида»no retry
Деградация источников — не ошибка

Недоступность отдельного internal-источника (D1/D2/D3) — не ошибка: секция деградирует до пустой, ответ 200 (см. Алгоритм п.7).

Предусловия и постусловия

Предусловия (истинны до сценария):

  • актор авторизован, имеет permission crm.leads.view, является участником агентской роли лида / руководителем агентства / Менеджером ПИН;
  • лид существует в TenantId актора и имеет основного инвестора (INV-LEAD-7);
  • PropertyObjectIds — непустой список (1..200) корректных Guid.

Постусловия (гарантированы после успеха):

  • состояние не изменилось (read-only: ни мутаций, ни доменных, ни интеграционных событий);
  • возвращён контекст только основного инвестора данного лида; чужой контекст недоступен ни при каких параметрах;
  • на каждый запрошенный (после дедупликации) объект есть ровно один элемент ответа; объекты без активности — с пустым контекстом (нули/false/пустой список);
  • личные заметки избранного (Note) не раскрыты кросс-владельца;
  • зафиксирован read-sensitive access-audit (actor, PrimaryInvestorUserId, без PII).

Acceptance Criteria

  • Happy path. Дано: активный лид, актор — исполнитель, PropertyObjectIds = [O1, O2]; инвестор смотрел O1 дважды, O1 у инвестора в избранном и в подборке S1, O2 — у агента в избранном. Когда: POST. Тогда: 200, для O1 — InvestorViews.Count = 2, FavoritedByInvestor = true, InInvestorSelections = [S1]; для O2 — FavoritedByAgent = true, остальное пусто.
  • 3-state независимость. Дано: O в избранном и у агента, и у инвестора. Когда: POST. Тогда: FavoritedByAgent = true и FavoritedByInvestor = true независимо; заметки не возвращены.
  • Объект без контекста. Дано: O3 не смотрелся, не в избранном, не в подборках. Когда: POST c O3. Тогда: элемент O3 присутствует с Count = 0, LastViewedAt = null, оба флага false, InInvestorSelections = [].
  • Чужой лид. Дано: актор не участник агентской роли / не руководитель / не Менеджер ПИН. Когда: POST. Тогда: 403 CRM_LEAD_FORBIDDEN, источники не вызываются.
  • Несуществующий лид. Дано: LeadId не в tenant. Когда: POST. Тогда: 404 CRM_LEAD_NOT_FOUND.
  • Деградация источника. Дано: selections недоступен. Когда: POST. Тогда: 200, InInvestorSelections пуст у всех объектов, просмотры и избранное — заполнены.
  • Свежесть. Дано: инвестор только что открыл O (просмотр ещё не закрыт worker-ом). Когда: POST. Тогда: O может вернуться с Count, не учитывающим открытый просмотр (eventual, ADR-0056) — не ошибка.
  • Дедупликация. Дано: PropertyObjectIds = [O1, O1, O2]. Когда: POST. Тогда: Items содержит по одному элементу на O1 и O2.

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

  • Каталог/избранное/подборки не персонализируются — обогащение остаётся композицией в CRM-слое (ADR-0056 Часть D); публичные методы и карточки не меняются. Новые optional поля контекста — без breaking change.
  • Logs: lead-property-object-context read (LeadId, tenant, actor, число запрошенных id; PrimaryInvestorUserId — только id); деградации источников логируются отдельно (source=user-activity|favorites|selections). Metrics: crm_lead_property_object_context_total{result}, доля деградаций по источнику, latency p95; traces — сквозной trace через traceparent во все три internal-вызова; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

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

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

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

  • Сущность-дом лида: lead.md (PrimaryInvestorUserId, INV-LEAD-7, INV-CRM-001).
  • Делегированные источники контекста (internal m2m): D1 — get-property-object-views-by-user.md (просмотры инвестора по объектам), D2 — get-property-object-favorite-status.md (3-state избранного для агента и инвестора; Note не раскрывается — favorite.md), D3 — list-property-object-selections-by-user.md (подборки инвестора по объектам).
  • Паттерн делегированной авторизации по активному лиду: UC-ACT-004.
  • Смежная карточка лида: get-lead.md; permission — identity/domain/permission.md (crm.leads.view).
  • ADR-0056 (Часть D — каталог без персонализации, обогащение композицией; polling-свежесть просмотров), ADR-0025 (scope-иерархия), ADR-0071 (владение процессами, участники лида).