Event Contract: pin.news.post-published (PostPublishedKafkaEvent)
Версионируемый контракт интеграционного события «пост ленты новостей опубликован/переопубликован».
Consumer-документы ссылаются сюда, не переописывая payload. Событие фиксирует факт перехода поста в
Published (первичная публикация и каждый republish) и несёт публичные display-поля + attribution автора
— потребители обходятся без синхронного запроса в news. Дом полей поста — post.md;
дом слепка версии — post-version.md.
Назначение
Даёт downstream-контекстам публичный snapshot опубликованного поста в момент публикации: notifications
рассылает уведомления подписчикам автора, user-activity (опц.) пишет действие «автор опубликовал пост» в
таймлайн лида. Метаданные опубликованного поста (заголовок, подпись, обложка, категория, привязки) публичны
по определению ленты; PII нет — ФИО автора в payload не входит и берётся потребителем из собственной
реплики identity (identity-replicas.md).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.news.post-published |
| Topic | pin.news.post-published.v1 |
| Partitions / key | 3 / key = PostId (= AggregateId; порядок публикаций per-post) |
| ContractVersion | 1 |
| Producer | news: публикация/переопубликация поста (дом publish-post.md) |
| Статус | draft |
Событие публикуется атомарно с переходом
Post → Publishedи захватом слепкаPostVersion(INV-NWS-005 / INV-PV-003): до фиксации изменения событие не отправляется, после — доставляется гарантированно (at-least-once). Kafka key =PostId— публикации одного поста упорядочены.
Payload PostPublishedKafkaEvent
Snapshot-поля ссылаются на дом post.md (и post-version.md
для замороженного CategorySlug); здесь перечислено только то, что реально в событии. Тело блоков
(PostContentDocument) не входит — потребитель, которому нужен контент, читает пост по PostId/Suffix
через public API/рендер.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
PostId | Guid | Да | Post.Id | Immutable | Ключ агрегата (= Kafka key). |
AuthorUserId | Guid | Да | Post.AuthorUserId | Immutable | Автор поста. ФИО не в payload — из реплики identity у потребителя. |
AuthorType | string (enum PostAuthorType) | Да | Post.AuthorType | Additive only (новые значения) | Investor / DeveloperManager / Agent / AgencyManager / PinEmployee; snapshot identity на момент создания. |
AuthorCompanyId | Guid? | Нет | Post.AuthorCompanyId | Additive only | Компания застройщика/агентства (ADR-0052); null — инвестор/сотрудник. |
CategoryId | Guid | Да | Post.CategoryId | Immutable | Категория поста (post-category.md). |
CategorySlug | string | Да | PostVersion.Snapshot.CategorySlug | Additive only | Человекочитаемый slug на момент публикации (заморожен в слепке — переименование категории позже не влияет). |
Title | string | Да | Post.Title | Additive only | Заголовок поста (публичный display). |
Subtitle | string? | Нет | Post.Subtitle | Additive only | Подзаголовок. |
Suffix | string | Да | Post.Suffix | Immutable | URL-slug (kebab-case), база /news/{suffix} (INV-NWS-001). |
CoverFileId | Guid? | Нет | Post.CoverFileId | Additive only | Обложка (storage/CDN, без FK; пресайн-ссылку строит потребитель). |
ProjectRefs | IReadOnlyList<Guid> | Да (может быть пустым) | Post.ProjectRefs.ProjectIds | Additive only | Проекты застройщика; пусто для не-DeveloperManager. |
PublishedAt | DateTimeOffset | Да | Post.PublishedAt | Immutable semantics | Момент текущей публикации (тайминг/сортировка у потребителя). |
VersionNumber | int | Да | Post.CurrentVersionNumber (после инкремента) = PostVersion.VersionNumber | Immutable semantics | Монотонный номер версии в пределах поста; бизнес-ключ идемпотентности user-activity. |
IsFirstPublish | bool | Да | Post.FirstPublishedAt == Post.PublishedAt (⇔ PostVersion.Reason = Published) | Additive only | true — первая публикация (Draft → Published); false — republish. |
PostId — immutable ключ агрегата; AuthorUserId/AuthorType/AuthorCompanyId/CategoryId/Suffix —
иммутабельны по семантике поста (правки роли/атрибуции пост не меняют). Остальные поля переиздаются при
каждом republish с новым VersionNumber.
Когда публикуется
| Условие | Переход состояния | Согласованность |
|---|---|---|
Первичная публикация (Reason = Published, IsFirstPublish = true; выставляет PublishedAt и FirstPublishedAt) | Draft → Published | Post, PostVersion и факт публикации события фиксируются в одном согласованном изменении (INV-NWS-005 / INV-PV-003) |
Повторная публикация после правок или из Unpublished (Reason = Republished, IsFirstPublish = false; FirstPublishedAt не меняется) | Published → Published / Unpublished → Published | та же |
Источник перехода — POST /api/v1/posts/{id}/publish (publish-post.md).
Правки черновика (Draft → Draft), Unpublish и soft-delete (→ Archived) события не публикуют —
версионируются и публикуются только переходы в Published (дом статусной машины — post.md).
Потребители (consumers)
| Consumer-контекст | Эффект | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
notifications | Рассылка подписчикам автора (fan-out по собственному стору подписок notifications — AuthorSubscription из news не читает; событие даёт факт публикации + attribution) | first-seen по идентификатору сообщения | < 1 мин |
user-activity (опц.) | ActionEvent «автор опубликовал пост» → таймлайн действий лида (ADR-0054) | business key PostId + VersionNumber | < 5 мин |
AuthorSubscription (лента subscriptions) — приватная сущность news; рассылка подписчикам
инкапсулирована в notifications со своей моделью подписок, поэтому событие сознательно не несёт список
получателей.
Гарантии доставки и порядок
Что контракт предоставляет downstream-контекстам и как они обязаны его потреблять:
- Доставка — at-least-once. Событие фиксируется атомарно с переходом
→ Published(INV-NWS-005 / INV-PV-003): «опубликованного поста без события» не существует, но одно и то же событие может прийти повторно. Дедупликация — обязанность потребителя (колонка «Идемпотентность»):notifications— first-seen по идентификатору сообщения;user-activity— по бизнес-ключу (PostId,VersionNumber). Повтор доставки не должен порождать дубли уведомлений/действий. - Порядок — только per-post (Kafka key =
PostId): републикации одного поста доставляются в порядке ростаVersionNumber; глобального порядка между разными постами нет. Потребитель держит guard по монотонномуVersionNumber— событие соVersionNumberне больше уже обработанного игнорируется как устаревшее (защита от переупорядочивания при ретраях). - Ретраи и DLQ — на стороне потребителя. Транзиентный сбой обработки ретраится транспортом; исчерпание
попыток → DLQ потребителя. Продьюсер
newsповторную доставку не инициирует и компенсаций не выполняет: событие терминально (описывает уже свершившийся факт публикации), откату не подлежит. - Окно eventual-consistency. Пост виден в лентах и по прямой ссылке сразу после коммита (публикация
в PostgreSQL и факт события — в одном изменении); подписчик получает уведомление асинхронно (SLA
notifications< 1 мин), таймлайн лида обновляется < 5 мин. Расхождение «пост уже виден ↔ уведомление ещё не пришло» — ожидаемое промежуточное состояние.
Обратных (входных) событий для публикации контракт не ожидает: producer собирает snapshot из
собственного агрегата Post/PostVersion; ФИО автора не входит — потребитель добирает его из своей реплики
identity.
Версионирование и совместимость
ContractVersion = 1; при несоответствии версии события consumer логирует warning и продолжает (нет upcasting/schema-migration). Топик кодирует версию суффиксом.v1по конвенции.- Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump.
- Breaking (удаление/переименование/смена типа/смысла) — новый
ContractVersion = 2+ новый топикpin.news.post-published.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Unknown enum
AuthorType— fallback: сохранить строку как есть, warning (attribution деградирует до «прочий автор», не fail). - Порядок публикаций одного поста гарантирован ключом
PostId; republish-события упорядочены между собой, потребители держат guard по монотонномуVersionNumber.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
PostId, AuthorUserId, AuthorType, AuthorCompanyId, CategoryId, VersionNumber, PublishedAt, IsFirstPublish | Public | include (метаданные опубликованного поста публичны) |
Title, Subtitle, Suffix, CategorySlug, CoverFileId | Public | include (заголовок/подпись/обложка/slug публикуются в ленте) |
ProjectRefs | Public | include (публичные привязки проектов застройщика) |
Все поля — Public: событие фиксирует уже опубликованный (общедоступный) контент. PII нет — ФИО автора в
payload не входит (берётся потребителем из реплики identity). Событие не содержит тела блоков
(PostContentDocument), содержимого подборок, secrets, tokens, платёжных данных, unbounded вложений.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: NotificationEntity, Entity / Aggregate: Post
- API (5): Admin: модерация постов и комментариев (m2m), GET /api/v1/feed/subscriptions — Лента по подпискам, POST /api/v1/posts — Создать черновик поста, POST /api/v1/posts/{id}/publish — публикация поста (snapshot + событие), PUT /api/v1/posts/{id}/blocks — Редактирование блоков поста (embed через selections)
- Use Cases (4): UC-NWS-001. Создать черновик и опубликовать пост, UC-NWS-004. Подписки и лента subscription, UC-NWS-005. Поиск постов (Elasticsearch), UC-NWS-007. Категории по типам авторов (eligibility публикации)
- События (2): События сервиса news: карта топиков и отклонённые события/консюмеры, Event Contract: pin.news.comment-added (CommentAddedKafkaEvent)
- Консюмеры (1): Consumer: News Events → Notifications (notifications-news-events)
- Дашборды (1): Панель Наблюдаемости: news-overview (эксплуатация)
- registries (1): Реестр событий PIN
Связанные документы
- Источник (операция):
publish-post.md(переход + публикация события); сущности:post.md(агрегат,PostAuthorType, статусная машина, INV-NWS-005),post-version.md(CategorySlug,VersionNumber,Reason),post-category.md,identity-replicas.md(реплика ФИО автора у потребителя). - Соседний контракт сервиса:
pin.news.comment-added.v1(отдельный топик/событие) — карта в README; эталон стиля:user-upserted.md. - ADR: ADR-0052 (tenancy), ADR-0054 (таймлайн → user-activity), ADR-0056 (масштаб).