Event Contract: pin.selections.selection-item-added (SelectionItemAddedKafkaEvent)
Версионируемый контракт интеграционного события «в подборку добавлен объект недвижимости». Consumer-документы
ссылаются сюда, не переописывая payload. Дом полей-источников —
entity: Selection / SelectionItem.
Назначение
Фиксирует добавление одного объекта недвижимости в подборку — поэлементный факт, которого нет в
агрегатном selection-updated (тот несёт только итоговый ItemsCount, без дельты состава). Событие
нужно двум потребителям, которым синхронный API не подходит: dev-stats наполняет этап Selections
воронки views → favorites → selections → leads → bookings → deals (ADR-0057) — агрегат по объекту
недвижимости, а не по подборке; user-activity пишет AddToSelectionActionEvent в историю действий
владельца (ADR-0054). Состав целиком в событие не входит — каждый эмит несёт ровно один добавленный
PropertyObjectId.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.selections.selection-item-added |
| Topic | pin.selections.selection-item-added.v1 |
| Partitions / key | 3 партиции / key = AggregateId = SelectionId (все добавления в одну подборку — в одной партиции, по порядку) |
| ContractVersion | 1 |
| Producer | selections: PUT /selections/{id}/items (по одному событию на каждый новый объект недвижимости в диффе) и POST /selections/from-favorites (по событию на каждый скопированный объект недвижимости) |
| Owning team | PIN Engagement |
| Статус | draft |
Событие публикуется атомарно с изменением состава подборки: пока изменение не зафиксировано, событие не отправляется потребителям; после фиксации — доставляется гарантированно (at-least-once). Батч из N новых элементов даёт N событий в той же атомарной фиксации, что и вставка элементов (см. «Батч и гранулярность»). Ключ агрегата —
SelectionId, тип агрегата —Selection; идентификатор сообщения уникален на каждый добавленный элемент (ключ дедупа потребителя).
Payload SelectionItemAddedKafkaEvent
Источники полей — entity: Selection / SelectionItem (раздел «Доменные и
интеграционные события»). Вложенных моделей нет.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
SelectionId | Guid | Да | SelectionItem.SelectionId / Selection.Id | Immutable | Подборка (= AggregateId, = Kafka key). |
SelectionItemId | Guid | Да | SelectionItem.Id (Guid v7) | Immutable | Идентичность добавленного элемента — бизнес-ключ идемпотентности воронки dev-stats (uniqState(SelectionItemId)). Событие эмитится только для нового элемента (survived/reorder — не re-эмитятся), поэтому SelectionItemId уникален на добавление. |
PropertyObjectId | Guid | Да | SelectionItem.PropertyObjectId | Immutable | Добавленный объект недвижимости — lookup иерархии dim_property_object (dev-stats) и ссылка действия (user-activity). |
OwnerUserId | Guid | Да | Selection.OwnerUserId | Immutable | Владелец = кто добавил. Не nullable: гость подборки не создаёт (INV-SEL-002) — субъект ActionEvent и ключ резолва сессии в user-activity. |
SelectionItemsCount | int | Нет (optional, default 0) | Selection.Items.Count после добавления | Additive only | Число объектов недвижимости в подборке после добавления (0..200, INV-SEL-004). Справочное; потребители читать не обязаны (funnel агрегирует по элементу, лента — по факту). |
OccurredAt | DateTimeOffset | Да | SelectionItem.CreatedAt | Immutable | Момент добавления (дублирует время события). EventDate = toDate(OccurredAt) воронки; доменное время ActionEvent. |
Version | long | Да | Selection.Version | Additive only | Монотонный порядковый номер добавления элемента за всю жизнь подборки (счётчик добавлений, инкремент на каждое добавление). Упорядочивает item-added одной подборки, в том числе внутри одного батча set-items (три добавления в одной атомарной фиксации → Version n+1, n+2, n+3), и относительно selection-updated. Не путать с оптимистической блокировкой записи и с SortOrder (позиция 0..N-1, переиспользуется). |
Title/Description подборки, полный список PropertyObjectId, Comment-ы элементов и PublicCode
в событие не входят (объём/приватность — правило selection.md): каждый эмит
самодостаточен для funnel/ленты; детали читаются API по id.
Батч и гранулярность (per-item)
Выбрана поэлементная модель (одно событие на один добавленный объект недвижимости), а не батч-событие со списком — сознательно (ADR-0056: простота потребителей > экономия сообщений при низком объёме):
| Аспект | Решение |
|---|---|
PUT /selections/{id}/items добавляет N новых объектов недвижимости | Producer фиксирует N фактов добавления (по одному на каждый новый PropertyObjectId из диффа замены состава, не на survived/reorder), которые дают N событий item-added — атомарно с заменой состава и selection-updated. |
POST /selections/from-favorites копирует M объектов недвижимости | M событий item-added в той же атомарной фиксации, что и selection-created. |
| Удаление/переупорядочивание элемента | Событие не публикуется (item-removed/reordered потребителям не нужны — funnel считает только добавления, лента фиксирует только AddToSelection); факт правки несёт selection-updated. |
| Почему per-item чище | dev-stats дедуплицирует uniqState(SelectionItemId) без разбора списков; user-activity пишет один action_event на элемент без цикла по батчу; каждый эмит — один MessageId, один SelectionItemId. Батч-вариант потребовал бы у обоих потребителей внутренний цикл и разъезд идемпотентности. |
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|---|---|
PUT /selections/{id}/items — в диффе замены состава есть новый PropertyObjectId (INV-SEL-003 дедупликация, INV-SEL-004 лимит) | Selection: Active → Active | N× item-added + selection-updated — атомарно с заменой состава |
POST /selections/from-favorites — состав скопирован из internal API favorites (INV-SEL-010) | Selection: [*] → Active (Source = Favorites) | M× item-added + selection-created — атомарно с созданием подборки |
POST /selectionsс начальнымpropertyObjectIdsдобавляет элементы тем же способом и эмититitem-addedидентично — будет отражено при доработке create-selection.md; в контракте фиксируем две операции, названные в ТЗ на закрытие пробела.
Событие не публикуется при: guard-отказах (чужая/удалённая подборка INV-SEL-002/009, конфликт
оптимистической блокировки); правке названия/описания без изменения состава; удалении подборки (soft
delete — отдельный selection-deleted.v1); снятии snapshot-а (news, синхронный
m2m).
Потребители (consumers)
Сверено с ../README.md (Events И Consumers) и service-boundaries.md.
| Consumer-контекст | Consumer | Эффект (событие → что меняется) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
dev-stats | DevStatsFunnelSelectionItemAddedConsumer (../../dev-stats/consumers/selection-item-added.md) | Частичная строка funnel_daily, этап Selections = uniqState(SelectionItemId); иерархия — из dim_property_object по PropertyObjectId | SelectionItemId (uniqState) + MessageId в батче | < 5 мин |
user-activity | UserActivitySelectionItemAddedConsumer (../../user-activity/consumers/selections-events.md) | AddToSelectionActionEvent в action_events истории действий OwnerUserId; собственный action-tracked.v1 | MessageId + business SourceMessageId | < 1 мин |
crm и news на топик не подписаны: CRM хранит SelectionId и читает подборку синхронно; news
работает только со snapshot-ами.
Доставка и обработка отказов
- At-least-once через outbox (N событий батча — в той же атомарной фиксации, что и вставка элементов). Поэлементная гранулярность локализует отказ: «отравленное» одно событие уходит в DLQ, не блокируя остальные добавления того же батча.
- Идемпотентность двойная:
dev-stats— по бизнес-ключуSelectionItemId(uniqState, порядку не чувствителен),user-activity— поMessageId+SourceMessageId. Повтор доставки этап воронки не раздувает и ленту не дублирует. Порядок добавлений одной подборки — поVersion(в т.ч. внутри батча), но funnel/лента к порядку не чувствительны. - Транзиентный сбой консюмера → повторная доставка до ack; неустранимая → DLQ + alert, без потери. Общая политика — README.md, раздел «Доставка, идемпотентность и обработка отказов».
Версионирование и совместимость
ContractVersion = 1; несовпадение версии события — warn-and-proceed.- Additive-only:
SelectionItemsCount/Versionчитаются потребителями как optional — новые optional-поля без bump; breaking (удаление/смена смысла) —ContractVersion = 2+ топикpin.selections.selection-item-added.v2+ dual-publish + sunset.v1. - Enum-полей нет.
- Кросс-топиковый порядок с
created/updated/deletedне гарантирован — consumer упорядочивает поOccurredAt(в пределах одной подборки — поVersion); funnel и лента идемпотентны и порядку не чувствительны (uniqState/ append-only).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
SelectionId, SelectionItemId, PropertyObjectId, SelectionItemsCount, OccurredAt, Version | Internal | include |
OwnerUserId | Internal (в публичные ответы API не попадает; Kafka-контур внутренний) | include |
Title, Description, полный список PropertyObjectId, Comment элементов, PublicCode | Internal/приватная зона владельца | exclude |
Событие НЕ содержит secrets, tokens, PII-профиля.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: ActionEvent (TPH-иерархия событий действий), Entity / Aggregate: Selection (+ child SelectionItem)
- Use Cases (1): UC-SEL-001. Создание подборки из текущего избранного
- События (2): События контекста selections: карта топиков и отклонённые события, Event Contract: pin.selections.selection-shared (SelectionSharedKafkaEvent)
- Консюмеры (2): Consumer: DevStatsFunnelSelectionItemAddedConsumer, Consumer: Selections Events (UserActivitySelectionItemAddedConsumer)
- registries (1): Реестр событий PIN
- index.md (1): Selections Service (Pin.Selections)
Связанные документы
- Источник полей: entity:
Selection/SelectionItem(INV-SEL-002..010). - Producer-операции: ../api/public/set-selection-items.md, ../api/public/create-selection-from-favorites.md.
- Соседние события: selection-created.md, selection-updated.md, selection-deleted.md; карта — README.md.
- Потребители: dev-stats/consumers/selection-item-added.md, user-activity/consumers/selections-events.md.
- Границы: ../README.md; решения: ADR-0054, ADR-0056, ADR-0057.