UC-ACT-004. Агент/менеджер видит активность инвестора (для карточки лида)
Назначение
Описывает сценарий: агент (или Менеджер ПИН) открывает карточку лида инвестора в CRM и видит его
поведенческую активность на платформе PIN — какие объекты недвижимости и как активно инвестор смотрел (сколько раз, сколько
суммарно/в среднем времени, когда в последний раз) и хронологию действий (применённые фильтры, поиск,
избранное, подборки). Это pull-режим: при открытии карточки лида CRM по m2m internal API дозагружает
историю из user-activity и мёржит её со своей накопленной push-лентой. Связан с контекстом
user-activity (модули views/actions); потребитель и владелец авторизации лида — crm.
| Поле | Значение |
|---|
| ID | UC-ACT-004 |
| Контекст | user-activity (см. docs/06-services/user-activity/); чтение из crm (карточка лида) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|
| Роль | Агент (через crm) / МенеджерПИН (прямой доступ, A1) |
| Permissions / scope | m2m crm — user-activity.activity.read-internal (просмотры) + user-activity.history.view-by-user (лента); авторизацию «активный лид агента на инвестора» делает crm (delegated). МенеджерПИН — user-activity.history.view-by-user напрямую (A1) |
| Tenant | user-activity вне tenant (ADR-0052 п.2) |
| Record-level | изоляция по целевому UserId инвестора + его SessionId («Правило чтения истории», ../domain/property-object-view.md); чужая активность недоступна |
Предусловие
- Агент авторизован в
crm; у него есть активный лид на инвестора (иначе A2). Лид хранит логическую
ссылку InvestorUserId (../../crm/domain/lead.md, ../../crm/domain/client.md).
- Инвестор идентифицирован по
UserId (identity); его существование user-activity синхронно не проверяет
(чужой контекст — нет активности ⇒ пустой результат, а не 404).
- Сервис
user-activity доступен (PostgreSQL up); иначе деградация карточки — A5.
Триггер
| Поле | Значение |
|---|
| Источник | UI navigation (в crm) → m2m API call (в user-activity) |
| Триггерящий элемент | Открытие карточки лида /crm/clients/[clientId] (вкладка «Активность»/«События») |
Основной Поток
- Агент открывает карточку лида инвестора и вкладку активности.
- Backend
crm резолвит целевой investorUserId из лида и проверяет record-level авторизацию: у агента
есть активный лид на этого инвестора (проверка — на стороне crm; user-activity доверяет userId
из маршрута в m2m-контексте).
crm (m2m, service-token) параллельно вызывает два internal-endpoint-а user-activity:
user-activity: проверяет m2m scope internal + permission
(user-activity.activity.read-internal / user-activity.history.view-by-user); каждая операция read-only
(дом — api-доки, здесь дельта):
- Правило чтения истории: определяет множество сессий инвестора (сессии с этим
UserId, индекс
ix_sessions_user_id) — так гостевые просмотры/действия до входа (в позже связанной сессии) попадают в
историю без backfill UserId.
property-object-activity: группирует Inactive-PropertyObjectView по
PropertyObjectId (для просмотров, где UserId == userId или SessionId ∈ сессии инвестора) →
{ PropertyObjectId, ViewsCount, TotalDurationSeconds, AvgDurationSeconds (derived), LastViewedAt },
сортировка по LastViewedAt desc. Open (Active) просмотры (Duration == null) — не входят.
timeline: страница ActionEvent (../domain/action-event.md) с предикатом
UserId∨SessionId + From/To + ActionTypes, сортировка OccurredAt desc; для ViewPropertyObject-
элементов дочитывается ViewDurationSeconds из связанного PropertyObjectView; FilterSnapshot — typed
AppliedFilterModel (ContractVersion = 1).
- Record-level гарантирован фильтром по
UserId/SessionId; вне tenant (ADR-0052).
- Операции read-only: состояние не меняется, доменных/интеграционных событий не поднимается. Побочный эффект —
read-sensitive access-audit (
actor, targetUserId, без PII).
user-activity возвращает страницы UserPropertyObjectActivityItem и UserActionTimelineItemModel →
HTTP 200 (пусто → Items = [], TotalCount = 0, не 404).
crm мёржит pull-выдачу со своей накопленной push-лентой (консюмер
../../crm/consumers/user-activity-actions.md событий
action-tracked/property-object-view-ended), дедуп по ActionEventId; display-данные объектов недвижимости (PropertyObjectId) добирает
батчем в catalog.
- Фронт
crm рендерит в карточке лида блок «Активность по объектам недвижимости» (сколько раз / сколько времени / когда
смотрел) и хронологию действий (фильтры/поиск/избранное/подборки) — «что смотрел и искал клиент».
Альтернативные Потоки
A1. Менеджер ПИН — прямой доступ (не через активный лид)
- Actor —
Operator(Admin) с permission user-activity.history.view-by-user; те же endpoints, платформенный
scope (без проверки «активного лида»). Read-sensitive audit фиксирует targetUserId.
- Возврат к основному потоку с шага 4.
A2. У агента нет активного лида на инвестора
crm не находит активный лид (record-level) и не вызывает user-activity → отдаёт 403 в свой фронт.
Авторизация — целиком на стороне crm (delegated); internal-endpoint не затрагивается.
A3. Пустая активность / инвестор без просмотров
- Оба endpoint-а →
200 с Items = [], TotalCount = 0 (несуществующий userId — тоже пустой результат,
не 404). Фронт crm показывает empty-state «Активности пока нет». Сценарий завершается успешно.
A4. Свежесть: незакрытые просмотры и лаг консюмеров
- Недавно открытые просмотры (
Active, Duration == null) в property-object-activity не входят; в timeline приходят
с ViewDurationSeconds = null — заполнятся после закрытия worker-ом activity-closer (10–15 мин, ADR-0056).
- Записи
AddToFavorites*/AddToSelection появляются после обработки событий favorites/selections
(eventual). Поток не меняется — карточка дозагрузится при обновлении.
A5. user-activity недоступен
- Endpoint-ы →
503/500; crm рендерит карточку лида без блока pull-активности (graceful degradation) или
только со своей накопленной push-лентой. Возврат к основному потоку при повторном открытии.
Постусловие
- Состояние
user-activity не изменилось (read-only: ни мутаций, ни доменных, ни интеграционных событий).
- Агент/менеджер увидел активность только целевого инвестора (record-level
UserId + его сессии); чужая
активность недоступна ни при каких параметрах.
- Свежесть eventual: закрытые просмотры видны после polling-цикла worker-а; consumer-записи — после
обработки событий; горизонт данных — retention 12 мес сырых записей (ADR-0056; старше —
dev-stats).
- Зафиксирован read-sensitive access-audit (actor service/operator +
targetUserId, без PII).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|
VALIDATION_FAILED | 400 | UserId = Guid.Empty, From > To, PageSize ∉ 1..100 | исправить параметры запроса crm |
UNAUTHENTICATED | 401 | нет валидного m2m service-token | обновить service-token; повтор |
USER_ACTIVITY_FORBIDDEN | 403 | нет permission (activity.read-internal/history.view-by-user); агент без активного лида — отклоняет crm до вызова (A2) | скрыть блок активности |
| — (не ошибка) | 200 | неизвестный UserId / нет активности (A3) | empty-state «Активности пока нет» |
USER_ACTIVITY_UNAVAILABLE | 503 | PostgreSQL недоступен (A5) | карточка без блока активности; retry |
События
Сценарий read-only: доменных и интеграционных событий не поднимает. CRM параллельно накапливает
push-ленту из событий, эмитируемых в других UC:
| Событие | Topic | Consumers |
|---|
ActionTrackedKafkaEvent (push-двойник, при записи действий) | pin.user-activity.action-tracked.v1 | crm (накопление ленты лида), news |
PropertyObjectViewEndedKafkaEvent (push-двойник, при закрытии просмотра) | pin.user-activity.property-object-view-ended.v1 | crm (лента лида), catalog, dev-stats |
Кросс-сервисные контракты (crm ↔ user-activity)
user-activity предоставляет (internal m2m, read-only; дом контрактов — api-доки):
| Аспект | Гарантия |
|---|
| Пустой результат | неизвестный / безактивный userId → 200, Items = [], TotalCount = 0 (не 404, A3) |
| Изоляция | record-level по целевому UserId + его сессии («Правило чтения»); чужие записи недоступны при любых параметрах запроса |
| Порядок / пагинация | timeline — OccurredAt desc; property-object-activity — LastViewedAt desc; стабильная страничная выборка (page/pageSize ∈ 1..100) |
| Полнота просмотров | в property-object-activity — только закрытые (Inactive) просмотры; Active (Duration == null) исключены; в timeline открытый просмотр отдаётся с ViewDurationSeconds = null (A4) |
| Горизонт данных | сырые записи — retention 12 мес (ADR-0056); глубже — многомерная аналитика dev-stats/ClickHouse (ADR-0057) |
| Аудит | read-sensitive access-audit (actor, targetUserId, без PII) на каждый internal-вызов |
user-activity ожидает от crm: валидный m2m service-token + permission
(activity.read-internal / history.view-by-user); доверенный userId в маршруте — record-level «активный
лид агента на инвестора» проверяет сам crm (delegated, A2), user-activity его не перепроверяет.
Pull + push (сборка на стороне crm). Карточка мёржит pull-выдачу этого UC с накопленной push-лентой
консюмера action-tracked/property-object-view-ended
(user-activity-actions.md); дедуп по ActionEventId — одна и
та же запись из обоих источников не задваивается. При недоступности user-activity (A5) — деградация до
push-ленты без блокировки карточки.
Acceptance Criteria
| # | Критерий | Проверка |
|---|
| AC1 | Агент видит по каждому объекту недвижимости ViewsCount/TotalDurationSeconds/AvgDurationSeconds/LastViewedAt | контракт-тест property-object-activity |
| AC2 | Хронология действий содержит ViewPropertyObject/ApplyFilter/Search/*Favorites/AddToSelection с OccurredAt desc, FilterSnapshot/SearchQuery — где применимо | контракт-тест timeline |
| AC3 | Возвращается активность только целевого инвестора; запрос по чужому userId не отдаёт данные другого | тест изоляции record-level (обязателен) |
| AC4 | История включает гостевые просмотры/действия до входа (через связанные сессии — «Правило чтения») | фикстура: гость → вход в той же сессии |
| AC5 | Агент без активного лида не получает данные — crm отдаёт 403 и не вызывает user-activity (A2) | тест delegated-авторизации в crm |
| AC6 | Незакрытые просмотры (Active) не входят в property-object-activity; в timeline ViewDurationSeconds = null (A4) | тест открытого просмотра |
| AC7 | Пустая активность / несуществующий userId → 200, Items = [] (не 404) (A3) | тест пустого состояния |
| AC8 | PII отсутствует в ответе; SearchQuery в логах усечён (Internal); access-audit targetUserId записан | проверка ответа и логов |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные Документы
- API методы (дом контракта+алгоритма): ../api/get-user-property-object-activity.md
(просмотры per-(user, property-object)), ../api/get-user-action-timeline.md
(лента действий).
- Domain entities: ../domain/property-object-view.md («Правило чтения истории»,
Duration),
../domain/action-event.md (TPH-лента, AppliedFilterModel),
../domain/session.md (связывание сессий с UserId),
../domain/property-object-view-stats.md (разграничение per-property-object vs per-(user, property-object)).
- Push-двойник: событие ../events/action-tracked.md
(
pin.user-activity.action-tracked.v1); consumer в CRM —
../../crm/consumers/user-activity-actions.md.
- Потребитель/авторизация лида: ../../crm/README.md,
../../crm/domain/lead.md, ../../crm/domain/client.md.
- Каталожное обогащение в контексте инвестора (тот же delegated-паттерн: агент видит просмотры/3-state
избранное/подборки инвестора оверлеем поверх платформенных карточек каталога) —
UC-CRM-019 и метод композиции
get-property-object-investor-context.md.
- Permissions: ../../identity/domain/permission.md
(
user-activity.activity.read-internal, user-activity.history.view-by-user).
- Обзор: ../README.md; поток «ACT → CRM» —
service-boundaries.md.
- Решения: ADR-0052 (вне tenant), ADR-0054 (модули
views/actions), ADR-0056 (PostgreSQL-only, polling,
retention), ADR-0057 (dev-stats).