GET /api/pin-manager/crm/leads — Сквозной список всех лидов
Назначение
Отдаёт Менеджеру ПИН постранично лиды всех агентств (по всем TenantId) с фильтрами по агентству, агенту
и статусу: ФИО клиента, «шапка хотелок», статус, срок жизни, агент-владелец, счётчики фиксаций и сделок — для
контроля воронки и помощи агентам (US-MGR-01). В отличие от агентского list-leads.md
(скоуп одного TenantId), здесь выборка сквозная, кросс-tenant (ADR-0052 — кросс-tenant только Менеджеру ПИН
и Службе заботы). Только чтение; дом агрегата — LeadEntity.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | pin-manager |
| Feature group | leads |
| Статус | draft |
| Документ | docs/06-services/crm/api/pin-manager/list-leads.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | Operator (Менеджер ПИН) |
| Auth policy / Permissions | permission crm.leads.view в platform-scope (роль «Менеджер ПИН»; identity/domain/permission.md) |
| Scope (RBAC) | платформа — кросс-tenant (сквозной доступ ко всем агентствам, US-MGR) |
| Record-level | не применяется: Менеджер ПИН видит лиды любого агента; AgencyCompanyId/ExecutorAgentUserId — опциональные фильтры, не ограничение |
| Tenant isolation | не применяется (кросс-tenant): выборка сквозная, без tenant-предиката; фильтр по агентству задаётся явным условием TenantId == AgencyCompanyId |
| Audit | read-sensitive: сквозное чтение логируется (ActorUserId, набор фильтров, Total); PII клиентов (ФИО/хотелки) в логи не пишется |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/pin-manager/crm/leads |
| Success status | 200 |
| Idempotency header | не применяется (read-only, безопасный метод) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims | Guid | Да | Required; permission crm.leads.view @ platform-scope | лог platform-доступа |
Paging | query | PagedQuery | Нет | Take 1..200 (default 20), Offset >= 0 (default 0) | — |
AgencyCompanyId | query | Guid? | Нет | != Guid.Empty если задан; фильтр по агентству (= LeadEntity.TenantId); несуществующий/пустой матч → пустая страница, без 404 | — |
ExecutorAgentUserId | query | Guid? | Нет | != Guid.Empty если задан; фильтр по текущему исполнителю лида | — |
Status | query | LeadStatus? | Нет | enum строкой Active/Expired/Archived; неизвестное → VALIDATION_FAILED | — |
Source | query | LeadSource? | Нет | enum строкой ConsultationRequest/BookingRequest/Referral/Manual; неизвестное → VALIDATION_FAILED | — |
Фильтры комбинируются по AND (все — логические предикаты: отсутствие совпадений → пустая страница, не
ошибка). Сортировка фиксирована LastEventAt desc, tiebreak CreatedAt desc → Id desc; лиды без событий
(LastEventAt = null) идут в хвост (NULLS LAST). Отдельный SortQuery не вводится — порядок задан
назначением списка.
Модель ответа
Постраничный список: элементы LeadListItemModel + метаданные страницы (Total, Take, Offset,
HasMore). Строка — тот же item-model LeadListItemModel, чей дом — агентский
list-leads.md (поля Id, ClientId, ExecutorAgentUserId, ClientName, Status,
Source, WishesSummary, ExpiresAt, LastEventAt, FixationsCount, DealsCount; агент строки —
ExecutorAgentUserId). Сквозной режим форму строки не меняет. Дельта pin-manager:
- проекция заполняется кросс-tenant (по всем
TenantId), а не в рамках одного агентства; - агентство строки — это
LeadEntity.TenantId(=AgencyCompanyId), по которому Менеджер ПИН группирует/ фильтрует список; display-имя агентства и текущего исполнителя (ExecutorAgentUserId) резолвит фронт черезidentity(в проекцию не кладётся).
Алгоритм
- Контекст и доступ. Операцию выполняет Менеджер ПИН с permission
crm.leads.viewв platform-scope; если платформенного права нет →CRM_LEADS_FORBIDDEN.TenantIdиз запроса не читается — операция кросс-tenant. - Проверка входа и предусловий.
Paging.Take ∈ [1..200],Offset >= 0;Status/Source— известные значения enum (unknown →VALIDATION_FAILED, а не «тихий» фильтр);AgencyCompanyId/ExecutorAgentUserId != Guid.Empty, если заданы. Существование сущностей по id не проверяется — это логические фильтры списка (несуществующий/чужойAgencyCompanyId/ExecutorAgentUserId→ пустая страница, не 404). Scope/ принадлежность не применяются (platform-scope). - Что читаем. Лиды по составленному предикату сквозной выборкой (без tenant-предиката; soft-deleted
отсекаются). Опциональные фильтры (AND):
TenantId == AgencyCompanyId,ExecutorAgentUserId,Status,Source. Для каждой строки —ClientNameклиента и счётчики порождённых фиксаций (FixationsCount) и сделок (DealsCount). Порядок —LastEventAt desc→CreatedAt desc→Id desc; постранично. - Бизнес-правила и маппинг. Переходов состояний нет; record-level в предикат не добавляется (Менеджер
ПИН видит все агентства). Маппинг поле-в-поле — item-model
LeadListItemModel(дом list-leads.md);WishesSummaryсобирается изLeadWishes(бюджет +Urgency+PurchaseGoal). Агентство строки =LeadEntity.TenantId. - Результат. Постраничный список
LeadListItemModel+ метаданные (Total,Take,Offset,HasMore) → HTTP200. Пустой результат —200с пустым списком (не 404).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Внешние вызовы | ФИО клиента — реплика из identity в схеме crm; display-имена агентств/агентов резолвит фронт |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | Take/Offset вне диапазона; неизвестный Status/Source; Guid.Empty в фильтре | «Проверьте параметры запроса» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CRM_LEADS_FORBIDDEN | 403 | нет permission crm.leads.view в platform-scope (актор — не Менеджер ПИН) | «Недостаточно прав для сквозного просмотра лидов» | no retry |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор — Менеджер ПИН с permission
crm.leads.viewв platform-scope; кросс-tenant,TenantIdв запрос не читается; FixationsCount/DealsCount— вычисляемые кросс-tenant счётчики порождённых сущностей, согласованы на момент запроса.
Постусловия (после запроса):
- состояние не изменяется (read-only); сквозное чтение логируется (
ActorUserId, набор фильтров,Total); PII клиентов (ФИО/хотелки) в логи не пишется; - возвращена сквозная страница лидов всех агентств; агентство строки =
LeadEntity.TenantId(=AgencyCompanyId).
Кросс-сервисные ожидания (eventual-согласованность). ФИО клиента — реплика из identity в схеме crm; display-имена агентств/агентов резолвит фронт отдельным запросом (в проекцию не кладутся).
Acceptance Criteria
- Happy path. Дано: лиды нескольких агентств. Когда:
GET …/leadsбез фильтров. Тогда:200, сквозная страница, порядокLastEventAt desc→CreatedAt desc→Id desc; лиды без событий — в хвост (NULLS LAST). - Фильтр агентства. Когда:
agencyCompanyId=A. Тогда: только лиды сTenantId == A. - Комбинация фильтров (AND). Когда:
executorAgentUserId=G&status=Active&source=Referral. Тогда: пересечение всех условий. - Несуществующий/пустой матч. Когда:
agencyCompanyIdбез лидов. Тогда:200c пустой страницей (не 404). - Неизвестный enum. Когда:
status=Fooилиsource=Bar. Тогда:400 VALIDATION_FAILED. - Нет platform-scope-права. Когда: актор — не Менеджер ПИН. Тогда:
403 CRM_LEADS_FORBIDDEN.
Совместимость и наблюдаемость
- Новые optional фильтры/поля ответа — additive (без breaking); удаление/переименование/смена типа поля или
значения enum
LeadStatus/LeadSource— breaking (migration + rollout). Unknown enum во входе —VALIDATION_FAILED(не игнор). - Logs:
all-leads listed(ActorUserId, набор фильтров,Total; ФИО/хотелки — не в логи); metrics:crm_pin_manager_leads_list_total{result}, latency p95 < 1 c (SLO списков); traces — сквозной trace черезtraceparent; dashboardpin-crm.
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей,
LeadWishes,LeadStatus/LeadSource, индексix_leads_last_event_at, record-level/ platform-scope для Менеджера ПИН): lead.md; ФИО/PII клиента — client.md. - Дом item-model
LeadListItemModelи агентская проекция того же списка: list-leads.md; соседняя кросс-проекция: list-fixations.md; карта областей — README. - События жизненного цикла (этим методом не публикуются): lead-created.md, lead-expired.md.
- ADR: 0052 tenancy (кросс-tenant только platform-scope).