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

GET /api/pin-manager/crm/leads — Сквозной список всех лидов

Назначение

Отдаёт Менеджеру ПИН постранично лиды всех агентств (по всем TenantId) с фильтрами по агентству, агенту и статусу: ФИО клиента, «шапка хотелок», статус, срок жизни, агент-владелец, счётчики фиксаций и сделок — для контроля воронки и помощи агентам (US-MGR-01). В отличие от агентского list-leads.md (скоуп одного TenantId), здесь выборка сквозная, кросс-tenant (ADR-0052 — кросс-tenant только Менеджеру ПИН и Службе заботы). Только чтение; дом агрегата — LeadEntity.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areapin-manager
Feature groupleads
Статусdraft
Документdocs/06-services/crm/api/pin-manager/list-leads.md

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

Доступ
ПроверкаЗначение
Actor typeOperator (Менеджер ПИН)
Auth policy / Permissionspermission crm.leads.view в platform-scope (роль «Менеджер ПИН»; identity/domain/permission.md)
Scope (RBAC)платформа — кросс-tenant (сквозной доступ ко всем агентствам, US-MGR)
Record-levelне применяется: Менеджер ПИН видит лиды любого агента; AgencyCompanyId/ExecutorAgentUserId — опциональные фильтры, не ограничение
Tenant isolationне применяется (кросс-tenant): выборка сквозная, без tenant-предиката; фильтр по агентству задаётся явным условием TenantId == AgencyCompanyId
Auditread-sensitive: сквозное чтение логируется (ActorUserId, набор фильтров, Total); PII клиентов (ФИО/хотелки) в логи не пишется

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/pin-manager/crm/leads
Success status200
Idempotency headerне применяется (read-only, безопасный метод)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаRequired; permission crm.leads.view @ platform-scopeлог platform-доступа
PagingqueryPagedQueryНетTake 1..200 (default 20), Offset >= 0 (default 0)
AgencyCompanyIdqueryGuid?Нет!= Guid.Empty если задан; фильтр по агентству (= LeadEntity.TenantId); несуществующий/пустой матч → пустая страница, без 404
ExecutorAgentUserIdqueryGuid?Нет!= Guid.Empty если задан; фильтр по текущему исполнителю лида
StatusqueryLeadStatus?Нетenum строкой Active/Expired/Archived; неизвестное → VALIDATION_FAILED
SourcequeryLeadSource?Нетenum строкой ConsultationRequest/BookingRequest/Referral/Manual; неизвестное → VALIDATION_FAILED
Фильтрация и сортировка

Фильтры комбинируются по AND (все — логические предикаты: отсутствие совпадений → пустая страница, не ошибка). Сортировка фиксирована LastEventAt desc, tiebreak CreatedAt descId 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 (в проекцию не кладётся).

Алгоритм

  1. Контекст и доступ. Операцию выполняет Менеджер ПИН с permission crm.leads.view в platform-scope; если платформенного права нет → CRM_LEADS_FORBIDDEN. TenantId из запроса не читается — операция кросс-tenant.
  2. Проверка входа и предусловий. Paging.Take ∈ [1..200], Offset >= 0; Status/Source — известные значения enum (unknown → VALIDATION_FAILED, а не «тихий» фильтр); AgencyCompanyId/ExecutorAgentUserId != Guid.Empty, если заданы. Существование сущностей по id не проверяется — это логические фильтры списка (несуществующий/чужой AgencyCompanyId/ExecutorAgentUserId → пустая страница, не 404). Scope/ принадлежность не применяются (platform-scope).
  3. Что читаем. Лиды по составленному предикату сквозной выборкой (без tenant-предиката; soft-deleted отсекаются). Опциональные фильтры (AND): TenantId == AgencyCompanyId, ExecutorAgentUserId, Status, Source. Для каждой строки — ClientName клиента и счётчики порождённых фиксаций (FixationsCount) и сделок (DealsCount). Порядок — LastEventAt descCreatedAt descId desc; постранично.
  4. Бизнес-правила и маппинг. Переходов состояний нет; record-level в предикат не добавляется (Менеджер ПИН видит все агентства). Маппинг поле-в-поле — item-model LeadListItemModel (дом list-leads.md); WishesSummary собирается из LeadWishes (бюджет + Urgency + PurchaseGoal). Агентство строки = LeadEntity.TenantId.
  5. Результат. Постраничный список LeadListItemModel + метаданные (Total, Take, Offset, HasMore) → HTTP 200. Пустой результат — 200 с пустым списком (не 404).

Диаграмма

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

ТипДетали
Внешние вызовыФИО клиента — реплика из identity в схеме crm; display-имена агентств/агентов резолвит фронт

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Take/Offset вне диапазона; неизвестный Status/Source; Guid.Empty в фильтре«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
CRM_LEADS_FORBIDDEN403нет 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 descCreatedAt descId desc; лиды без событий — в хвост (NULLS LAST).
  • Фильтр агентства. Когда: agencyCompanyId=A. Тогда: только лиды с TenantId == A.
  • Комбинация фильтров (AND). Когда: executorAgentUserId=G&status=Active&source=Referral. Тогда: пересечение всех условий.
  • Несуществующий/пустой матч. Когда: agencyCompanyId без лидов. Тогда: 200 c пустой страницей (не 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; dashboard pin-crm.

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

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

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

  • Сущность (дом полей, 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).