UC-UAC-001. Агрегированная активность нескольких инвесторов-участников лида
Назначение
Описывает сценарий: у лида в CRM может быть несколько инвесторов-участников — основной инвестор и
со-инвесторы/созаёмщики (InvestorPrimary + сколько угодно InvestorSecondary). Агент (или менеджер ПИН)
открывает карточку лида и хочет видеть поведенческую активность всех инвесторов лида как единую картину:
какие объекты недвижимости смотрела «сторона покупателя» в совокупности, кто из со-инвесторов чем интересовался,
и сводную хронологию действий. Активность каждого инвестора живёт в user-activity и изолирована по владельцу
(record-level по UserId + его сессии), поэтому карточка собирается на стороне crm: CRM резолвит
множество инвесторов-участников лида, делает fan-out по m2m internal API user-activity (по одному вызову на
инвестора), затем агрегирует и атрибутирует результат по участникам. Связан с контекстом user-activity
(модули views/actions); потребитель и владелец авторизации лида — crm.
Префикс ID: в реестре контекст
user-activityиспользуетACT; данный документ заведён под явным IDUC-UAC-001и опирается на те же internal-контракты, что и одиночный UC-ACT-004 (активность одного инвестора).
Metadata
| Поле | Значение |
|---|---|
| ID | UC-UAC-001 |
| Контекст | user-activity (см. docs/06-services/user-activity/); чтение из crm (карточка лида) |
| Тип | основной (+ A1 менеджер ПИН, A2 нет активного лида, A3 инвесторы без активности, A4 пересечение интересов, A5 частичная недоступность, A6 участник добавлен/снят) |
| Статус | 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 напрямую |
| Tenant | user-activity вне tenant (ADR-0052 п.2) |
| Record-level | изоляция по каждому целевому UserId инвестора + его SessionId («Правило чтения истории», ../domain/property-object-view.md); данные инвестора вне состава лида недоступны |
Предусловие
- Агент авторизован в
crm; у него активный лид, в составе участников которого — инвесторы (иначе A2). Состав инвесторов — участники лида с ролямиInvestorPrimary/InvestorSecondary(../../crm/domain/lead.md,LeadParticipant). - Каждый инвестор идентифицирован по
UserId(identity); существованиеuser-activityсинхронно не проверяет (нет активности ⇒ пустой результат, не404). - Сервис
user-activityдоступен; при частичной недоступности — деградация карточки (A5).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI navigation (в crm) → m2m API fan-out (в user-activity) |
| Триггерящий элемент | Открытие карточки лида (вкладка «Активность»/«События») с несколькими инвесторами в составе |
Основной Поток
- Агент открывает карточку лида и вкладку активности.
- Backend
crmрезолвит множество целевых инвесторов лида (InvestorPrimary+ всеInvestorSecondary) и проверяет record-level авторизацию: лид активен и принадлежит агенту (проверка целиком на сторонеcrm;user-activityдоверяетuserIdиз маршрута в m2m-контексте). Инвесторы вне состава лида в множество не попадают. crmделает fan-out — для каждого инвестора параллельно вызывает два internal-endpoint-аuser-activity(по одному набору на инвестора):GET /users/{userId}/property-object-activity(../api/internal/get-user-property-object-activity.md) — агрегаты просмотров по объектам недвижимости per-инвестор;GET /users/{userId}/timeline(../api/internal/get-user-action-timeline.md) — лента действий per-инвестор.
user-activityна каждый вызов независимо: проверяет m2m scopeinternal+ permission; применяет «Правило чтения истории» (множество сессий инвестора по егоUserId, чтобы гостевые просмотры/действия до входа попали в историю); отдаёт агрегаты просмотров (ViewsCount/TotalDurationSeconds/AvgDurationSeconds/LastViewedAtper объект) и страницу действий; record-level гарантирован фильтром поUserId/SessionIdкаждого инвестора по отдельности; вне tenant. Каждая операция read-only; побочный эффект — read-sensitive access-audit (actor,targetUserId, без PII) на каждый вызов.crmагрегирует ответы по инвесторам в единую картину карточки лида:- объединяет активность в совокупный интерес «стороны покупателя» и одновременно сохраняет атрибуцию по участнику (какой инвестор смотрел/искал) — карточка показывает и сводку, и разбивку по со-инвесторам;
- по объектам недвижимости, которые смотрели несколько инвесторов (A4), сводит per-объект показатели
(например, суммарные просмотры стороны покупателя) с раскрытием вклада каждого инвестора; дедуп записей
ленты — по
ActionEventId(одна запись из pull и push-ленты не задваивается); - мёржит fan-out-выдачу со своей накопленной push-лентой (консюмер
../../crm/consumers/user-activity-actions.md событий
action-tracked/property-object-view-ended) — по каждому инвестору; - display-данные объектов недвижимости добирает батчем в
catalog.
- Операции
user-activityсостояние не меняют — доменных/интеграционных событий не поднимается. - Фронт
crmрендерит карточку лида: сводный блок «Активность инвесторов по объектам» (совокупно + по участникам) и общую хронологию действий с отметкой автора-инвестора — «что смотрела и искала сторона покупателя, и кто именно».
Альтернативные Потоки
A1. Менеджер ПИН — прямой доступ
- Актор —
Operator(Admin)сuser-activity.history.view-by-user; те же per-инвестор endpoints, платформенный scope (без проверки «активного лида»). CRM всё равно резолвит множество инвесторов лида как целевой набор. Read-sensitive audit фиксируетtargetUserIdкаждого инвестора. Возврат к шагу 4.
A2. У агента нет активного лида
crmне находит активный лид агента (record-level) и не вызываетuser-activityни по одному инвестору → отдаёт403в свой фронт. Авторизация — целиком на сторонеcrm(delegated).
A3. Инвесторы без активности
- По части или по всем инвесторам оба endpoint-а →
200сItems = [],TotalCount = 0(несуществующий/ безактивныйuserId— тоже пустой результат, не404). Карточка показывает частичный/полный empty-state «Активности пока нет». Сценарий завершается успешно.
A4. Пересечение интересов со-инвесторов
- Один и тот же объект недвижимости смотрели несколько инвесторов лида.
user-activityотдаёт per-инвестор агрегаты независимо; сведение per-объект (совокупные просмотры стороны покупателя + вклад каждого) делаетcrmна агрегации (шаг 5). Задвоения записей ленты нет — дедуп поActionEventId.
A5. Частичная недоступность user-activity
- Часть fan-out-вызовов вернула
503/500;crmрендерит карточку по успешно полученным инвесторам и своей накопленной push-ленте (graceful degradation), помечая, что данные части инвесторов временно недоступны. Полная недоступность — карточка без pull-блока (только push-лента). Возврат к полной картине при повторном открытии.
A6. Состав инвесторов лида изменился
- Инвестор добавлен/снят в составе лида (../../crm/domain/lead.md) → при следующем
открытии карточки
crmрезолвит актуальное множество инвесторов и делает fan-out по нему: активность добавленного включается, снятого — исключается из новой сборки (карточка отражает текущий состав лида, а не исторический). Историческая накопленная push-лента по снятому участнику в новой сводке не показывается.
Постусловие
- Состояние
user-activityне изменилось (read-only: ни мутаций, ни доменных, ни интеграционных событий). - Агент/менеджер увидел активность только инвесторов из состава лида (record-level по каждому
UserId+ его сессии); активность любого инвестора вне состава лида недоступна. - Свежесть eventual: закрытые просмотры видны после polling-цикла worker-а (10–15 мин, ADR-0056);
consumer-записи — после обработки событий; горизонт данных — retention 12 мес (старше —
dev-stats). - Зафиксирован read-sensitive access-audit на каждый вызов (actor +
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; агент без активного лида — отклоняет crm до вызовов (A2) | скрыть блок активности |
| — (не ошибка) | 200 | инвестор без активности / неизвестный userId (A3) | empty-state по этому инвестору |
USER_ACTIVITY_UNAVAILABLE | 503 | часть/все вызовы недоступны (A5) | карточка по доступным инвесторам + push-лента; 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, per-инвестор; дом контрактов — api-доки):
| Аспект | Гарантия |
|---|---|
| Гранулярность | эндпоинты — per single userId; агрегация по нескольким инвесторам лида выполняется crm fan-out-ом (нет мульти-user-эндпоинта — record-level изоляция сохраняется на каждом вызове) |
| Пустой результат | неизвестный/безактивный userId → 200, Items = [] (не 404, A3) |
| Изоляция | record-level по целевому UserId + его сессии на каждый вызов; активность вне запрошенного инвестора недоступна |
| Порядок / пагинация | timeline — OccurredAt desc; property-object-activity — LastViewedAt desc; стабильная страничная выборка |
| Полнота просмотров | в property-object-activity — только закрытые (Inactive) просмотры; открытые (Active) — с ViewDurationSeconds = null в timeline |
| Аудит | read-sensitive access-audit на каждый вызов (actor, targetUserId, без PII) |
user-activity ожидает от crm: валидный m2m service-token + permission на каждый вызов; доверенный
userId в маршруте — record-level «инвестор в составе активного лида агента» проверяет сам crm (delegated,
A2), user-activity его не перепроверяет и множество инвесторов не резолвит (это делает crm).
Fan-out + агрегация (на стороне crm). Единая картина лида собирается crm: N пар вызовов (по инвестору) +
merge с push-лентой (дедуп по ActionEventId) + сведение per-объект и атрибуция по участнику. При частичной
недоступности (A5) — деградация до доступных инвесторов и push-ленты без блокировки карточки.
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Для лида с несколькими инвесторами crm делает fan-out по каждому и собирает совокупную активность + разбивку по участникам | тест fan-out + агрегации (шаги 3–5) |
| AC2 | Каждый per-инвестор вызов изолирован record-level (UserId + сессии); активность инвестора вне состава лида недоступна ни при каких параметрах | тест изоляции на каждом вызове (обязателен) |
| AC3 | Объект, просмотренный несколькими со-инвесторами, сводится per-объект с раскрытием вклада каждого; записи ленты не задваиваются (дедуп ActionEventId) (A4) | тест пересечения интересов |
| AC4 | История каждого инвестора включает гостевые просмотры/действия до входа (через связанные сессии — «Правило чтения») | фикстура: гость → вход в той же сессии, на одном из инвесторов |
| AC5 | Агент без активного лида не получает данные — crm отдаёт 403 и не вызывает user-activity ни по одному инвестору (A2) | тест delegated-авторизации в crm |
| AC6 | Инвесторы без активности / неизвестные userId → 200, Items = []; карточка показывает частичный empty-state, не 404 (A3) | тест пустого состояния |
| AC7 | Частичная недоступность → карточка по доступным инвесторам + push-лента, без блокировки (A5) | тест graceful degradation |
| AC8 | Изменение состава инвесторов лида отражается при следующем открытии: добавленный включён, снятый исключён (A6) | тест актуального состава |
| AC9 | Read-sensitive access-audit записан на каждый инвесторский вызов; PII в ответах нет | проверка ответов и логов |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-CRM-019. Агент просматривает каталог в контексте инвестора активного лида
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы (дом контракта+алгоритма, per-инвестор): ../api/internal/get-user-property-object-activity.md (просмотры per-(user, объект)), ../api/internal/get-user-action-timeline.md (лента действий).
- Domain: ../domain/property-object-view.md («Правило чтения истории»),
../domain/action-event.md (лента,
AppliedFilterModel), ../domain/session.md (связывание сессий сUserId). - Push-двойник и накопление ленты: ../events/action-tracked.md; consumer в CRM — ../../crm/consumers/user-activity-actions.md.
- Потребитель/авторизация и состав инвесторов лида: ../../crm/README.md,
../../crm/domain/lead.md (
LeadParticipant,InvestorPrimary/InvestorSecondary). - Permissions: ../../identity/domain/permission.md
(
user-activity.activity.read-internal,user-activity.history.view-by-user). - Одиночный аналог: UC-ACT-004 (активность одного инвестора).
- Каталожное обогащение в контексте инвестора (delegated-композиция per-объект: просмотры/3-state избранное/ подборки инвестора оверлеем поверх платформенного каталога) — UC-CRM-019 и метод композиции get-property-object-investor-context.md.
- Решения: ADR-0052 (вне tenant), ADR-0054 (модули
views/actions), ADR-0056 (PostgreSQL-only, polling, retention), ADR-0057 (dev-stats).