GET /api/agent/crm/leads/{leadId}/events — Лента событий лида
Назначение
Возвращает агенту ленту «что делает и чего хочет клиент» по конкретному лиду: просмотры объектов, применённые
фильтры, избранное, подборки (добавлена/просмотрена), заявки на консультацию и бронирование, созданные из лида
фиксации и сделки, продления срока жизни (UC-1..3). Экран — карточка лида ЛК агента. Лента наполняется
асинхронно консюмерами user-activity, selections и командами заявок (см. таблицу типов ниже); этот метод
её только читает страницами.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent |
| Feature group | leads |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/list-lead-events.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Агент или Руководитель агентства — JWT identity) |
| Auth policy / Permissions | permission crm.leads.view в scope агентства (identity/domain/permission.md) |
| Scope (RBAC) | организация (компания-агентство) и ниже |
| Record-level | Агент — только свои лиды (активный участник, Lead.ExecutorAgentUserId = actor); Руководитель агентства — весь TenantId. Застройщик/инвестор в area agent не допускаются |
| Tenant isolation | TenantId = AgencyCompanyId из identity claims (ADR-0052); crm.lead_events режется по TenantId автоматически |
| Audit | none (чтение собственной ленты; PII в payload ленты нет — см. lead.md) |
Рядовой агент видит только свои лиды (Lead.ExecutorAgentUserId = actor); руководитель агентства — все лиды
своего TenantId. Застройщик/инвестор в area agent не допускаются. Нарушение record-level — 403 CRM_LEAD_FORBIDDEN.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/agent/crm/leads/{leadId}/events |
| Success status | 200 (в т.ч. пустая лента — Items = []) |
| Idempotency header | не применяется (GET — safe/idempotent по природе) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required (= AgencyCompanyId) | tenant isolation |
LeadId | route {leadId} | Guid | Да | != Guid.Empty; существование — шаг 2 | record-level по Lead.ExecutorAgentUserId |
Type | query | string? (enum LeadEventType) | Нет | ∈ LeadEventType (lead.md); null — все типы (фильтр категории ленты) | — |
Page | query | int | Нет (default 1) | >= 1 | — |
PageSize | query | int | Нет (default 50) | 1..200 | — |
Выдача сортируется server-side по OccurredAt desc (свежие события вверху); клиентские сортировки/курсоры по
времени не принимаются.
Модель ответа — постраничный список LeadEventModel
Постраничный контейнер: Items, TotalCount, Page, PageSize; TotalCount = число событий ленты лида (с
учётом фильтра Type).
Модель LeadEventModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | LeadEventEntity.Id | — | Идентификатор события ленты. |
Type | string (enum LeadEventType) | Да | LeadEventEntity.Type | — | Тип события (см. таблицу payload). |
OccurredAt | DateTimeOffset | Да | LeadEventEntity.OccurredAt | — | Момент события у источника (ключ сортировки). |
Payload | LeadEventPayload (полиморфный по Type, ContractVersion = 1) | Да | LeadEventEntity.Payload (typed jsonb) | — | Типизированные данные события; конкретный подтип — по Type. |
LeadEventPayload — строго типизированный полиморфный контракт (по дискриминатору Type), не сырой
object/json. Дом каждого подтипа и его полей — lead.md (раздел «Enum LeadEventType
и typed payload»); здесь — только соответствие Type → подтип и источник наполнения:
Type | Payload-подтип (дом — lead.md) | Кто пишет в ленту |
|---|---|---|
PropertyObjectViewed | LeadPropertyObjectViewedPayload | consumer user-activity-actions.md |
FilterApplied | LeadFilterAppliedPayload | consumer user-activity-actions.md |
FavoriteAdded | LeadFavoriteAddedPayload | consumer user-activity-actions.md |
SelectionAttached | LeadSelectionPayload | consumer selection-shared.md |
SelectionViewed | LeadSelectionPayload | consumer selection-shared.md |
ConsultationRequested | LeadConsultationRequestedPayload | команда заявки на консультацию/презентацию |
BookingRequested | LeadBookingRequestedPayload | команда create-booking-request.md |
FixationCreated | LeadFixationRefPayload | команда создания фиксации из лида |
DealCreated | LeadDealRefPayload | команда создания сделки из лида |
LeadExtended | LeadExtendedPayload | команда продления / авто-продление по активности |
Алгоритм
- Контекст и доступ. Операцию выполняет агент/руководитель агентства (permission
crm.leads.view) в рамкахTenantId. - Проверка входа и предусловий.
- формат:
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.
- формат:
- Что читаем. Страницу ленты
crm.lead_eventsпоLeadId(+опциональныйType), сортировка поOccurredAtdesc (покрывающий индексix_lead_events_lead_id_occurred_at). - Маппинг. Поле-в-поле —
Id/Type/OccurredAt;Payload— типизированный полиморфный подтипLeadEventPayloadпо дискриминаторуType(ContractVersion = 1). ВнутренниеSourceMessageId/TenantId/CreatedAtнаружу не проецируются. - Результат. Постраничный список → 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_FAILED | 400 | LeadId = Guid.Empty, неизвестный Type, paging вне границ | «Проверьте параметры запроса» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
FORBIDDEN | 403 | нет permission crm.leads.view в scope | «Доступ запрещён» | no retry |
CRM_LEAD_NOT_FOUND | 404 | лид не существует или вне TenantId актора | «Лид не найден» | no retry |
CRM_LEAD_FORBIDDEN | 403 | рядовой агент читает чужой лид (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; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy path. Given агент-владелец и существующий лид; When
GET; Then200— страница ленты, сортировкаOccurredAt desc, полиморфныйPayloadпо дискриминаторуType. - Фильтр типа. Given заданный
Type; WhenGET; Then только события этого типа;TotalCountучитывает фильтр. - Чужой лид. Given рядовой агент и чужой лид (
ExecutorAgentUserId != actor); WhenGET; Then403 CRM_LEAD_FORBIDDEN; руководитель агентства поTenantIdпроходит. - Не найден. Given несуществующий или чужой по
TenantIdлид; WhenGET; Then404 CRM_LEAD_NOT_FOUND. - Пустая лента. Given лид без событий; When
GET; Then200,Items = [],TotalCount = 0(не404). - Eventual-наполнение. Given только что произошедшая активность инвестора (просмотр/фильтр/избранное); When
GETсразу; Then событие может ещё отсутствовать — лента наполняется после обработки консюмером (user-activity/selections) или командой, в пределах лага очереди/outbox. - Закрытый лид. Given лид в статусе
Expired/Archived; WhenGET; Then200— история доступна. - Валидация. Given неизвестный
Typeили paging вне границ; WhenGET; Then400 VALIDATION_FAILED.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): GET /api/agent/crm/leads/{leadId} — Карточка лида, GET /api/pin-manager/crm/leads/{leadId} — Карточка любого лида (Менеджер ПИН)
- События (1): Event Contract: pin.selections.selection-shared (SelectionSharedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Дом ленты (все поля/типы/индексы): domain/lead.md (
LeadEventEntity,LeadEventType,LeadPropertyObjectViewedPayload/LeadSelectionPayload/…,ix_lead_events_lead_id_occurred_at); родитель — domain/client.md. - Наполнители ленты: консюмеры user-activity-actions.md,
selection-shared.md, referral-registered.md;
команды заявок — create-booking-request.md (
BookingRequested). - Карта области agent и событий: ../../README.md.
- Решения: ADR-0052 (tenant = агентство), ADR-0056 (eventual-наполнение ленты).