GET /api/agent/crm/leads — Список лидов агента
Список лидов агентской воронки для страницы «Лиды» ЛК агента (US-AGT). Read-only проекция агрегата
LeadEntity с сортировкой по последней активности клиента (LastEventAt desc, индекс
ix_leads_last_event_at), фильтрами и record-level разграничением «свои / всё агентство» (переключатель
руководителя). Мутаций и событий не производит.
Назначение
Отдать агенту постранично его активные/истёкшие/архивные лиды (ФИО клиента, краткая «шапка хотелок», статус, срок жизни, счётчики порождённых фиксаций и сделок) для навигации по воронке.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent |
| Feature group | leads |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/list-leads.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Агент или Руководитель агентства) |
| Auth policy / Permissions | crm.leads.view |
| Scope (RBAC) | организация (агентство) — TenantId = AgencyCompanyId |
| Record-level | Агент — только свои (ExecutorAgentUserId = actor); Руководитель — весь TenantId, опциональный фильтр по ExecutorAgentUserId |
| Tenant isolation | TenantId в каждом запросе, авто-предикат по scope |
| Audit | read (не sensitive): PII клиентов в логи не пишется |
Агент видит только свои лиды (ExecutorAgentUserId = actor); руководитель агентства — весь TenantId,
опционально сузить фильтром по ExecutorAgentUserId. Попытка агента заглянуть за пределы своего исполнения
(ExecutorAgentUserId != actor) отклоняется, а не молча сужается до пустого списка.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/agent/crm/leads |
| Success status | 200 |
| Idempotency header | не применяется (read-only, безопасный метод) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required (AgencyCompanyId) | Tenant isolation |
Paging | query | PagedQuery | Нет | Take 1..200 (default 20), Offset >= 0 (default 0) | — |
Status | query | LeadStatus? | Нет | enum строкой Active/Expired/Archived; неизвестное → ValidationError | — |
ClientId | query | Guid? | Нет | != Guid.Empty; несуществующий/чужой → пустой список (scope+filter), без 404 | — |
Source | query | LeadSource? | Нет | enum строкой ConsultationRequest/BookingRequest/Referral/Manual | — |
ExecutorAgentUserId | query | Guid? | Нет | только для Руководителя; для Агента задан и != actor → CRM_LEADS_SCOPE_FORBIDDEN | — |
Фиксированная сортировка LastEventAt desc (индекс ix_leads_last_event_at), tiebreak CreatedAt desc →
Id desc; лиды без событий (LastEventAt = null) идут в хвост (NULLS LAST).
Модель ответа — постраничный список LeadListItemModel
Постраничный контейнер: Items: IReadOnlyList<LeadListItemModel> + Meta (Total, Take, Offset,
HasMore). Поля элемента — проекция LeadEntity и ClientEntity:
LeadListItemModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | LeadEntity.Id | — | Идентификатор лида. |
ClientId | Guid | Да | LeadEntity.ClientId | — | Карточка клиента (навигация). |
ExecutorAgentUserId | Guid | Да | LeadEntity.ExecutorAgentUserId | — | Текущий исполнитель лида (для колонки «Агент» в режиме руководителя). |
ClientName | string | Да | ClientEntity.Name (FullName) | PII: не в логи | ФИО клиента одной строкой. |
Status | LeadStatus | Да | LeadEntity.Status (enum строкой) | — | Active/Expired/Archived. |
Source | LeadSource | Да | LeadEntity.Source (enum строкой) | — | Как появился лид. |
WishesSummary | string? | Нет | computed из LeadEntity.Wishes (LeadWishes) | PII: не в логи | Краткая «шапка»: бюджет-диапазон + Urgency + PurchaseGoal; null, если хотелки пусты. |
ExpiresAt | DateTimeOffset | Да | LeadEntity.ExpiresAt | — | Срок жизни лида. |
LastEventAt | DateTimeOffset? | Нет | LeadEntity.LastEventAt | — | Последнее событие ленты (ключ сортировки). |
FixationsCount | int | Да | число заявок на фиксацию по LeadId | — | Порождено заявок на фиксацию. |
DealsCount | int | Да | число сделок по LeadId | — | Порождено сделок. |
WishesSummary — производная строка (типизированная), не сырой jsonb; полный VO — в доме
lead.md.
Алгоритм
- Контекст и доступ. Операцию выполняет агент/руководитель агентства (permission
crm.leads.view; иначеCRM_LEADS_FORBIDDEN) в tenant своего агентства (TenantId = AgencyCompanyId). - Проверка входа и предусловий.
- формат:
Paging.Take ∈ 1..200,Offset >= 0;Status/Source— известные значения enum (unknown → ошибка валидации);ClientId != Guid.Emptyпри заданном; - несуществующий/чужой
ClientIdдаёт пустую страницу (фильтр, не 404); - scope: если actor не Руководитель и
ExecutorAgentUserId != actor→CRM_LEADS_SCOPE_FORBIDDEN.
- формат:
- Что читаем. Страницу лидов с проекцией строк: связь
Lead → Client(ClientName), счётчики порождённых фиксаций/сделок поLeadId; фильтрыStatus/ClientId/Source, record-level (см. шаг 4),TenantIdиз scope,deleted_at IS NULL; сортировкаLastEventAt desc → CreatedAt desc(индексix_leads_last_event_at). - Правила scope и маппинг. Record-level: Агент →
ExecutorAgentUserId == actor; Руководитель без фильтра → весьTenantId; Руководитель сExecutorAgentUserId→ExecutorAgentUserId == query.ExecutorAgentUserId. Маппинг поле-в-поле — таблица «Модель ответа»;WishesSummaryсобирается изLeadWishes(бюджет +Urgency+PurchaseGoal). - Результат. Постраничный список (
Items,Meta = {Total, Take, Offset}) → HTTP200. Пустой результат —200сItems = [](не 404).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | none (read-only) |
| Внешние вызовы | none (данные — своя схема crm; ФИО — реплика из identity, консюмер user-upserted) |
| Проекции / поиск | none (прямой запрос PostgreSQL по ix_leads_last_event_at) |
| Уведомления | none |
Ошибки и failure modes
Нарушение record-level по ExecutorAgentUserId — не пустая страница, а явный отказ 403 CRM_LEADS_SCOPE_FORBIDDEN. Пустой список (нет 404) возможен только как исход фильтрации: несуществующий/
чужой ClientId, отсутствие подходящих лидов.
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | Take вне 1..200, неизвестный Status/Source, ClientId = Guid.Empty | «Проверьте параметры запроса» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CRM_LEADS_FORBIDDEN | 403 | нет permission crm.leads.view | «Недостаточно прав» | no retry |
CRM_LEADS_SCOPE_FORBIDDEN | 403 | Агент запросил ExecutorAgentUserId != actor (чужие лиды / всё агентство) | «Недостаточно прав для просмотра лидов другого агента» | no retry |
Совместимость и наблюдаемость
- Новые optional поля ответа/фильтры — additive (без breaking); удаление/переименование/смена типа поля или
значения enum — breaking (migration + rollout). Unknown enum во входе —
ValidationError(не игнор). - Logs:
leads listed(TenantId,ExecutorAgentUserId, счётчикTotal, набор фильтров; ФИО/хотелки — не в логи); metrics:crm_leads_list_total{result}, latency p95 (< 1 c, SLO списков); traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy path. Given агент без фильтров; When
GET; Then200— свои лиды, сортировкаLastEventAt desc(лиды без событий — в хвост,NULLS LAST);WishesSummaryзаполнен при непустых хотелках. - Фильтр по агенту. Given руководитель агентства с фильтром
ExecutorAgentUserId; WhenGET; Then200— лиды указанного агента; без фильтра — весьTenantId. - Scope запрещён. Given агент задал
ExecutorAgentUserId != actor; WhenGET; Then403 CRM_LEADS_SCOPE_FORBIDDEN. - Фильтры. Given
Status/Source/ClientId; WhenGET; Then выдача сужена; несуществующий/чужойClientIdдаёт пустую страницу (не404). - Нет прав. Given отсутствует permission
crm.leads.view; WhenGET; Then403 CRM_LEADS_FORBIDDEN. - Валидация. Given неизвестный
Status/SourceилиTakeвне1..200; WhenGET; Then400 VALIDATION_FAILED. - Пусто. Given под фильтр не попал ни один лид; When
GET; Then200сItems = [](не404).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): GET /api/agent/crm/statistics — Статистика агентства/агента, GET /api/developer/crm/deals — Сделки застройщика (список/поиск), GET /api/investor/crm/leads — Мои лиды (инвестор), GET /api/pin-manager/crm/leads — Сквозной список всех лидов
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: lead.md (дом полей,
LeadWishes,LeadStatus/LeadSource,ix_leads_last_event_at, INV-CRM-001), client.md (FullName, PII). - События жизненного цикла: lead-created.md, lead-expired.md; соседний метод воронки: create-booking-request.md.
- Контекст: README; tenancy — ADR-0052.