Event Contract: pin.favorites.favorites-merged (FavoritesMergedKafkaEvent)
Версионируемый контракт интеграционного события «избранное сессии слито в аккаунт».
Consumer-документы ссылаются сюда, не переописывая payload. Дом полей-источников и алгоритма —
merge-strategy.md (сущность FavoritesMergeOperation, стратегии
DeviceWins / AccountWins / Merge).
Назначение
Фиксирует завершение операции слияния гостевого и аккаунтного избранного (POST /favorites/merge
или автослияние вырожденного случая) одним событием со стратегией и счётчиками — вместо веера
favorite-added/favorite-removed по каждой затронутой записи (те при слиянии сознательно не
публикуются, ADR-0056). Потребитель user-activity пишет ActionEvent «объединил избранное» в
историю действий пользователя.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.favorites.favorites-merged |
| C# type | FavoritesMergedKafkaEvent (проект Pin.Favorites.Contracts) |
| Topic | pin.favorites.favorites-merged.v1 |
| Partitions / key | 3 партиции (низкий объём: ≤ 1 операция на пару «пользователь × сессия», INV-FAV-101) / key = UserId (все слияния одного пользователя — в одной партиции, по порядку) |
| ContractVersion | 1 |
| Producer | favorites: операция слияния (шаг 6 алгоритма merge-strategy.md) |
| Owning team | PIN Engagement |
| Статус | draft |
Событие публикуется атомарно с мутациями записей (удаление / перенос владельца) и созданием записи
FavoritesMergeOperation — в одном согласованном изменении (INV-FAV-104). Идемпотентный повтор merge
(INV-FAV-101) события не публикует. Ключ дедупликации — идентификатор сообщения (MessageId);
ключ партиционирования — UserId; AggregateId = MergeOperationId,
AggregateType = "FavoritesMergeOperation".
Payload FavoritesMergedKafkaEvent
Payload — все поля FavoritesMergeOperation (merge-strategy.md),
кроме заметок (текст заметок в операции и так не хранится).
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
MergeOperationId | Guid | Да | FavoritesMergeOperation.Id | Immutable | Идентификатор операции слияния (= AggregateId). |
UserId | Guid | Да | FavoritesMergeOperation.UserId | Immutable | Пользователь, в чей аккаунт слито избранное (= Kafka key). ActionEvent приписывается ему. |
AnonymousId | Guid | Да | FavoritesMergeOperation.AnonymousId | Immutable | Гостевая сессия-источник (связана с UserId — проверено INV-FAV-102). |
Strategy | string (enum FavoritesMergeStrategy) | Да | FavoritesMergeOperation.Strategy | Additive only (новые значения) | DeviceWins / AccountWins / Merge — применённая стратегия (текстовая сериализация enum). |
IsAutomatic | bool | Да | FavoritesMergeOperation.IsAutomatic | Additive only | true — автослияние вырожденного случая без диалога (таблица триггеров merge-strategy.md). |
AccountCountBefore | int | Да | FavoritesMergeOperation.AccountCountBefore | Additive only | Записей в аккаунте до слияния. |
SessionCountBefore | int | Да | FavoritesMergeOperation.SessionCountBefore | Additive only | Записей в гостевой сессии до слияния. |
MovedCount | int | Да | FavoritesMergeOperation.MovedCount | Additive only | Перенесено записей сессии в аккаунт. |
RemovedFromAccountCount | int | Да | FavoritesMergeOperation.RemovedFromAccountCount | Additive only | Удалено записей аккаунта (только DeviceWins). |
RemovedFromSessionCount | int | Да | FavoritesMergeOperation.RemovedFromSessionCount | Additive only | Удалено записей сессии (дубли Merge; всё — AccountWins). |
NotesMergedCount | int | Да | FavoritesMergeOperation.NotesMergedCount | Additive only | Заметок перенесено/перезаписано по таблице конфликтов заметок. |
TotalCountAfter | int | Да | FavoritesMergeOperation.TotalCountAfter | Additive only | Итоговое число записей аккаунта. |
OccurredAt | DateTimeOffset | Да | FavoritesMergeOperation.CreatedAt | Immutable | Момент выполнения слияния. |
Списки PropertyObjectId затронутых записей и тексты заметок в payload не входят (объём + приватность;
потребителю достаточно счётчиков — актуальный список читается GET /favorites).
Вложенных моделей нет.
Когда публикуется
| Условие | Переход состояния |
|---|---|
POST /favorites/merge { strategy } — первое слияние пары (UserId, AnonymousId); мутации записей выполнены | FavoritesMergeOperation: [*] → создана; записи Favorite: перенос владельца / физическое удаление по таблице стратегий |
Автослияние вырожденного случая (IsAutomatic = true, всегда Strategy = Merge) | та же |
| Повторный merge той же пары (INV-FAV-101) | нет перехода — идемпотентный успех, событие не публикуется |
Потребители (consumers)
| Consumer-контекст | Эффект (side effects) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
user-activity (будущий док: docs/06-services/user-activity/consumers/favorites-actions.md) | ActionEvent «объединил избранное» (Strategy, счётчики) в историю действий UserId | MessageId; страховка — business key MergeOperationId (unique по паре, INV-FAV-101) | < 1 мин |
Карточке лида CRM событие напрямую не нужно: агент видит суммарное действие через историю
user-activity, актуальный список избранного — через факты add (карточка лида не пересчитывает
состав по merge-счётчикам).
Гарантии доставки, порядок и восстановление
- Доставка — at-least-once. Публикация атомарна с мутациями записей и созданием
FavoritesMergeOperation(одно согласованное изменение, INV-FAV-104): операция зафиксирована → событие гарантированно доставится; идемпотентный повтор merge (INV-FAV-101) события не порождает. Дубликаты доставки дедуплицируются поMessageId; страховка — business keyMergeOperationId(unique по паре). - Порядок — внутри партиции по
UserId. Все слияния одного пользователя идут по порядку; на пару «пользователь × сессия» приходится ≤ 1 операция (INV-FAV-101), поэтому конкурирующих merged-событий по одной паре не бывает. - Восстановление у потребителя. Транзиентный сбой обработки — retry до успеха; неизвестное значение
Strategy(появилась новая стратегия) — не падение: ActionEvent пишется с исходной строкой стратегии + warning; poison — в DLQ по политике потребителя. Replay безопасен (дедупликацияMessageId/MergeOperationId). - Окно согласованности. Итоговый набор аккаунта доступен сразу после ответа
POST /favorites/merge(GET /favorites— то же хранилище, сильная согласованность); ActionEvent «объединил избранное» в историиuser-activityпоявляется асинхронно (< 1 мин).
Версионирование и совместимость
ContractVersion = 1; несовпадение версии контракта — warn-and-proceed.- Additive-only: новые optional-поля/счётчики — без bump; breaking —
ContractVersion = 2+ топикpin.favorites.favorites-merged.v2+ dual-publish + sunset.v1. - Unknown enum
Strategy(появилась новая стратегия) — fallback: consumer пишет ActionEvent с исходной строкой стратегии + warning; падать нельзя. - Событие — факт завершённой операции: replay безопасен (дедупликация
MessageId/MergeOperationId).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
MergeOperationId, Strategy, IsAutomatic, счётчики, OccurredAt | Internal | include |
UserId, AnonymousId | Internal | include |
Списки PropertyObjectId сторон, тексты заметок | Приватные данные владельца | exclude — не публикуются |
Событие НЕ содержит secrets, tokens, PII-профиля. AnonymousId в событии — уже связанная с
пользователем сессия (INV-FAV-102), утечки чужого идентификатора нет.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Merge Strategy: слияние избранного при авторизации (device-wins / account-wins / merge)
- API (1): POST /api/v1/favorites/merge — Применить стратегию слияния избранного
- События (2): Event Contract: pin.favorites.favorite-added (FavoriteAddedKafkaEvent), Event Contract: pin.favorites.favorite-removed (FavoriteRemovedKafkaEvent)
- index.md (1): Favorites Service (Pin.Favorites)
- registries (1): Реестр событий PIN
Связанные документы
- Дом операции/полей: merge-strategy.md (
FavoritesMergeOperation, алгоритм слияния, INV-FAV-101..105); записи — favorite.md. - События add/remove (при merge не публикуются): favorite-added.md, favorite-removed.md.
- Границы: ../README.md; consumers контекста: ../consumers/README.md.
- Решения: ADR-0054, ADR-0056.