Event Contract: pin.favorites.favorite-added (FavoriteAddedKafkaEvent)
Версионируемый контракт интеграционного события «объект недвижимости добавлен в избранное».
Consumer-документы ссылаются сюда, не переописывая payload. Дом полей-источников —
entity: Favorite.
Назначение
Фиксирует факт добавления объекта недвижимости в избранное владельцем (авторизованным пользователем или гостевой
сессией) и даёт downstream-контекстам это действие без синхронного запроса в favorites:
user-activity (модуль actions, ADR-0054) превращает событие в ActionEvent «добавил в избранное» —
из истории действий факт попадает в карточку лида CRM (поток «Действия пользователя:
user-activity → crm»). Личная заметка (Note) в событие не входит
(приватные данные владельца — правило favorite.md).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.favorites.favorite-added |
| C# type | FavoriteAddedKafkaEvent (проект Pin.Favorites.Contracts) |
| Topic | pin.favorites.favorite-added.v1 |
| Partitions / key | 6 партиций (лайк — самое частое действие вовлечения; 1–10k одновременно активных, ADR-0056) / key = AggregateId = FavoriteId (порядок «add → remove» одной записи гарантирован внутри партиции) |
| ContractVersion | 1 |
| Producer | favorites: POST /favorites (добавление) и POST /favorites/import (bulk-импорт с устройства) |
| Owning team | PIN Engagement |
| Статус | draft |
Событие публикуется атомарно с созданием записи Favorite. Ключ дедупликации у потребителя —
идентификатор сообщения (MessageId); ключ партиционирования — FavoriteId (он же AggregateId,
AggregateType = "Favorite").
Payload FavoriteAddedKafkaEvent
Источники полей — entity: Favorite (раздел «Доменные и интеграционные
события»); здесь перечислено только то, что реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
FavoriteId | Guid | Да | Favorite.Id | Immutable | Идентификатор записи избранного (= AggregateId, = Kafka key). |
UserId | Guid? | Условно (XOR c AnonymousId, INV-FAV-001) | Favorite.UserId | Immutable | Владелец-пользователь; null — гостевая запись. |
AnonymousId | Guid? | Условно (XOR c UserId) | Favorite.AnonymousId | Immutable | Гостевая сессия-владелец (anonymous_id из user-activity); null — запись пользователя. user-activity приписывает действие сессии, а после связывания сессии с пользователем — ему. |
PropertyObjectId | Guid | Да | Favorite.PropertyObjectId | Immutable | Объект недвижимости, добавленный в избранное. Существование объекта недвижимости producer-ом не гарантируется (INV-FAV-004) — consumer не должен на нём падать. |
OccurredAt | DateTimeOffset | Да | Favorite.CreatedAt | Immutable | Момент добавления (хронология ActionEvent строится по этому полю). |
Инвариант payload: заполнен ровно один из UserId / AnonymousId (проекция INV-FAV-001);
оба null или оба заполнены — контрактная ошибка producer-а, consumer логирует error и пропускает
сообщение (skip, не DLQ — данные невосстановимы ретраем).
Note (заметка ≤ 4000) в payload не входит никогда — приватные данные владельца
(favorite.md, раздел «Безопасность»). Вложенных моделей нет.
Когда публикуется
| Условие | Переход состояния |
|---|---|
POST /favorites — создана новая запись Favorite (идемпотентный повтор по INV-FAV-002 — запись уже была — событие не публикуется) | Favorite: [*] → создана |
POST /favorites/import — за каждую фактически созданную запись bulk-импорта (дубли пропущены — событий по ним нет) | Favorite: [*] → создана (батч) |
Событие не публикуется при:
- переносе владельца записи в слиянии (
AnonymousId → UserId) — факт покрытpin.favorites.favorites-merged.v1, дублировать перенос N событиями add — лишний трафик и ложные ActionEvent (ADR-0056); - установке/изменении заметки (
PUT /favorites/{propertyObjectId}/note) — только доменноеFavoriteNoteUpdatedEvent(в памяти процесса); заметка приватна и действием для карточки лида не является.
Потребители (consumers)
Сверено с ../README.md (Events И Consumers) и границами сервисов.
| Consumer-контекст | Эффект (side effects) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
user-activity (будущий док: docs/06-services/user-activity/consumers/favorites-actions.md) | ActionEvent «добавил в избранное» (PropertyObjectId, владелец, OccurredAt) в историю действий; далее — проброс в карточку лида CRM потоком user-activity → crm | MessageId (дубль — skip) | < 1 мин |
CRM не подписан на топик напрямую: карточка лида получает действие через user-activity
(консолидация действий — ADR-0054; один consumer вместо веера подписчиков — ADR-0056).
Гарантии доставки, порядок и восстановление
- Доставка — at-least-once. Публикация атомарна с созданием записи
Favorite: если запись создана — событие гарантированно будет доставлено; если изменение откатилось — события нет (ложных фактов «добавил» не бывает). При ретраях доставки возможны дубликаты сообщения — потребитель дедуплицирует поMessageId. - Порядок — только внутри партиции по
FavoriteId. Для одной записиfavorite-addedвсегда предшествует парномуfavorite-removed(общий ключ → одна партиция). Между разнымиFavoriteIdи разными владельцами глобального порядка нет: хронологию действий потребитель строит поOccurredAt, а не по позиции в топике. - Восстановление у потребителя. Транзиентный сбой обработки (недоступность хранилища
user-activity) — сообщение ретраится до успеха. Нарушение контракта payload (оба владельцаnullлибо оба заполнены — проекция INV-FAV-001) невосстановимо ретраем: error-лог + skip (в DLQ не отправляется). Неразбираемое (poison) сообщение — в DLQ по политике потребителя. Replay топика безопасен: ActionEvent-проекция идемпотентна поMessageId. - Окно согласованности. Публикация асинхронна: ответ
POST /favorites(пользователь уже видит активное «сердечко») приходит сразу, а ActionEvent «добавил в избранное» в историиuser-activityи карточке лида CRM появляется < 1 мин. Недоступность шины не блокирует добавление — лента действий отстаёт до восстановления (criticalitylow).
Версионирование и совместимость
- Версия — один
ContractVersion = 1; при несовпадении версии контракта потребитель логирует warning и продолжает (warn-and-proceed). - Additive-only без bump: новые optional-поля с безопасным default.
- Breaking (удаление/переименование/смена типа
UserId/PropertyObjectId) —ContractVersion = 2+ новый топикpin.favorites.favorite-added.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Enum-полей в payload нет — правило unknown enum не применяется.
- Событие — факт (append-only, не снапшот состояния): потребителю достаточно дедупликации по
MessageId, version-guard состояния не нужен.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
FavoriteId, PropertyObjectId, OccurredAt | Internal | include |
UserId, AnonymousId | Internal (идентификаторы, не PII-профиль) | include |
Note | Приватная заметка владельца | exclude — никогда не публикуется |
Событие НЕ содержит secrets, tokens, телефонов, ФИО, текста заметок. Topic — внутренний контур Kafka (доступ по ACL сервисов); публичной выдачи события нет.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): POST /api/v1/favorites — Добавить объект недвижимости в избранное
- События (2): Event Contract: pin.favorites.favorite-removed (FavoriteRemovedKafkaEvent), Event Contract: pin.favorites.favorites-merged (FavoritesMergedKafkaEvent)
- Консюмеры (2): Consumer: DevStatsFunnelFavoriteAddedConsumer, Consumer: Favorites Events (UserActivityFavoriteAddedConsumer / ...RemovedConsumer)
- index.md (1): Favorites Service (Pin.Favorites)
- registries (1): Реестр событий PIN
Связанные документы
- Источник полей и условий: entity:
Favorite(INV-FAV-001..006). - Парное событие удаления: favorite-removed.md; слияние: favorites-merged.md.
- Границы/карта API: ../README.md; consumers контекста: ../consumers/README.md.
- Потребитель (будущий путь):
docs/06-services/user-activity/consumers/favorites-actions.md. - Решения: ADR-0054 (консолидация действий в
user-activity), ADR-0056 (simplicity-first).