GET /api/investor/crm/leads/{leadId} — Карточка лида (инвестор)
Карточка своего лида для инвестора (страница «Мои лиды» ЛК): какой агент ведёт, статус связи, срок жизни
лида и список порождённых сделок. Read-only проекция агрегата LeadEntity (lead.md);
здесь описана только форма InvestorLeadCardModel и алгоритм её сборки. Это урезанный вид агентской
карточки (../agent/get-lead.md): инвестору не показываются заявки на фиксацию и
внутренняя аналитика агентства — «шапка хотелок» (LeadWishes), карточка клиента агентства (PII/Client*),
счётчики продлений и лента событий лида (о лиде не знает застройщик; фиксации инвестору недоступны — README,
раздел «Безопасность»).
Назначение
Отдать инвестору сводку одного своего лида по leadId: ответственный агент (ExecutorAgentUserId — display-данные
клиент резолвит в identity), текущий Status, происхождение (Source), срок жизни (ExpiresAt) и
компактный список сделок, порождённых из лида, — для перехода в «Мои сделки».
| Поле | Значение |
|---|
| Сервис/контекст | crm |
| API area | investor |
| Feature group | leads |
| Статус | draft |
| Документ | docs/06-services/crm/api/investor/get-lead.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Инвестор — только свои лиды) |
| Auth policy / Permissions | crm.leads.view (JWT инвестора); фактический доступ ограничен record-level self |
| Scope (RBAC) | самообслуживание: инвестор видит только записи с PrimaryInvestorUserId = actor (не organization-scope) |
| Record-level | LeadEntity.PrimaryInvestorUserId = ActorUserId; иначе FORBIDDEN (403) — чужой лид не раскрывается |
| Tenant isolation | Запрос не tenanted (Lead.TenantId = AgencyCompanyId ≠ актор): чтение инвесторской стороны по явному record-level предикату PrimaryInvestorUserId (ADR-0052); удалённые записи скрыты глобальным фильтром |
| Audit | read (данные принадлежат самому инвестору); телефоны/PII в логи не пишутся |
Инвестор видит только свои лиды: PrimaryInvestorUserId = ActorUserId. Несовпадение — явный FORBIDDEN
(403), без маскировки под «не найдено» (в отличие от агентской карточки). Запрос не tenant-scoped:
доступ определяется record-level предикатом PrimaryInvestorUserId, а не TenantId (ADR-0052).
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/investor/crm/leads/{leadId} |
| Success status | 200 |
| Idempotency header | не применяется (безопасный, семантически идемпотентный GET) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
LeadId | route | Guid | Да | != Guid.Empty; существующий лид | — |
ActorUserId | identity claims | Guid | Да | JWT инвестора | Record-level (PrimaryInvestorUserId = actor) |
Модель ответа InvestorLeadCardModel
Проекция LeadEntity (дом полей — lead.md); ниже — только форма инвесторской
карточки. Коллекция Deals живёт внутри контейнера InvestorLeadCardModel — ограниченный компактный
ряд (у лида немного сделок); глубокий постраничный просмотр — отдельный инвесторский список сделок
list-deals. Фиксации, Wishes, карточка клиента агентства и лента событий в модель не входят.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
LeadId | Guid | Да | Lead.Id | — | Идентификатор лида. |
Status | LeadStatus (enum→string) | Да | Lead.Status | — | Active/Expired/Archived (lead.md). |
Source | LeadSource (enum→string) | Да | Lead.Source | — | Как появился лид (заявка/реферал/…); иммутабельно. |
ExecutorAgentUserId | Guid | Да | Lead.ExecutorAgentUserId | — | Ответственный агент; display-данные (ФИО/контакты) клиент резолвит в identity. |
ExpiresAt | DateTimeOffset | Да | Lead.ExpiresAt | — | До какого момента лид активен (срок жизни). |
ExpiredAt | DateTimeOffset? | Нет | Lead.ExpiredAt | — | Факт перевода в Expired worker-ом; null, если не истёк. |
CreatedAt | DateTimeOffset | Да | Lead.CreatedAt | — | Когда лид появился. |
Deals | IReadOnlyList<InvestorLeadDealRefModel> | Да | crm.deals (LeadId) | — | Порождённые сделки (может быть пустым). |
InvestorLeadDealRefModel — компактный ряд сделки
Проекция DealEntity (дом полей, VO Money, enum DealStatus — deal.md);
инвесторская дельта — без агентских/застройщических внутренних полей.
| Поле | Тип | Обязательность | Источник | Описание |
|---|
DealId | Guid | Да | Deal.Id | Сделка (переход в «Мои сделки»). |
Title | string | Да | Deal.Title | Заголовок карточки (объект недвижимости/ЖК). |
Status | DealStatus (enum→string) | Да | Deal.Status | Application/Booking/Sale/Completed/Cancelled (US-INV-03). |
PropertyObjectId | Guid | Да | Deal.PropertyObjectId | Объект; display-данные — API chessboard. |
Price | Money (owned VO) | Да | Deal.Price | Стоимость объекта недвижимости (US-INV-05; контракт VO — deal.md). |
UpdatedAt | DateTimeOffset | Да | Deal.UpdatedAt | Последнее обновление сделки (US-INV-04). |
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор с permission
crm.leads.view; ActorUserId — из
токена.
- Проверка входа и предусловий.
LeadId != Guid.Empty, иначе VALIDATION_FAILED (400). Существование
и record-level — при загрузке.
- Что читаем. Лид по
LeadId (scope-bypass developer/agency-tenant, ADR-0052). Если лида нет или он
удалён → CRM_LEAD_NOT_FOUND. Порождённые сделки — проекцией по LeadId = LeadId && PrimaryInvestorUserId = ActorUserId, порядок UpdatedAt desc. Фиксации, Wishes, лента событий и клиентская карточка агентства
не загружаются.
- Бизнес-правила и маппинг. Record-level:
lead.PrimaryInvestorUserId == ActorUserId; иначе FORBIDDEN (403)
— чужой лид не раскрывается. Состояние: карточка доступна в любом Status (Active/Expired/Archived).
Маппинг: lead → InvestorLeadCardModel (поля выше, enum-ы строкой), Deals — из проекции шага 3.
Переходов состояний нет.
- Результат.
InvestorLeadCardModel → HTTP 200. Допустима микро-рассинхронизация лида и списка сделок
(карточка — сводка, не финансовая консистентность); свежесть — на момент запроса. Лид не найден/удалён →
CRM_LEAD_NOT_FOUND (404); чужой лид → FORBIDDEN (403).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none (read-only GET) |
| External calls | none (объект недвижимости/проект в Deal* — снапшоты в crm; display агента — отдельный запрос клиента к identity) |
| Cache / projections / search | none (прямое чтение агрегата и проекции сделок) |
| Notifications | none |
| Отдельные запросы | глубокий просмотр сделок — инвесторский list-deals; лента событий инвестору не отдаётся. |
Ошибки и failure modes
Чужой лид не маскируется под 404: PrimaryInvestorUserId != actor возвращает явный 403 FORBIDDEN.
Отсутствующий или удалённый лид — отдельный код 404 CRM_LEAD_NOT_FOUND.
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | LeadId = Guid.Empty | «Некорректный идентификатор лида» | no retry |
FORBIDDEN | 403 | лид существует, но PrimaryInvestorUserId != actor (чужой лид) | «Нет доступа к лиду» | no retry |
CRM_LEAD_NOT_FOUND | 404 | лида с таким Id нет / удалён | «Лид не найден» | no retry |
Acceptance Criteria
- Given свой лид, When инвестор запрашивает карточку, Then возвращается
InvestorLeadCardModel (Status/Source/ExecutorAgentUserId/ExpiresAt + компактный Deals), ответ 200; фиксации, Wishes, клиентская карточка агентства и лента событий в модель не входят.
- Given
Status ∈ {Active, Expired, Archived}, When запрос, Then карточка читается; ExpiredAt заполнен только для лида, переведённого worker-ом в Expired.
- Given лид существует, но
PrimaryInvestorUserId != actor, When запрос, Then FORBIDDEN (403) — чужой лид не раскрывается.
- Given лида с таким
Id нет или он удалён, When запрос, Then CRM_LEAD_NOT_FOUND (404); пустой LeadId → VALIDATION_FAILED (400).
- Given у лида ещё нет порождённых сделок, When запрос, Then
Deals = [].
- Given лид и его сделки читаются как сводка, When запрос, Then допустима микрорассинхронизация лида и списка сделок (не финансовая консистентность); список сделок упорядочен
UpdatedAt desc и ограничен PrimaryInvestorUserId = actor.
Совместимость и наблюдаемость
- Новые optional поля
InvestorLeadCardModel/InvestorLeadDealRefModel — additive (без breaking change);
удаление/переименование/смена типа поля или enum — breaking (migration + rollout клиентов). Enum-поля
(LeadStatus/LeadSource/DealStatus) сериализуются строкой; неизвестное значение клиент обязан
толерантно трактовать как «прочее», не падать.
- Logs:
investor-lead-card requested (LeadId, ActorUserId, result; PII не пишется); metrics:
crm_investor_lead_card_requests_total{result}, latency p95 (< 500 мс, SLO чтений README); traces —
OTel, correlation через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/
(будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы