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

GET /api/agent/crm/leads/{leadId}/events — Лента событий лида

Назначение

Возвращает агенту ленту «что делает и чего хочет клиент» по конкретному лиду: просмотры объектов, применённые фильтры, избранное, подборки (добавлена/просмотрена), заявки на консультацию и бронирование, созданные из лида фиксации и сделки, продления срока жизни (UC-1..3). Экран — карточка лида ЛК агента. Лента наполняется асинхронно консюмерами user-activity, selections и командами заявок (см. таблицу типов ниже); этот метод её только читает страницами.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Агент или Руководитель агентства — JWT identity)
Auth policy / Permissionspermission crm.leads.view в scope агентства (identity/domain/permission.md)
Scope (RBAC)организация (компания-агентство) и ниже
Record-levelАгент — только свои лиды (активный участник, Lead.ExecutorAgentUserId = actor); Руководитель агентства — весь TenantId. Застройщик/инвестор в area agent не допускаются
Tenant isolationTenantId = AgencyCompanyId из identity claims (ADR-0052); crm.lead_events режется по TenantId автоматически
Auditnone (чтение собственной ленты; PII в payload ленты нет — см. lead.md)
Доступ

Рядовой агент видит только свои лиды (Lead.ExecutorAgentUserId = actor); руководитель агентства — все лиды своего TenantId. Застройщик/инвестор в area agent не допускаются. Нарушение record-level — 403 CRM_LEAD_FORBIDDEN.

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/agent/crm/leads/{leadId}/events
Success status200 (в т.ч. пустая лента — Items = [])
Idempotency headerне применяется (GET — safe/idempotent по природе)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired (= AgencyCompanyId)tenant isolation
LeadIdroute {leadId}GuidДа!= Guid.Empty; существование — шаг 2record-level по Lead.ExecutorAgentUserId
Typequerystring? (enum LeadEventType)НетLeadEventType (lead.md); null — все типы (фильтр категории ленты)
PagequeryintНет (default 1)>= 1
PageSizequeryintНет (default 50)1..200
Сортировка

Выдача сортируется server-side по OccurredAt desc (свежие события вверху); клиентские сортировки/курсоры по времени не принимаются.

Модель ответа — постраничный список LeadEventModel

Постраничный контейнер: Items, TotalCount, Page, PageSize; TotalCount = число событий ленты лида (с учётом фильтра Type).

Модель LeadEventModel

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаLeadEventEntity.IdИдентификатор события ленты.
Typestring (enum LeadEventType)ДаLeadEventEntity.TypeТип события (см. таблицу payload).
OccurredAtDateTimeOffsetДаLeadEventEntity.OccurredAtМомент события у источника (ключ сортировки).
PayloadLeadEventPayload (полиморфный по Type, ContractVersion = 1)ДаLeadEventEntity.Payload (typed jsonb)Типизированные данные события; конкретный подтип — по Type.

LeadEventPayloadстрого типизированный полиморфный контракт (по дискриминатору Type), не сырой object/json. Дом каждого подтипа и его полей — lead.md (раздел «Enum LeadEventType и typed payload»); здесь — только соответствие Type → подтип и источник наполнения:

TypePayload-подтип (дом — lead.md)Кто пишет в ленту
PropertyObjectViewedLeadPropertyObjectViewedPayloadconsumer user-activity-actions.md
FilterAppliedLeadFilterAppliedPayloadconsumer user-activity-actions.md
FavoriteAddedLeadFavoriteAddedPayloadconsumer user-activity-actions.md
SelectionAttachedLeadSelectionPayloadconsumer selection-shared.md
SelectionViewedLeadSelectionPayloadconsumer selection-shared.md
ConsultationRequestedLeadConsultationRequestedPayloadкоманда заявки на консультацию/презентацию
BookingRequestedLeadBookingRequestedPayloadкоманда create-booking-request.md
FixationCreatedLeadFixationRefPayloadкоманда создания фиксации из лида
DealCreatedLeadDealRefPayloadкоманда создания сделки из лида
LeadExtendedLeadExtendedPayloadкоманда продления / авто-продление по активности

Алгоритм

  1. Контекст и доступ. Операцию выполняет агент/руководитель агентства (permission crm.leads.view) в рамках TenantId.
  2. Проверка входа и предусловий.
    • формат: LeadId != Guid.Empty, Type ∈ LeadEventType (при заданном), Page >= 1, PageSize ∈ 1..200 — иначе ошибка валидации;
    • лид существует в TenantId (отсутствие/чужой tenant) — иначе CRM_LEAD_NOT_FOUND (404);
    • состояние: лента читается для лида в любом Status (Active/Expired/Archived): история доступна и по закрытому лиду;
    • record-level: рядовой агент — только lead.ExecutorAgentUserId = actor, иначе CRM_LEAD_FORBIDDEN (403); Руководитель агентства проходит по TenantId.
  3. Что читаем. Страницу ленты crm.lead_events по LeadId (+опциональный Type), сортировка по OccurredAt desc (покрывающий индекс ix_lead_events_lead_id_occurred_at).
  4. Маппинг. Поле-в-поле — Id/Type/OccurredAt; Payload — типизированный полиморфный подтип LeadEventPayload по дискриминатору Type (ContractVersion = 1). Внутренние SourceMessageId/TenantId/ CreatedAt наружу не проецируются.
  5. Результат. Постраничный список → HTTP 200. Пустая лента → 200 с Items = [], TotalCount = 0 (не 404). Freshness — eventual: события появляются после обработки консюмерами/командами (лаг очереди/outbox, ADR-0056).

Диаграмма

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

ТипДетали
Интеграционные событияnone (read-only query)
Внешние вызовыnone (display-обвязку объектов/подборок ЛК добирает отдельными запросами по id из payload)
Проекции / поискnone (лента читается query-time; source of truth — crm.lead_events)
Уведомленияnone

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400LeadId = Guid.Empty, неизвестный Type, paging вне границ«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
FORBIDDEN403нет permission crm.leads.view в scope«Доступ запрещён»no retry
CRM_LEAD_NOT_FOUND404лид не существует или вне TenantId актора«Лид не найден»no retry
CRM_LEAD_FORBIDDEN403рядовой агент читает чужой лид (ExecutorAgentUserId != actor)«Доступ к лиду запрещён»no retry

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

  • Новые типы LeadEventType и additive-поля payload (ContractVersion = 1, дом — lead.md) расширяют ленту без breaking change; клиент обязан игнорировать неизвестный Type/подтип Payload (forward-compatible). Удаление/переименование поля модели или подтипа — breaking (migration + rollout).
  • Logs: lead-events-listed (tenantId, leadId, type?, count — без PII); metrics: crm_agent_operations_total{operation="list-lead-events"}, latency p95 < 1 c (SLO списков, README); traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy path. Given агент-владелец и существующий лид; When GET; Then 200 — страница ленты, сортировка OccurredAt desc, полиморфный Payload по дискриминатору Type.
  • Фильтр типа. Given заданный Type; When GET; Then только события этого типа; TotalCount учитывает фильтр.
  • Чужой лид. Given рядовой агент и чужой лид (ExecutorAgentUserId != actor); When GET; Then 403 CRM_LEAD_FORBIDDEN; руководитель агентства по TenantId проходит.
  • Не найден. Given несуществующий или чужой по TenantId лид; When GET; Then 404 CRM_LEAD_NOT_FOUND.
  • Пустая лента. Given лид без событий; When GET; Then 200, Items = [], TotalCount = 0 (не 404).
  • Eventual-наполнение. Given только что произошедшая активность инвестора (просмотр/фильтр/избранное); When GET сразу; Then событие может ещё отсутствовать — лента наполняется после обработки консюмером (user-activity/selections) или командой, в пределах лага очереди/outbox.
  • Закрытый лид. Given лид в статусе Expired/Archived; When GET; Then 200 — история доступна.
  • Валидация. Given неизвестный Type или paging вне границ; When GET; Then 400 VALIDATION_FAILED.

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

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

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