Event Contract: pin.selections.selection-updated (SelectionUpdatedKafkaEvent)
Версионируемый контракт интеграционного события «подборка изменена». Consumer-документы ссылаются сюда,
не переописывая payload. Дом полей-источников — entity: Selection.
Назначение
Фиксирует правку живой подборки владельцем: название/описание (PUT /selections/{id}) или состав и
порядок целиком (PUT /selections/{id}/items). Потребитель — user-activity (ActionEvent «изменил
подборку», ADR-0054). Событие сознательно одно на обе операции правки (ADR-0056: два топика
title-updated/items-updated не дают потребителю ничего — ActionEvent один и тот же); детализация
«что именно поменялось» ленте действий не требуется, а snapshot-ы постов правка живой подборки не
затрагивает (INV-SEL-001).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.selections.selection-updated |
| Topic | pin.selections.selection-updated.v1 |
| Partitions / key | 3 партиции / key = AggregateId = SelectionId (правки одной подборки — в одной партиции, по порядку) |
| ContractVersion | 1 |
| Producer | selections: PUT /selections/{id} и PUT /selections/{id}/items |
| Owning team | PIN Engagement |
| Статус | draft |
Событие публикуется атомарно с правкой подборки (обновление корня + замена элементов): пока изменение не зафиксировано, событие не отправляется потребителям; после фиксации — доставляется гарантированно (at-least-once). Конфликт оптимистической блокировки откатывает и правку, и событие. Ключ агрегата —
SelectionId, тип агрегата —Selection.
Payload SelectionUpdatedKafkaEvent
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
SelectionId | Guid | Да | Selection.Id | Immutable | Изменённая подборка (= AggregateId, = Kafka key). |
OwnerUserId | Guid | Да | Selection.OwnerUserId | Immutable | Владелец — субъект ActionEvent (только он может менять, INV-SEL-002). |
ItemsCount | int | Да | Selection.Items.Count после правки | Additive only | Актуальное число объектов недвижимости (0..200, INV-SEL-004); при правке только названия — текущее без изменений. |
OccurredAt | DateTimeOffset | Да | Selection.UpdatedAt после мутации | Immutable | Момент правки (дублирует время события). |
Title/Description, дельта состава (какие объекты недвижимости добавлены/убраны), Comment-ы — не входят:
потребителю достаточно факта и счётчика, состав читается API по id
(selection.md, правило под таблицей событий). Вложенных моделей нет.
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|---|---|
PUT /selections/{id} — название/описание фактически изменились (no-op с теми же значениями события не публикует) | Selection: Active → Active (UpdatedAt обновлён) | атомарно с правкой подборки |
PUT /selections/{id}/items — состав/порядок фактически изменились (дедупликация INV-SEL-003, нормализация INV-SEL-006, лимит INV-SEL-004) | Selection: Active → Active | атомарно с заменой состава |
Событие не публикуется при:
- guard-отказах: чужая или удалённая подборка (INV-SEL-002, INV-SEL-009), конфликт оптимистической блокировки;
- снятии/отвязке snapshot-а (
news, m2m) — сама подборка не меняется; - soft delete — это отдельное
pin.selections.selection-deleted.v1.
Потребители (consumers)
| Consumer-контекст | Consumer | Эффект (событие → что меняется) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
user-activity | UserActivitySelectionUpdatedConsumer (будущий док: docs/06-services/user-activity/consumers/selections-actions.md) | ActionEvent «изменил подборку» (ItemsCount) в историю действий OwnerUserId | MessageId (дубль — skip) | < 1 мин |
crm не подписан: карточка клиента показывает подборку живым чтением
GET /internal/selections/{id} — событие ей не нужно. news не подписан: посты защищены
иммутабельными snapshot-ами (INV-SEL-001) и правок живой подборки не видят.
Доставка и обработка отказов
- At-least-once через outbox (атомарно с правкой; конфликт оптимистической блокировки откатывает и правку, и событие — «проигравший» перечитывает и повторяет, событие для отката не публикуется).
user-activityдедуплицирует поMessageId; правки одной подборки упорядочены ключомSelectionIdвнутри партиции, между топиками — поOccurredAt. Событие — уведомительный факт: потеря/отставание не рушит консистентность (состав всегда доступен по API).- Транзиентный сбой консюмера → повторная доставка до ack; неустранимая → DLQ + alert, без потери. Общая политика — README.md, раздел «Доставка, идемпотентность и обработка отказов».
Версионирование и совместимость
ContractVersion = 1; несовпадение версии события — warn-and-proceed.- Additive-only без bump (кандидат на additive-расширение — optional-поле
ChangeKindDetails/Items, если ленте действий понадобится различать правки); breaking —ContractVersion = 2+ топикpin.selections.selection-updated.v2+ dual-publish + sunset.v1. - Enum-полей нет.
- Кросс-топиковый порядок с
created/deletedне гарантирован — consumer упорядочивает поOccurredAt(см. selection-created.md).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
SelectionId, ItemsCount, OccurredAt | Internal | include |
OwnerUserId | Internal | include |
Title, Description, дельта PropertyObjectId, Comment элементов | Internal/зона владельца | exclude |
Событие НЕ содержит secrets, tokens, PII-профиля.
Обратные ссылки
Автоссылки: где используется этот документ.
- События (5): События контекста selections: карта топиков и отклонённые события, Event Contract: pin.selections.selection-created (SelectionCreatedKafkaEvent), Event Contract: pin.selections.selection-deleted (SelectionDeletedKafkaEvent), Event Contract: pin.selections.selection-item-added (SelectionItemAddedKafkaEvent), Event Contract: pin.selections.selection-shared (SelectionSharedKafkaEvent)
- registries (1): Реестр событий PIN
- index.md (1): Selections Service (Pin.Selections)
Связанные документы
- Источник полей: entity:
Selection(INV-SEL-002..009). - Иммутабельность копий постов: ../domain/selection-snapshot.md.
- Соседние события: selection-created.md, selection-deleted.md; карта — README.md.
- Границы: ../README.md; решения: ADR-0054, ADR-0056.