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

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 typepin.selections.selection-item-added
Topicpin.selections.selection-item-added.v1
Partitions / key3 партиции / key = AggregateId = SelectionId (все добавления в одну подборку — в одной партиции, по порядку)
ContractVersion1
Producerselections: PUT /selections/{id}/items (по одному событию на каждый новый объект недвижимости в диффе) и POST /selections/from-favorites (по событию на каждый скопированный объект недвижимости)
Owning teamPIN Engagement
Статусdraft

Событие публикуется атомарно с изменением состава подборки: пока изменение не зафиксировано, событие не отправляется потребителям; после фиксации — доставляется гарантированно (at-least-once). Батч из N новых элементов даёт N событий в той же атомарной фиксации, что и вставка элементов (см. «Батч и гранулярность»). Ключ агрегата — SelectionId, тип агрегата — Selection; идентификатор сообщения уникален на каждый добавленный элемент (ключ дедупа потребителя).

Payload SelectionItemAddedKafkaEvent

Источники полей — entity: Selection / SelectionItem (раздел «Доменные и интеграционные события»). Вложенных моделей нет.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
SelectionIdGuidДаSelectionItem.SelectionId / Selection.IdImmutableПодборка (= AggregateId, = Kafka key).
SelectionItemIdGuidДаSelectionItem.Id (Guid v7)ImmutableИдентичность добавленного элемента — бизнес-ключ идемпотентности воронки dev-stats (uniqState(SelectionItemId)). Событие эмитится только для нового элемента (survived/reorder — не re-эмитятся), поэтому SelectionItemId уникален на добавление.
PropertyObjectIdGuidДаSelectionItem.PropertyObjectIdImmutableДобавленный объект недвижимости — lookup иерархии dim_property_object (dev-stats) и ссылка действия (user-activity).
OwnerUserIdGuidДаSelection.OwnerUserIdImmutableВладелец = кто добавил. Не nullable: гость подборки не создаёт (INV-SEL-002) — субъект ActionEvent и ключ резолва сессии в user-activity.
SelectionItemsCountintНет (optional, default 0)Selection.Items.Count после добавленияAdditive onlyЧисло объектов недвижимости в подборке после добавления (0..200, INV-SEL-004). Справочное; потребители читать не обязаны (funnel агрегирует по элементу, лента — по факту).
OccurredAtDateTimeOffsetДаSelectionItem.CreatedAtImmutableМомент добавления (дублирует время события). EventDate = toDate(OccurredAt) воронки; доменное время ActionEvent.
VersionlongДаSelection.VersionAdditive 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 → Activeitem-added + selection-updated — атомарно с заменой состава
POST /selections/from-favorites — состав скопирован из internal API favorites (INV-SEL-010)Selection: [*] → Active (Source = Favorites)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-statsDevStatsFunnelSelectionItemAddedConsumer (../../dev-stats/consumers/selection-item-added.md)Частичная строка funnel_daily, этап Selections = uniqState(SelectionItemId); иерархия — из dim_property_object по PropertyObjectIdSelectionItemId (uniqState) + MessageId в батче< 5 мин
user-activityUserActivitySelectionItemAddedConsumer (../../user-activity/consumers/selections-events.md)AddToSelectionActionEvent в action_events истории действий OwnerUserId; собственный action-tracked.v1MessageId + 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, VersionInternalinclude
OwnerUserIdInternal (в публичные ответы API не попадает; Kafka-контур внутренний)include
Title, Description, полный список PropertyObjectId, Comment элементов, PublicCodeInternal/приватная зона владельцаexclude

Событие НЕ содержит secrets, tokens, PII-профиля.

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

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

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