Event Contract: pin.user-activity.property-object-view-ended (PropertyObjectViewEndedKafkaEvent)
Версионируемый контракт интеграционного события «Просмотр объекта недвижимости завершён». Consumer-документы
ссылаются сюда, не переописывая payload. Дом полей — entity: PropertyObjectView
(раздел «Доменные и интеграционные события»); здесь перечислено только то, что реально в событии.
Назначение
Фиксирует закрытие одного просмотра карточки объекта недвижимости с измеренной длительностью. Producer — контекст
user-activity: polling-worker activity-closer переводит PropertyObjectView Active → Inactive по порогу
неактивности (delta now() − LastActivityAt) и атомарно с этим публикует событие (ADR-0056 п.3, INV-LV-004).
Длительность просмотра — ключевая метрика (ADR-0056 п.2): событие делает её доступной downstream без
синхронного запроса в user-activity. Публичное, потому что на нём строятся: собственные агрегаты
популярности объекта недвижимости, застройщицкая аналитика (dev-stats) и лента карточки лида (crm).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.user-activity.property-object-view-ended |
| Topic | pin.user-activity.property-object-view-ended.v1 |
| Partitions / key | 6 (ADR-0056: 1–10k активных) / key = PropertyObjectViewId (= AggregateId) |
| ContractVersion | 1 |
| Producer | user-activity — worker activity-closer (закрытие просмотра по неактивности; форс-закрытие при закрытии/смене родительской сессии) |
| Статус | draft |
Событие публикуется атомарно с закрытием агрегата. Метаданные события несут MessageId, CreatedAt,
Version (= ContractVersion), AggregateId = PropertyObjectViewId (= Kafka key),
AggregateType = "PropertyObjectView".
Payload PropertyObjectViewEndedKafkaEvent
Snapshot закрытого просмотра. Дом полей — PropertyObjectView; перечислено только то, что
реально в событии. PII нет — только идентификаторы, длительность и снимок фильтра.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
PropertyObjectViewId | Guid | Да | PropertyObjectView.Id | Immutable | Ключ агрегата (= Kafka key). Бизнес-ключ идемпотентности consumer-ов. |
PropertyObjectId | Guid | Да | PropertyObjectView.PropertyObjectId | Immutable | Просмотренный объект недвижимости (логическая ссылка chessboard/catalog, без FK). |
SessionId | Guid | Да | PropertyObjectView.SessionId | Immutable | Сессия, в которой открыт объект недвижимости. |
AnonymousId | Guid | Да | PropertyObjectView.AnonymousId | Immutable | Устройство-владелец просмотра. |
UserId | Guid? | Нет | Session.UserId (актуальный на момент закрытия) | Immutable | Пользователь — актуальный из связанной сессии; null — гостевой просмотр (до входа). |
ViewSource | string (enum ViewSource) | Да | PropertyObjectView.ViewSource | Additive only (новые значения) | Откуда открыта карточка: Catalog / Search / Selection / Favorites / News / DirectLink / Unknown. |
ViewedAt | DateTimeOffset | Да | PropertyObjectView.CreatedAt | Immutable | Момент открытия карточки (начало просмотра). |
LastActivityAt | DateTimeOffset | Да | PropertyObjectView.LastActivityAt | Immutable | Последний heartbeat «карточка открыта» (конец измеренной активности; вход greatest для PropertyObjectViewStats.LastViewedAt). |
DurationSeconds | int | Да | PropertyObjectView.Duration (= LastActivityAt − CreatedAt, целые секунды) | Immutable | Длительность просмотра. >= 0 (INV-LV: chk_property_object_views_duration_nonneg). |
FilterSnapshot | AppliedFilterModel? | Нет | PropertyObjectView.FilterSnapshot | Additive only | Иммутабельный снимок фильтра, по которому найден объект недвижимости (null — просмотр без фильтра). Дом модели ниже. |
Display-атрибуты объекта недвижимости (проект, застройщик, город, цена) в payload не включаются: consumers добирают их
батчем из catalog по PropertyObjectId — синхронного вызова в catalog на каждый просмотр нет (INV-LV-006). В
событии — только идентификаторы и снимок фильтра. Device.IpAddress/UserAgent родительской сессии (квази-PII)
в событие не попадают (правило session.md).
Вложенная модель AppliedFilterModel (ContractVersion = 1)
Дом модели — action-event.md (раздел «AppliedFilterModel
(jsonb, ContractVersion = 1)»); здесь — только дельта, без переописывания таблицы полей. Typed versioned
jsonb: все поля optional (город/застройщик/проект/цена/комнаты/площадь/статусы/сортировка), минимум одно
заполнено (INV-ACT-006); id-поля — логические ссылки на справочники chessboard/pin.handbook (мягкая
валидация, см. дом модели), *Name-поля —
денормализованные display-значения на момент применения (карточка лида читается без lookup-ов). Совместимость
модели — additive-only в рамках ContractVersion = 1.
Когда публикуется
Единственный publisher — worker activity-closer (ADR-0056 п.3); публикация атомарна с закрытием записи.
| Условие | Переход состояния | Согласованность |
|---|---|---|
now() − PropertyObjectView.LastActivityAt > InactivityThreshold (10–15 мин, конфиг); worker выбирает Active-просмотры под блокировкой с пропуском занятых (INV-LV-004) | PropertyObjectView Active → Inactive: Status → Inactive, EndedAt = now(), Duration = LastActivityAt − CreatedAt | закрытие агрегата + событие в одном согласованном изменении |
Закрытие/форс-закрытие родительской Session (INV-LV-007, INV-SES-002/005): все её Active-просмотры закрываются тем же согласованным изменением | Active → Inactive (та же вычислительная логика Duration) | то же |
Inactive-запись иммутабельна (INV-LV-008) — событие по одному PropertyObjectViewId публикуется ровно один раз;
повторная доставка обрабатывается идемпотентностью consumer-ов.
Потребители (consumers)
Сверено с property-object-view.md («Доменные и интеграционные события») и property-object-view-stats.md.
| Consumer-контекст | Consumer | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
user-activity | PropertyObjectViewEndedConsumer | Инкремент PropertyObjectViewStats по PropertyObjectId: ViewsCount += 1, TotalDuration += DurationSeconds, LastViewedAt = greatest(_, LastActivityAt) | business key property-object-view-stats:{PropertyObjectViewId} (INV-LVS-003) | < 1 мин |
dev-stats | DevStatsPropertyObjectViewEndedConsumer | Insert факта просмотра в ClickHouse (многомерная аналитика застройщика: даты/зрители/иерархия проект→здание→планировка, ADR-0057) | MessageId дедуп | < 5 мин |
crm | CrmPropertyObjectViewEndedConsumer | Элемент PropertyObjectViewed в ленте карточки лида (привязка по UserId; длительность как сигнал интереса агенту) | MessageId + business key PropertyObjectViewId | < 5 мин |
Гарантии доставки и порядок
- Доставка — at-least-once, публикация атомарна с закрытием записи (outbox).
Inactive-запись иммутабельна (INV-LV-008) ⇒ на одинPropertyObjectViewIdприходится ровно одна публикация; повтор возможен только на уровне доставки и снимается идемпотентностью consumer-ов (бизнес-ключPropertyObjectViewId). - Порядок по ключу. Ключ
PropertyObjectViewId— по одному событию на ключ, межсобытийного упорядочивания consumer держать не обязан. АгрегатPropertyObjectViewStatsтолерантен к порядку прихода (LastViewedAt = greatest(...), INV-LVS-004). - Межтопиковый порядок с
session-endedне гарантирован (разные топики): при закрытии/форс-закрытии сессии еёproperty-object-view-ended(по каждому просмотру) иsession-endedпубликуются в одном согласованном изменении, но идут независимо — consumer, читающий оба (dev-stats), не должен предполагать, что просмотры закрываются раньше или позже сессии; каждое событие самодостаточно и несёт собственную длительность.
Версионирование и совместимость
- Версия — один
ContractVersion = 1; при несовпадении версии consumer логирует warning и продолжает (upcasting/schema-migration нет). Топик кодирует версию суффиксом.v1по конвенции. - Additive-only для minor: новые поля — optional с безопасным default, без bump (например, новые
optional-поля
AppliedFilterModel). - Breaking (удаление/переименование/смена типа или смысла) — новый
ContractVersion = 2+ новый топикpin.user-activity.property-object-view-ended.v2+ параллельная публикация (dual-publish) на период миграции + deadline миграции consumers + sunset.v1. - Unknown enum
ViewSource— обрабатывается явно: сохранить строку как есть, warning, поведение какUnknown(fallback; producer уже нормализует недоверенный вход вUnknown— INV-LV upstream).
Безопасность
Actor producer — worker (system, без пользовательского контекста). Событие вне tenant (ADR-0052 п.2):
изоляция record-level по UserId/AnonymousId — на стороне consumer-ов. PII в payload нет.
| Поле | Класс данных | Правило |
|---|---|---|
PropertyObjectViewId, PropertyObjectId, SessionId, AnonymousId, UserId, ViewedAt, LastActivityAt, DurationSeconds | Internal | include |
ViewSource | Internal | include |
FilterSnapshot (AppliedFilterModel; *Name — денормализованные display-значения) | Internal | include; в логах не разворачивается (пишется только наличие/ContractVersion) |
Событие НЕ содержит secrets, tokens, credentials, IpAddress/UserAgent, полные PII/платёжные реквизиты,
приватные пользовательские данные, unbounded вложения.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: PostReaction + проекция PostRankingSignals (Design), Entity / Aggregate: PropertyObjectView
- API (1): PUT /api/user-activity/property-object-views/{id}/heartbeat — Продление активности просмотра объекта недвижимости
- Use Cases (4): UC-ACT-001. Трекинг сессии гостя и пользователя, UC-ACT-002. Трекинг длительности и числа просмотров объекта недвижимости, UC-ACT-003. Инвестор просматривает свою историю просмотров и действий, UC-ACT-005. Слияние гостевой и пользовательской активности при входе
- События (1): Event Contract: pin.user-activity.session-linked (SessionLinkedKafkaEvent)
- Консюмеры (4): Consumer: DevStatsPropertyObjectViewEndedConsumer, Consumer: Identity Sign-Ins (UserActivitySignedInConsumer), Consumer: User-Activity Actions (crm-user-activity-actions), Consumer: User-Activity Popularity (pin-catalog-popularity)
- Фоновые задачи (1): Background Job: ActivityCloserJob
- registries (1): Реестр событий PIN
Связанные документы
- Сущность (дом payload): property-object-view.md; дом модели фильтра: action-event.md; агрегат-приёмник: property-object-view-stats.md; родитель и механизм закрытия: session.md.
- Источник публикации: ../background/activity-closer.md (worker); собственный consumer: ../consumers/property-object-view-ended.md; границы контекста: ../README.md.
- Consumer-контексты: ../../dev-stats/README.md (facts, ADR-0057), ../../crm/README.md (лента карточки лида).
- Решения: ADR-0052 (вне tenant), ADR-0054 (модуль
views), ADR-0056 п.2–3 (трекинг длительности, polling-закрытие, инкрементальный консюмер), ADR-0057 (dev-stats).