UC-ACT-003. Инвестор просматривает свою историю просмотров и действий
Назначение
Описывает пользовательский сценарий: авторизованный инвестор открывает раздел профиля /profile/events
(«история событий пользователя») и видит единую хронологическую ленту своего поведения на платформе PIN:
просмотренные объекты недвижимости (с длительностью и источником открытия) и действия (применён фильтр,
поиск, добавление/удаление избранного, добавление в подборку). Лента строится над append-only реестром
ActionEvent: каждый просмотр карточки порождает ViewPropertyObjectActionEvent
тем же согласованным изменением, что и запись PropertyObjectView, поэтому
один реестр даёт и «просмотренные объекты недвижимости», и остальные действия; длительность/ViewSource
дочитываются из связанного PropertyObjectView. Связан с контекстом user-activity (модуль actions).
Metadata
| Поле | Значение |
|---|---|
| ID | UC-ACT-003 |
| Контекст | user-activity (см. docs/06-services/user-activity/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор (авторизованный UserEntity, JWT sub) |
| Permissions / scope | none — публичный self-scope (rate limit по актору/IP); чужая история по id инвестора — не этот UC (internal crm / permission user-activity.history.view-by-user, см. get-user-action-timeline.md) |
| Tenant | TenantId = null — инвестор вне tenant (ADR-0052 п.2) |
| Record-level | self-scope, обязателен: UserId == actor.UserId ИЛИ SessionId ∈ сессии actor-а (правило чтения истории «до и после входа» — property-object-view.md); предикат подставляет система из actor-контекста, не из тела запроса |
Предусловие
- Инвестор авторизован (access-JWT identity), актор даёт
UserId(JWTsub); при отсутствии JWT иX-Anonymous-Id— сценарий не начинается (401, fail closed до БД). - Есть хотя бы одна запись
ActionEvent/PropertyObjectView, принадлежащая актору (поUserIdили связанной сессии); пустая история — валидный кейс (A1). - Сервис
user-activityдоступен (PostgreSQL up); display-данные объектов недвижимости (catalog) — опциональная зависимость фронта, на состав ленты не влияет.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button / переход по навигации |
| Триггерящий элемент | Страница профиля /profile/events; необязательные фильтры по дате и типу действия |
Основной Поток
- Инвестор открывает
/profile/events; фронт запрашивает первую страницу ленты. - Фронт вызывает
GET /api/user-activity/me/history?page=1&pageSize=20(api-метод ../api/get-my-history.md) с access-JWT; заголовокX-Anonymous-Idприкладывается фронтом, но для авторизованного инвестора self-scope берётся поUserId. - API валидирует JWT, наполняет actor-контекст (
UserId,AnonymousId,SessionId). - Обработка запроса (дом — 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 jsonbAppliedFilterModel. - Результат: страница
UserHistoryItemModel(Items,Page,PageSize,TotalCount,TotalPages) → HTTP200. Операция read-only: состояние не меняется, события не публикуются.
- Проверка входа:
- Фронт рендерит ленту: для
ViewPropertyObject— объект недвижимости, длительность иViewSource; дляApplyFilter— снимок фильтра (AppliedFilterModel) иResultsCount; дляSearch—SearchQuery; дляAddToFavorites/RemoveFromFavorites/AddToSelection—PropertyObjectId+FavoriteId/SelectionId. - Display-данные объектов недвижимости (адрес/цена/фото) фронт добирает батчем в
catalogпо спискуPropertyObjectIdстраницы. - Прокрутка/следующая страница → повтор шага 2 с
page+1(илиFromDate/ToDate/ActionTypesпри применении фильтров — A2).
Про
ApplyFilterв ленте. ЗаписиApplyFilter— это устоявшиеся фильтры: на витрине каталога кнопки «Найти» нет, фронт трекает применение фильтра вторым запросом с trailing-дебаунсом 5000–7000 мс (не каждое промежуточное изменение слайдера/чекбокса, см. ../api/track-action.md). Поэтому лента показывает осмысленные наборы параметров, а не поток промежуточных состояний фильтра.
Альтернативные Потоки
A1. Пустая история
- Выборка возвращает
TotalCount = 0→ телоItems = [](не404). - Фронт показывает empty-state «Пока нет активности». Сценарий завершается успешно.
A2. Фильтр по дате и типу действия
- Инвестор задаёт диапазон дат и/или типы (напр. только
ViewPropertyObject) → запрос сfromDate/toDate/ повторяемымtypes. - Валидатор проверяет
FromDate <= ToDateи допустимость каждогоActionEventType(unknown → 400, строго — чтобы не отдать всё молча); выборка добавляет границыOccurredAtи фильтрActionTypes. Возврат к основному потоку на шаг 5.
A3. До-входные гостевые записи в ленте
- До входа инвестор действовал как гость (записи с
UserId = nullв позже связанной сессии). - Множество сессий инвестора включает эти сессии → предикат
SessionId ∈ сессии инвестораподнимает гостевые записи без backfillUserId(правило чтения истории «до и после входа», property-object-view.md); инвестор видит единую историю. Возврат к основному потоку.
A4. Просмотр ещё открыт (не закрыт worker-ом)
- В ленте есть
ViewPropertyObject, чейPropertyObjectView.Status = Active→Duration = null. DurationSecondsэлемента ленты —null(длительность появится после закрытия worker-омactivity-closer, лаг polling-delta 10–15 мин, ADR-0056 п.3); фронт показывает просмотр без длительности. То жеnull— если записьPropertyObjectViewвычищена retention (12 мес).
A5. Consumer-производное действие ещё не долетело (лаг favorites/selections)
- Инвестор только что добавил объект недвижимости в избранное / подборку — событие
favorites/selectionsещё не обработано консюмеромuser-activity(записиAddToFavorites*/AddToSelectionреестраActionEventсоздаются по этим событиям, дедуп поSourceMessageId, INV-ACT-004). - До обработки лента этих действий не содержит (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 | Условие | Поведение клиента |
|---|---|---|---|
unauthenticated | 401 | Нет ни JWT, ни X-Anonymous-Id (актор не разрешён, fail closed до БД) | refresh-flow identity; повтор запроса |
VALIDATION_FAILED | 400 | pageSize вне 1..100, page < 1, FromDate > ToDate, неизвестный ActionTypes | исправить параметры запроса |
| — (не ошибка) | 200 | Нет записей под фильтром (A1) | тело Items = [], TotalCount = 0 |
USER_ACTIVITY_UNAVAILABLE | 503 | PostgreSQL недоступен | 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 / Search | public 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 |
| AC8 | Favorite.Note/Device.*/внутренние id сессий не выдаются; SearchQuery в логи не пишется | проверка ответа и логов |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-ACT-001. Трекинг сессии гостя и пользователя
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API метод (дом контракта + алгоритм): ../api/get-my-history.md.
- Domain entity: ../domain/action-event.md (реестр
ActionEvent, TPH-подтипы, домAppliedFilterModel), ../domain/property-object-view.md («Правило чтения истории»,Duration/ViewSource), ../domain/session.md (session-scope истории «до/после входа»). - Kafka-контракты (источник данных ленты): ../events/action-tracked.md, ../events/property-object-view-ended.md.
- Чужая история (не этот UC): ../api/get-user-action-timeline.md,
../api/get-user-property-object-activity.md (internal
crm, карточка лида). - Контекст: ../README.md; прототип —
prototype-mapping.md (
/profile/events). - Решения: ADR-0052 (инвестор/гость вне tenant), ADR-0054 (модуль
actions), ADR-0056 (PostgreSQL-only, без Redis, polling-закрытие просмотров).