События сервиса news: карта топиков и отклонённые события/консюмеры
Индекс интеграционных событий pin.news.* (по одному контракт-доку на топик) + дом решений об
интеграциях сервиса: что публикуется, что потребляется и что сознательно не публикуется/консюмится.
Минимальность — ADR-0056: топик существует только при живом потребителе, которому синхронный API не
подходит.
Metadata
| Поле | Значение |
|---|---|
| Owning team | PIN Engagement |
| Статус документа | draft |
| Область действия | контекст news, папка events/ (produced + consumed + отклонённые) |
| Assumptions | contract-доки post-published.md/comment-added.md, consumer-доки ../consumers/*.md — планируются; дом полей payload — доменные агрегаты |
| Зависимости | Kafka (outbox producer + консюмеры), identity (upstream-топики), selections (синхронный m2m вместо Kafka) |
Метаданные и общие правила
Каждое событие несёт метаданные: MessageId (Guid v7, для дедупликации), CreatedAt, версию контракта,
типизированный payload, AggregateId, AggregateType; сквозной trace восстанавливается из метаданных. Оба
produced-топика публикуются через транзакционный outbox атомарно с мутацией агрегата. У обоих
ContractVersion = 1.
Ключ партиционирования обоих топиков = PostId — гарантирует per-post порядок: публикация и
комментарии одного поста упорядочены между собой. У обоих AggregateId = PostId (= Kafka key),
AggregateType = "Post"; comment-added дополнительно несёт CommentId в payload
(NumberOfPartitions = 3 у каждого топика).
Публикуемые события (по одному контракт-доку на топик)
| Событие | Topic | Producer-операция | Consumers | Контракт |
|---|---|---|---|---|
PostPublishedKafkaEvent | pin.news.post-published.v1 | POST /api/v1/posts/{id}/publish (Publish/Republish → Published) | notifications (рассылка подписчикам); user-activity (таймлайн действий автора, опц.) | post-published.md |
CommentAddedKafkaEvent | pin.news.comment-added.v1 | POST /api/v1/posts/{id}/comments (Add/Reply → Visible) | notifications (автору поста + автору родительского комментария) | comment-added.md |
Дом полей payload — доменные агрегаты (post.md / comment.md, раздел «Доменные и интеграционные события»); ниже — типизированная проекция (дельта), а не второй дом.
Payload PostPublishedKafkaEvent
| Поле | Тип | Обяз. | Источник (Post) | Описание |
|---|---|---|---|---|
PostId | Guid | Да | Id | Ключ агрегата (= Kafka key). |
AuthorUserId | Guid | Да | AuthorUserId | Автор поста (адресат уведомлений подписчиков). |
AuthorType | string (enum PostAuthorType) | Да | AuthorType | Investor/DeveloperManager/Agent/AgencyManager/PinEmployee. |
AuthorCompanyId | Guid? | Нет | AuthorCompanyId | Компания-автор; null — инвестор/сотрудник. |
CategoryId | Guid | Да | CategoryId | Категория поста. |
Suffix | string | Да | Suffix | URL-slug (kebab-case) для диплинка в уведомлении. |
Title | string | Да | Title | Заголовок опубликованного поста. |
Subtitle | string? | Нет | Subtitle | Подзаголовок. |
CoverFileId | Guid? | Нет | CoverFileId | Обложка (storage/CDN, без FK). |
ProjectIds | IReadOnlyList<Guid> | Да (может быть пустым) | ProjectRefs.ProjectIds | Привязка застройщика к проектам. |
VersionNumber | int | Да | CurrentVersionNumber | Номер PostVersion этой публикации. |
PublishedAt | DateTimeOffset | Да | PublishedAt | Момент текущей публикации. |
FirstPublishedAt | DateTimeOffset | Да | FirstPublishedAt | Момент первой публикации (republish не меняет). |
Payload CommentAddedKafkaEvent
| Поле | Тип | Обяз. | Источник (Comment) | Описание |
|---|---|---|---|---|
CommentId | Guid | Да | Id | Идентификатор добавленного комментария. |
PostId | Guid | Да | PostId | Пост-владелец (= AggregateId = Kafka key, per-post порядок). |
ParentCommentId | Guid? | Нет | ParentCommentId | null — корневой; иначе — адресат «ответили на ваш комментарий». |
RootCommentId | Guid? | Нет | RootCommentId | Корень треда. |
AuthorUserId | Guid | Да | AuthorUserId | Автор комментария (для anti-self-notify у notifications). |
Depth | short | Да | Depth | Уровень вложенности (0..6). |
CreatedAt | DateTimeOffset | Да | CreatedAt | Момент создания. |
Тело комментария (Body) в payload не входит — потребителю (notifications) не нужно, UGC читается
по API поста; минимизация полезной нагрузки и поверхности данных.
Гарантии доставки и окна согласованности
Оба produced-топика — at-least-once, публикация атомарна с мутацией агрегата (транзакционный outbox):
до фиксации изменения событие не уходит, после — доставляется гарантированно; дубли доставки возможны и
дедуплицируются потребителем по бизнес-ключу (PostId+VersionNumber для публикации, CommentId для
комментария). Порядок гарантирован только per-post (ключ = PostId).
| Поток | Что видит пользователь сразу (read-путь news) | Асинхронное следствие и окно |
|---|---|---|
| Публикация поста | Пост в ленте global и по прямой ссылке /news/{suffix} — сразу после коммита (частичный индекс по Status=Published) | Лента subscription — pull-запросом подписчика, тоже сразу (события для неё не требуется); поисковая проекция news-posts (fts, ADR-0059) — через секунды (upsert по доменному событию); notifications рассылает подписчикам с SLA < 1 мин |
| Добавление комментария | Комментарий в дереве поста и инкремент CommentsCount — сразу (в одной транзакции) | notifications уведомляет адресатов с SLA < 1 мин; в окне доставки уведомления ещё нет, но комментарий уже виден |
Реплика identity (consumed) | Прежняя подпись автора/компании (последний применённый upsert) | Новая подпись/статус — после обработки консюмером (лаг < 1 мин SLO); эффекты Blocked/Suspended (INV-REP-002/003) применяются query-слоем выдачи, а не в момент приёма события |
Публикация без реального потребителя не заводится (ADR-0056): post-published/comment-added существуют,
потому что у них есть notifications; отклонённые кандидаты — ниже.
Потребляемые события (consumed)
Реплики подписей identity для join-free ленты (дом — identity-replicas.md).
Мутация реплики и отметка идемпотентности — в одном согласованном изменении.
| Топик (upstream) | Событие | Consumer-док | Side effect | Идемпотентность |
|---|---|---|---|---|
pin.identity.users.v1 | UserUpsertedKafkaEvent / UserBlockedKafkaEvent | ../consumers/identity-users.md | Upsert AuthorReplica (подпись автора без джойна); Blocked → пометка/скрытие постов автора (INV-REP-002) | MessageId + монотонный SourceVersion-guard (INV-REP-001/004) |
pin.identity.companies.v1 | CompanyUpsertedKafkaEvent / CompanySuspendedKafkaEvent | ../consumers/identity-companies.md | Upsert CompanyReplica; Suspended → исключение постов компании из публичных лент (INV-REP-003) | то же |
Контракты источника — user-upserted.md / user-blocked.md / company-upserted.md / company-suspended.md. Реплики собственных событий не публикуют (ADR-0056 — не плодить события без потребителей).
Отклонённые события/консюмеры (решение, не пробел)
pin.selections.selection-shared / snapshot-created — НЕ консюмятся
news не подписан на эти топики. snapshot-created selections не публикует (снятие snapshot —
синхронный m2m); selection-shared selections публикует, но для crm/user-activity (лента лида /
история действий агента) — к embed-подборкам поста отношения не имеет
(selections/events/README.md). Embed-интеграция news↔selections
— синхронная через internal API selections (m2m: create/detach/get snapshot); связь
PostId ↔ SnapshotId news держит в своей транзакции.
| # | Причина | Детали |
|---|---|---|
| 1 | Событие к embed не относится / отсутствует | selection-shared адресован crm/user-activity, не блоку поста; snapshot-created selections не эмитит — консюмить нечего (ADR-0056). |
| 2 | Единственное взаимодействие синхронно | news вызывает POST /internal/selections/{id}/snapshots и получает SnapshotId в ответе (UC-SEL-004); событие ничего не добавило бы. |
| 3 | Консистентность на синхронном ответе | SnapshotId фиксируется в typed-jsonb блоке поста в транзакции news; событийная связка открыла бы окно рассинхрона. |
Пересмотр: серверная операция selections с реальным событийным потребителем — additive.
pin.news.post-liked / post-unpublished / post-deleted — НЕ публикуются
| Кандидат | Причина отклонения |
|---|---|
post-liked | Лайки моделируются PostReaction (спроектировано, реализация отложена — решение №10, ADR-0056) как часть отложенного ранжирования; внешнего потребителя нет (post-ranking-model.md). |
post-unpublished | Снятие с публикации меняет Status в БД news; из ленты пост выпадает частичным индексом, из Elasticsearch-проекции поиска — внутренним доменным событием PostUnpublishedEvent (не Kafka, ADR-0059); notifications «отписку» не рассылает — потребителя факта нет. |
post-deleted | Soft-delete + detach snapshot-ов — синхронный вызов selections (UC-SEL-004), а не событие; внешнего потребителя нет. |
Пересмотр — additive при появлении реального потребителя (запуск ранжирования, внешний индекс/кэш).
Просмотры постов — не событие news
Просмотр поста трекает фронт → ActionEvent в user-activity (ADR-0054), а не Kafka-событие news.
Денормализованный Post.ViewsCount — только для отображения/сортировки, пересчёт reconcile-job-ом.
Безопасность
| Поток | Класс данных | Правило |
|---|---|---|
post-published payload | Public | Опубликованный пост публичен; include (secrets/PII в payload нет). |
comment-added payload | Internal | Только id-граф комментария + Depth/CreatedAt; тело Body (UGC) исключено — exclude. |
consumed identity-реплики | PII (ФИО автора / название компании) | Приходят из доверенного топика identity (m2m ACL); в репликах news — публичная display-подпись; PhoneNumber/Email не реплицируются; в логах ФИО маскируется. |
AuthorUserId / AuthorCompanyId / id-поля | Internal | include. |
Actor/permission — на стороне producer-операций (news.posts.*, гейт CommentsEnabled); консюмеры —
system actor (без пользовательского контекста). События НЕ содержат secrets, tokens, raw PII/платёжных
реквизитов, тел постов/комментариев целиком, unbounded-вложений.
Связанные документы
- Контракты (планируются): post-published.md, comment-added.md; консюмеры: ../consumers/identity-users.md, ../consumers/identity-companies.md.
- Дома полей: ../domain/post.md, ../domain/comment.md, ../domain/identity-replicas.md, ../domain/post-ranking-model.md.
- Отклонённые интеграции: ../../selections/events/README.md, UC-SEL-004, ../../selections/domain/selection-snapshot.md.
- Границы/потоки: ../README.md; решения: ADR-0054 (просмотры →
user-activity), ADR-0056 (simplicity-first / минимальность топиков).