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

UC-ACT-003. Инвестор просматривает свою историю просмотров и действий

Назначение

Описывает пользовательский сценарий: авторизованный инвестор открывает раздел профиля /profile/events («история событий пользователя») и видит единую хронологическую ленту своего поведения на платформе PIN: просмотренные объекты недвижимости (с длительностью и источником открытия) и действия (применён фильтр, поиск, добавление/удаление избранного, добавление в подборку). Лента строится над append-only реестром ActionEvent: каждый просмотр карточки порождает ViewPropertyObjectActionEvent тем же согласованным изменением, что и запись PropertyObjectView, поэтому один реестр даёт и «просмотренные объекты недвижимости», и остальные действия; длительность/ViewSource дочитываются из связанного PropertyObjectView. Связан с контекстом user-activity (модуль actions).

Metadata

ПолеЗначение
IDUC-ACT-003
Контекстuser-activity (см. docs/06-services/user-activity/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольИнвестор (авторизованный UserEntity, JWT sub)
Permissions / scopenone — публичный self-scope (rate limit по актору/IP); чужая история по id инвестора — не этот UC (internal crm / permission user-activity.history.view-by-user, см. get-user-action-timeline.md)
TenantTenantId = null — инвестор вне tenant (ADR-0052 п.2)
Record-levelself-scope, обязателен: UserId == actor.UserId ИЛИ SessionId ∈ сессии actor-а (правило чтения истории «до и после входа» — property-object-view.md); предикат подставляет система из actor-контекста, не из тела запроса

Предусловие

  • Инвестор авторизован (access-JWT identity), актор даёт UserId (JWT sub); при отсутствии JWT и X-Anonymous-Id — сценарий не начинается (401, fail closed до БД).
  • Есть хотя бы одна запись ActionEvent/PropertyObjectView, принадлежащая актору (по UserId или связанной сессии); пустая история — валидный кейс (A1).
  • Сервис user-activity доступен (PostgreSQL up); display-данные объектов недвижимости (catalog) — опциональная зависимость фронта, на состав ленты не влияет.

Триггер

ПолеЗначение
ИсточникUI button / переход по навигации
Триггерящий элементСтраница профиля /profile/events; необязательные фильтры по дате и типу действия

Основной Поток

  1. Инвестор открывает /profile/events; фронт запрашивает первую страницу ленты.
  2. Фронт вызывает GET /api/user-activity/me/history?page=1&pageSize=20 (api-метод ../api/get-my-history.md) с access-JWT; заголовок X-Anonymous-Id прикладывается фронтом, но для авторизованного инвестора self-scope берётся по UserId.
  3. API валидирует JWT, наполняет actor-контекст (UserId, AnonymousId, SessionId).
  4. Обработка запроса (дом — get-my-history.md):
    • Проверка входа: Page >= 1, PageSize ∈ 1..100; FromDate <= ToDate, если оба заданы; каждый элемент ActionTypes — валидное значение ActionEventType (unknown → ValidationError).
    • Определение сессий: множество сессий инвестора (сессии с его UserId) — для до-входных гостевых записей (индекс ix_sessions_user_id).
    • Чтение страницы ленты ActionEvent с предикатом self-scope (UserId == actor.UserId ИЛИ SessionId ∈ сессии инвестора) + необязательные границы OccurredAt >= FromDate / OccurredAt <= ToDate + фильтр ActionTypes; сортировка OccurredAt desc, пагинация (индекс ix_action_events_user/ix_action_events_session).
    • Обогащение просмотров: для ViewPropertyObject-элементов дочитываются DurationSeconds/ViewSource из связанных PropertyObjectView; FilterSnapshot — typed jsonb AppliedFilterModel.
    • Результат: страница UserHistoryItemModel (Items, Page, PageSize, TotalCount, TotalPages) → HTTP 200. Операция read-only: состояние не меняется, события не публикуются.
  5. Фронт рендерит ленту: для ViewPropertyObject — объект недвижимости, длительность и ViewSource; для ApplyFilter — снимок фильтра (AppliedFilterModel) и ResultsCount; для SearchSearchQuery; для AddToFavorites/ RemoveFromFavorites/AddToSelectionPropertyObjectId + FavoriteId/SelectionId.
  6. Display-данные объектов недвижимости (адрес/цена/фото) фронт добирает батчем в catalog по списку PropertyObjectId страницы.
  7. Прокрутка/следующая страница → повтор шага 2 с page+1 (или FromDate/ToDate/ActionTypes при применении фильтров — A2).

Про ApplyFilter в ленте. Записи ApplyFilter — это устоявшиеся фильтры: на витрине каталога кнопки «Найти» нет, фронт трекает применение фильтра вторым запросом с trailing-дебаунсом 5000–7000 мс (не каждое промежуточное изменение слайдера/чекбокса, см. ../api/track-action.md). Поэтому лента показывает осмысленные наборы параметров, а не поток промежуточных состояний фильтра.

Альтернативные Потоки

A1. Пустая история

  1. Выборка возвращает TotalCount = 0 → тело Items = [] (не 404).
  2. Фронт показывает empty-state «Пока нет активности». Сценарий завершается успешно.

A2. Фильтр по дате и типу действия

  1. Инвестор задаёт диапазон дат и/или типы (напр. только ViewPropertyObject) → запрос с fromDate/toDate/ повторяемым types.
  2. Валидатор проверяет FromDate <= ToDate и допустимость каждого ActionEventType (unknown → 400, строго — чтобы не отдать всё молча); выборка добавляет границы OccurredAt и фильтр ActionTypes. Возврат к основному потоку на шаг 5.

A3. До-входные гостевые записи в ленте

  1. До входа инвестор действовал как гость (записи с UserId = null в позже связанной сессии).
  2. Множество сессий инвестора включает эти сессии → предикат SessionId ∈ сессии инвестора поднимает гостевые записи без backfill UserId (правило чтения истории «до и после входа», property-object-view.md); инвестор видит единую историю. Возврат к основному потоку.

A4. Просмотр ещё открыт (не закрыт worker-ом)

  1. В ленте есть ViewPropertyObject, чей PropertyObjectView.Status = ActiveDuration = null.
  2. DurationSeconds элемента ленты — null (длительность появится после закрытия worker-ом activity-closer, лаг polling-delta 10–15 мин, ADR-0056 п.3); фронт показывает просмотр без длительности. То же null — если запись PropertyObjectView вычищена retention (12 мес).

A5. Consumer-производное действие ещё не долетело (лаг favorites/selections)

  1. Инвестор только что добавил объект недвижимости в избранное / подборку — событие favorites/selections ещё не обработано консюмером user-activity (записи AddToFavorites*/AddToSelection реестра ActionEvent создаются по этим событиям, дедуп по SourceMessageId, INV-ACT-004).
  2. До обработки лента этих действий не содержит (eventual): read-only выборка отдаёт лишь уже записанные ActionEvent. После доставки события действие появляется в ленте при следующем запросе страницы (шаг 2). Возврат к основному потоку.

Постусловие

  • Состояние системы не изменилось (read-only: ни мутаций, ни доменных, ни интеграционных событий).
  • Инвестор увидел только собственные записи (record-level UserId == actor ИЛИ SessionId ∈ его сессии); чужие записи недоступны ни при каких параметрах запроса.
  • Приватные данные не выданы: Favorite.Note, Device.IpAddress/UserAgent, внутренние id сессий наружу не выходят; SearchQuery — Internal, но принадлежит самому актору.
  • Свежесть eventual: DurationSeconds появляется после закрытия просмотра worker-ом; consumer-действия (AddToFavorites*/AddToSelection) — после доставки событий favorites/selections.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
unauthenticated401Нет ни JWT, ни X-Anonymous-Id (актор не разрешён, fail closed до БД)refresh-flow identity; повтор запроса
VALIDATION_FAILED400pageSize вне 1..100, page < 1, FromDate > ToDate, неизвестный ActionTypesисправить параметры запроса
— (не ошибка)200Нет записей под фильтром (A1)тело Items = [], TotalCount = 0
USER_ACTIVITY_UNAVAILABLE503PostgreSQL недоступенretry с backoff; error-state страницы

События

Сценарий read-only: доменных и интеграционных событий не поднимает. Лента читает append-only реестр, наполняемый в других сценариях: ActionTrackedKafkaEvent (../events/action-tracked.md) при записи действий и PropertyObjectViewEndedKafkaEvent (../events/property-object-view-ended.md) при закрытии просмотра — здесь они лишь источник данных, а не события, поднимаемые этим UC.

Кросс-сервисные зависимости ленты

Лента — проекция append-only реестра ActionEvent/PropertyObjectView; сам UC ничего не публикует и синхронно ни к кому не ходит. Записи попадают в реестр разными путями, с разной свежестью:

Тип записиКак попадает в реестрСвежесть в ленте
ViewPropertyObjectвнутри контекста, тем же согласованным изменением, что создание PropertyObjectView (открытие карточки)факт — сразу; DurationSeconds — после закрытия worker-ом (10–15 мин, A4) или null после retention
ApplyFilter / Searchpublic track-API (устоявшийся фильтр — trailing-дебаунс 5000–7000 мс)сразу после записи
AddToFavorites / RemoveFromFavorites / AddToSelectionконсюмеры событий favorites/selections (async, дедуп по SourceMessageId)после доставки события (A5)

Display-данные объектов недвижимости (адрес/цена/фото) — опциональная батч-догрузка фронта в catalog по PropertyObjectId[] страницы; недоступность catalog на состав и порядок ленты не влияет (деградация только визуальная, история читается из user-activity).

Acceptance Criteria

#КритерийПроверка
AC1Инвестор видит только свои записи; запрос под другим UserId/AnonymousId не возвращает чужиетест изоляции record-level (обязателен, README «Безопасность»)
AC2Лента содержит и просмотры (ViewPropertyObject с DurationSeconds/ViewSource), и действия одним хронологическим списком (OccurredAt desc)контракт-тест ответа на смешанной фикстуре
AC3До-входные гостевые записи связанной сессии попадают в историю инвестора (A3)тест правила чтения (гость → link → history)
AC4Фильтры fromDate/toDate/types и пагинация работают; unknown ActionType → 400тест выборки и строгой валидации enum
AC5Открытый просмотр отдаётся с DurationSeconds = null, закрытый — с числом (A4)тест на Active/Inactive PropertyObjectView
AC6Пустая история → TotalCount = 0, Items = [], HTTP 200 (A1)тест пустого состояния
AC7Отсутствие актора → 401 до запроса БД (fail closed)тест без JWT/X-Anonymous-Id
AC8Favorite.Note/Device.*/внутренние id сессий не выдаются; SearchQuery в логи не пишетсяпроверка ответа и логов

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

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

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