Event Contract: pin.news.comment-added (CommentAddedKafkaEvent)
Версионируемый контракт интеграционного события «добавлен видимый комментарий». Consumer-документы
ссылаются сюда, не переописывая payload. Payload денормализован под адресацию уведомлений: несёт
идентификаторы адресатов (автор поста, автор родительского комментария, автор комментария) без обращения
потребителя обратно в news.
Назначение
Фиксирует создание видимого (Visible) комментария или ответа под опубликованным постом ленты
(../domain/comment.md, переход [*] → Visible). Единственный downstream-потребитель
— notifications, который по событию уведомляет автора поста («ваш пост прокомментировали») и автора
родительского комментария («вам ответили»), не запрашивая news синхронно. Тело комментария (Body) в
событие не входит — это UGC/PII; при необходимости потребитель читает его через публичное API news.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.news.comment-added |
| Topic | pin.news.comment-added.v1 |
| Partitions / key | 3 / key = PostId (= AggregateId; порядок комментариев per-post) |
| ContractVersion | 1 |
| Producer | news: POST /api/v1/posts/{id}/comments (../api/public/comments/add-comment.md) |
| Статус | draft |
Событие публикуется атомарно с созданием агрегата
Comment: до фиксации изменения событие не отправляется, после — доставляется гарантированно (at-least-once). Kafka key =PostId— все комментарии одного поста попадают в одну партицию и упорядочены.
Payload CommentAddedKafkaEvent
Дом полей комментария — entity: Comment; поля поста (Suffix, AuthorUserId) —
entity: Post. Здесь перечислено только то, что реально в событии; денормализованные
поля адресатов включены осознанно, чтобы notifications не делал синхронный запрос в news.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
CommentId | Guid | Да | Comment.Id | Immutable | Идентификатор комментария; ключ бизнес-идемпотентности у потребителя. |
PostId | Guid | Да | Comment.PostId (= Post.Id) | Immutable | Пост-владелец обсуждения; = Kafka key (порядок per-post). |
PostSuffix | string | Да | Post.Suffix | Additive only | URL-slug поста (/news/{suffix}, INV-NWS-001) для deep-link в уведомлении; денормализован. |
PostAuthorUserId | Guid | Да | Post.AuthorUserId | Immutable | Автор поста — адресат уведомления «ваш пост прокомментировали». |
ParentCommentId | Guid? | Нет | Comment.ParentCommentId | Immutable | null — корневой комментарий; иначе идентификатор родителя (ответ). |
ParentCommentAuthorUserId | Guid? | Нет | Comment(Parent).AuthorUserId | Immutable | Автор родительского комментария — адресат «вам ответили»; null у корневого. |
CommentAuthorUserId | Guid | Да | Comment.AuthorUserId | Immutable | Автор нового комментария (актор); используется для дедупликации адресатов (себе не уведомляем). |
CreatedAt | DateTimeOffset | Да | Comment.CreatedAt | Immutable | Момент создания комментария. |
Body (текст комментария) в payload не входит: UGC/PII — потребитель читает его при необходимости через
публичное API news (GET /api/v1/posts/{id}/comments), а не из события (правило безопасности ниже).
RootCommentId/Depth/Path также исключены — для адресации уведомлений не нужны; дом дерева —
../domain/comment.md.
Когда публикуется
| Условие | Переход состояния | Согласованность |
|---|---|---|
Создание видимого комментария (корневого или ответа) — операция add-comment, инварианты INV-CMT-001..003 | [*] → Visible (../domain/comment.md) | Comment, инкремент Post.CommentsCount и факт публикации события фиксируются в одном согласованном изменении |
Скрытие/удаление комментария (HiddenByModerator/DeletedByAuthor) не публикует событие — терминальные
переходы обрабатываются локально (INV-CMT-005: декремент CommentsCount), уведомлять некого.
Потребители (consumers)
| Consumer-контекст | Эффект | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
notifications | Уведомления адресатам (см. правило ниже): «ваш пост прокомментировали» — PostAuthorUserId; «вам ответили» — ParentCommentAuthorUserId | CommentId (business key) + идентификатор сообщения | < 1 мин |
Правило адресации и дедупликации (в потребителе):
- множество адресатов =
{ PostAuthorUserId, ParentCommentAuthorUserId }минус{ CommentAuthorUserId }(автор не уведомляется о собственном комментарии); - если
PostAuthorUserId == ParentCommentAuthorUserId— одно уведомление (дедуп по получателю; тип — «вам ответили» как более специфичный); ParentCommentAuthorUserId == null(корневой комментарий) — уведомляется толькоPostAuthorUserId;- идемпотентность per получатель — по паре (
CommentId,RecipientUserId): повторная доставка события не создаёт дубль уведомления.
Версионирование и совместимость
ContractVersion = 1; при несоответствии версии события consumer логирует warning и продолжает (нет upcasting/schema-migration). Топик кодирует версию по конвенции — суффикс.v1.- Additive-only для minor: новые optional-поля с безопасным default — без bump
ContractVersion. - Breaking (удаление/переименование/смена типа или смысла поля) —
ContractVersion = 2+ новый топикpin.news.comment-added.v2+ dual-publish на период миграции + deadline миграции consumer + sunset.v1. - Порядок per-post гарантирован ключом =
PostId; ответ в треде публикуется после корня (CommentIdхронологически монотонен), потребитель обрабатывает события одного поста последовательно.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
CommentId, PostId, ParentCommentId | Internal | include (идемпотентность/адресация) |
PostSuffix | Public | include (публичный slug опубликованного поста) |
PostAuthorUserId, ParentCommentAuthorUserId, CommentAuthorUserId | Internal | include (адресация уведомлений); в логах producer/consumer — маскирование id |
CreatedAt | Internal | include |
Body (текст комментария) | PII / UGC | exclude — в payload не входит; потребитель читает через публичное API news при необходимости |
Событие НЕ содержит secrets, tokens, raw credentials, полные PII, платёжных данных и unbounded-вложений.
Адресаты уведомлений — пользователи (User); tenant не применяется — Comment не tenanted (публичный UGC
под опубликованным постом, ../domain/comment.md); доступ к топику — m2m ACL Kafka
(producer news, consumer notifications).
Гарантии доставки и порядок
- At-least-once, атомарно с агрегатом. Событие уходит только после фиксации создания
Comment(транзакционный outbox): дубли доставки возможны, потерь нет; потребитель дедуплицирует поCommentId(шаг адресации выше). Окно между фиксацией и доставкой — секунды (лаг outbox → SLA потребителя < 1 мин); в этом окне уведомление ещё не отправлено, но комментарий уже виден в дереве поста (read-путьnewsне ждёт события). - Порядок per-post гарантирован ключом
PostId(одна партиция на пост): для одного поста корневой комментарий доставляется раньше ответа на него (CommentIdv7 хронологически монотонен). Между разными постами порядок не гарантирован — потребителю он не нужен (адресация независима на пост). - Что видит адресат в промежуточном состоянии. Пока событие в пути, автор поста уже может увидеть комментарий, открыв пост, но push/ин-апп уведомления ещё нет — это ожидаемая eventual-семантика, а не рассинхрон.
Acceptance Criteria
- Корневой комментарий. Дано опубликованный пост с
CommentsEnabled = trueи авторомA; когда пользовательB(B ≠ A) добавляет корневой комментарий; тогда публикуется одно событие сParentCommentId = null,ParentCommentAuthorUserId = null,PostAuthorUserId = A,CommentAuthorUserId = B, и уведомляется толькоA. - Ответ в треде. Дано корневой комментарий автора
Cпод постом автораA; когдаBотвечает на него; тогда событие несётParentCommentAuthorUserId = C,PostAuthorUserId = A, и уведомляютсяAиC(два адресата). - Дедуп адресатов. Дано пост автора
A, гдеAже оставил корневой комментарий; когдаBотвечает авторуA; тогдаPostAuthorUserId == ParentCommentAuthorUserId == A→ одно уведомление типа «вам ответили». - Комментарий к своему посту. Дано пост автора
A; когдаAкомментирует собственный пост корневым комментарием; тогда множество адресатов{A} \ {A}пусто — уведомление не отправляется. - Повторная доставка. Дано событие с
CommentId = Xуже обработаноnotifications; когда то же событие доставлено повторно; тогда по идемпотентности (CommentId,RecipientUserId) новых уведомлений не создаётся. - Скрытие/удаление. Дано видимый комментарий; когда он переходит в
HiddenByModeratorилиDeletedByAuthor; тогда событиеcomment-addedне публикуется (уведомлять некого) — публикуется только при[*] → Visible. - Deep-link. Событие несёт
PostSuffix, достаточный для сборки ссылки/news/{suffix}без обратного запроса вnews.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: Comment (вложенные, переключаемые)
- API (2): Admin: модерация постов и комментариев (m2m), POST /api/v1/posts/{id}/comments — добавить комментарий/ответ
- Use Cases (1): UC-NWS-003. Вложенные переключаемые комментарии
- События (1): События сервиса news: карта топиков и отклонённые события/консюмеры
- Консюмеры (1): Consumer: News Events → Notifications (notifications-news-events)
- Фоновые задачи (1): Background Job: CommentCountReconcileJob
- Дашборды (1): Панель Наблюдаемости: news-overview (эксплуатация)
- registries (1): Реестр событий PIN
Связанные документы
- Сущности:
../domain/comment.md(поля, инварианты INV-CMT-001..006, статусы),../domain/post.md(SuffixINV-NWS-001,AuthorUserId, денорм.CommentsCount). - Источник (операция-producer):
../api/public/comments/add-comment.md. - Соседний контракт сервиса:
post-published.md(pin.news.post-published.v1); карта событий — README (раздел «Events и Consumers»). - Потребитель (планируется):
notifications/consumers/news-comment-added.md. - Permission-каталог:
../../identity/domain/permission.md(news.comments.moderate). - ADR: ADR-0052, ADR-0056.