GET /api/internal/user-activity/users/{userId}/property-object-activity — Активность инвестора по объектам недвижимости (карточка лида CRM)
Internal m2m-запрос для карточки лида CRM (B3): по каждому объекту недвижимости, который инвестор
открывал, отдаёт агрегаты «сколько раз / суммарно и в среднем сколько смотрел / когда смотрел последний
раз». Агрегация считается на чтении по просмотрам PropertyObjectView
с правилом чтения истории «UserId + сессии пользователя»
(property-object-view.md, «Правило чтения истории»). Это не PropertyObjectViewStats
(property-object-view-stats.md) — тот агрегат платформенный (per-property-object, все зрители), а
здесь срез per-(user, property-object).
Назначение
Дать агенту/менеджеру в карточке лида (/crm/clients/[clientId]) понимание интереса инвестора: какие объекты недвижимости
и как активно он смотрел. Owning-контекст — user-activity, модуль views, feature group views.
Вызывает crm по m2m internal API; record-level авторизацию (активный лид агента на этого инвестора)
делает CRM — этот метод доверяет userId из маршрута в m2m-контексте. Данные ограничены retention сырых
property_object_views (12 мес, ADR-0056).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | user-activity |
| API area | internal (m2m; вызывающий — crm) |
| Feature group | views |
| Статус | draft |
| Документ | docs/06-services/user-activity/api/internal/get-user-property-object-activity.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | ServiceAccount (m2m internal, вызывающий сервис crm) |
| Auth policy / Permissions | m2m internal (service token / gateway mTLS); scope internal; permission user-activity.activity.read-internal |
| Scope (RBAC) | не применяется (актор вне tenant; user-activity вне tenant, ADR-0052 п.2) |
| Record-level | изоляция по UserId из маршрута; проверку прав на этот UserId (активный лид инвестора у агента) выполняет вызывающий crm — delegated authorization |
| Tenant isolation | не применяется (вне tenant, ADR-0052 п.2; tenant-интерсепторы не подключаются) |
| Audit | read-sensitive: чтение чужой истории по id инвестора — access-лог actor(service) + targetUserId; PII в ответе нет |
Self-scope по актору здесь не применяется: userId приходит маршрутом (доверенный m2m-вызов), а не из
JWT actor-контекста, как в public self-scope методах.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/internal/user-activity/users/{userId}/property-object-activity |
| Success status | 200 (в т.ч. пустой список — активности нет / инвестор без просмотров) |
| Idempotency header | не применяется (GET — safe/idempotent по природе) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | route {userId} | Guid | Да | != Guid.Empty; существование в identity не проверяется (чужой контекст) | record-level по владельцу; авторизацию (активный лид) делает crm |
Paging | query | PagingQuery | Нет | Page >= 1; PageSize ∈ 1..100 (default 20) | — |
Сортировка выдачи — server-side OrderByMethod<UserPropertyObjectActivityItem>: LastViewedAt desc, тай-брейк
ViewsCount desc (недавно и активно смотренные объекты недвижимости — вверху). Клиентские сортировки/timestamp не
принимаются.
Модель ответа — страница UserPropertyObjectActivityItem
Ответ — страница (Items, TotalCount, Page, PageSize); TotalCount = число различных объектов
недвижимости в истории инвестора. Элемент:
UserPropertyObjectActivityItem
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
PropertyObjectId | Guid | Да | property_object_views.property_object_id (ключ группировки) | — | Объект недвижимости. Display-данные (адрес/цена/фото) добирает crm батчем в catalog; существование не гарантируется (INV-LV-006). |
ViewsCount | long | Да | count(*) по группе (только Inactive) | — | Сколько раз инвестор открывал карточку объекта недвижимости (закрытые просмотры). |
TotalDurationSeconds | long | Да | sum(duration) по группе, в секундах | — | Суммарная длительность просмотров объекта недвижимости инвестором. |
AvgDurationSeconds | long | Да | computed: ViewsCount == 0 ? 0 : TotalDurationSeconds / ViewsCount | — | Средняя длительность одного просмотра. Derived, не хранится (аналог INV-LVS-005). |
LastViewedAt | DateTimeOffset | Да | max(created_at) по группе | — | Момент последнего открытия объекта недвижимости инвестором. |
Открытые (Active) просмотры в агрегат не входят: их Duration ещё null (заполняется worker-ом при
закрытии). Это консистентно с PropertyObjectViewStats, который инкрементируется только по property-object-view-ended
(INV-LVS-002); лаг видимости — до цикла polling-worker-а (10–15 мин, ADR-0056).
Алгоритм
- Контекст и доступ. Вызывает
crm(m2m internal); требуется scopeinternalи permissionuser-activity.activity.read-internal. Запрос вне tenant (ADR-0052 п.2); record-level авторизацию (активный лид агента на этого инвестора) выполняет вызывающийcrm— здесьuserIdиз маршрута доверенный. - Проверка входа.
UserId != Guid.Empty;Page >= 1,PageSize ∈ 1..100— иначеVALIDATION_FAILED(400). Существование пользователя не проверяется (identity — чужой контекст): нет активности → пустая страница, не404. Целевого агрегата со статусом нет (read-only). - Что читаем.
- идентификаторы сессий инвестора по
UserId(правило чтения истории, property-object-view.md): так гостевые просмотры до входа, попавшие в позже связанную сессию, входят в историю без backfillUserId; - агрегация просмотров на чтении: по закрытым (
Inactive)PropertyObjectView, принадлежащим инвестору напрямую (UserId) или через его сессии (SessionId), с группировкой поPropertyObjectId; для каждой группы — число просмотров, суммарная и (вычисляемая) средняя длительность, последнее открытие; сортировкаLastViewedAt desc, тай-брейкViewsCount desc, пагинация.
- идентификаторы сессий инвестора по
- Расчёт и маппинг. Переходов состояний нет (read-only). Маппинг поле-в-поле — по таблице ответа;
AvgDurationSeconds = ViewsCount == 0 ? 0 : TotalDurationSeconds / ViewsCount(derived, не хранится — аналог INV-LVS-005). Открытые (Active) просмотры в агрегат не входят: ихDurationещёnull(заполняется worker-ом при закрытии), консистентно сPropertyObjectViewStats(INV-LVS-002). - Результат. Страница (
Items,TotalCount,Page,PageSize),TotalCount= число различных объектов недвижимости в истории инвестора →200. Нет активности / несуществующийuserId→200с пустым списком (не404). Freshness eventual: просмотры видны после закрытия worker-ом (лаг polling 10–15 мин, ADR-0056); горизонт данных — retention 12 мес.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (read-only query) |
| External calls | none (display-данные объектов недвижимости crm добирает батчем в catalog вне этого метода) |
| Cache / projections / search | опциональный read-cache (short TTL, ключ userId+paging, privacy-scope по userId) — по умолчанию выключен, агрегат считается query-time |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | UserId = Guid.Empty или paging вне границ | «Проверьте параметры запроса» | no retry |
UNAUTHORIZED | 401 | m2m без валидного service token | «Неавторизованный вызов» | no retry |
FORBIDDEN | 403 | актор без scope internal / permission user-activity.activity.read-internal | «Доступ запрещён» | no retry |
404 не возвращается: отсутствие активности — валидный пустой результат (200, Items = []).
Acceptance-критерии
- Given инвестор с закрытыми просмотрами по нескольким объектам, When
GET /users/{userId}/property-object-activity, Then200, страница агрегатов per-(user, объект), сортировкаLastViewedAt desc, тай-брейкViewsCount desc;TotalCount= число различных объектов. - Given просмотры, попавшие в позже связанную сессию (гостевые до входа), When
GET, Then они входят в историю по правилу «UserId+ сессии пользователя» без backfillUserId. - Given объект с только открытыми (
Active) просмотрами, WhenGET, Then он не попадает в агрегат (ихDurationещёnull) — виден после закрытия worker-ом (лаг polling-цикла), консистентно сPropertyObjectViewStats(INV-LVS-002). - Given группа объекта, When
GET, ThenAvgDurationSeconds = TotalDurationSeconds / ViewsCount(и0приViewsCount = 0), вычислено на чтении, не хранится. - Given неизвестный
userId, WhenGET, Then200,Items = [](не 404). - Given
pagingвне границ (page < 1илиpageSizeвне1..100), WhenGET, ThenVALIDATION_FAILED(400). - Given вызывающий без permission
user-activity.activity.read-internal, WhenGET, ThenFORBIDDEN(403); без service-токена —UNAUTHORIZED(401). - Given данные старше retention (12 мес), When
GET, Then они отсутствуют — долгосрочный горизонт живёт вdev-stats(ADR-0057).
Совместимость и наблюдаемость
- Новые optional-поля
UserPropertyObjectActivityItem— additive, без breaking change; удаление/переименование/смена типа поля — breaking (versioned internal-контракт + согласованный rollout сcrm). Unknown enum в ответе отсутствует (все поля скалярные). - Freshness eventual: закрытые просмотры видны после polling-цикла worker-а (ADR-0056); горизонт — retention
12 мес (старше живёт в
dev-stats, ADR-0057). - Logs:
user-activity internal property-object-activity read(actorservice,targetUserId, число объектов недвижимости в ответе — без PII); metrics:user_activity_internal_property_object_activity_requests_total{result}, latency p95 < 1 с (SLO internal-списков, README); traces — сквозной trace черезtraceparent; dashboardpin-user-activity; runbook —docs/06-services/user-activity/runbooks/(будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): GET /api/internal/user-activity/users/{userId}/property-objects/view-stats — Батч просмотров инвестора по объектам недвижимости (обогащение карточек CRM), GET /api/internal/user-activity/users/{userId}/timeline — Хронология действий пользователя (карточка лида CRM)
- Use Cases (4): UC-ACT-002. Трекинг длительности и числа просмотров объекта недвижимости, UC-ACT-003. Инвестор просматривает свою историю просмотров и действий, UC-ACT-004. Агент/менеджер видит активность инвестора (для карточки лида), UC-UAC-001. Агрегированная активность нескольких инвесторов-участников лида
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: property-object-view.md (дом полей, «Правило чтения истории»),
property-object-view-stats.md (разграничение per-property-object vs per-(user, property-object)),
session.md (связывание сессий с
UserId). - Соседние internal-методы B3:
get-user-action-timeline(лента действий лида),get-property-object-view-stats(PropertyObjectViewStatsдляcatalog) — ../README.md, раздел API. - Обзор: ../README.md; поток «ACT → CRM» — service-boundaries.md.
- Решения: ADR-0052 (вне tenant),
ADR-0054 (модуль
views), ADR-0056 (polling-worker, retention), ADR-0057 (dev-stats).