Перейти к основному содержимому

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 typepin.news.post-published
Topicpin.news.post-published.v1
Partitions / key3 / key = PostId (= AggregateId; порядок публикаций per-post)
ContractVersion1
Producernews: публикация/переопубликация поста (дом publish-post.md)
Статусdraft

Событие публикуется атомарно с переходом Post → Published и захватом слепка PostVersion (INV-NWS-005 / INV-PV-003): до фиксации изменения событие не отправляется, после — доставляется гарантированно (at-least-once). Kafka key = PostId — публикации одного поста упорядочены.

Payload PostPublishedKafkaEvent

Snapshot-поля ссылаются на дом post.mdpost-version.md для замороженного CategorySlug); здесь перечислено только то, что реально в событии. Тело блоков (PostContentDocument) не входит — потребитель, которому нужен контент, читает пост по PostId/Suffix через public API/рендер.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
PostIdGuidДаPost.IdImmutableКлюч агрегата (= Kafka key).
AuthorUserIdGuidДаPost.AuthorUserIdImmutableАвтор поста. ФИО не в payload — из реплики identity у потребителя.
AuthorTypestring (enum PostAuthorType)ДаPost.AuthorTypeAdditive only (новые значения)Investor / DeveloperManager / Agent / AgencyManager / PinEmployee; snapshot identity на момент создания.
AuthorCompanyIdGuid?НетPost.AuthorCompanyIdAdditive onlyКомпания застройщика/агентства (ADR-0052); null — инвестор/сотрудник.
CategoryIdGuidДаPost.CategoryIdImmutableКатегория поста (post-category.md).
CategorySlugstringДаPostVersion.Snapshot.CategorySlugAdditive onlyЧеловекочитаемый slug на момент публикации (заморожен в слепке — переименование категории позже не влияет).
TitlestringДаPost.TitleAdditive onlyЗаголовок поста (публичный display).
Subtitlestring?НетPost.SubtitleAdditive onlyПодзаголовок.
SuffixstringДаPost.SuffixImmutableURL-slug (kebab-case), база /news/{suffix} (INV-NWS-001).
CoverFileIdGuid?НетPost.CoverFileIdAdditive onlyОбложка (storage/CDN, без FK; пресайн-ссылку строит потребитель).
ProjectRefsIReadOnlyList<Guid>Да (может быть пустым)Post.ProjectRefs.ProjectIdsAdditive onlyПроекты застройщика; пусто для не-DeveloperManager.
PublishedAtDateTimeOffsetДаPost.PublishedAtImmutable semanticsМомент текущей публикации (тайминг/сортировка у потребителя).
VersionNumberintДаPost.CurrentVersionNumber (после инкремента) = PostVersion.VersionNumberImmutable semanticsМонотонный номер версии в пределах поста; бизнес-ключ идемпотентности user-activity.
IsFirstPublishboolДаPost.FirstPublishedAt == Post.PublishedAt (⇔ PostVersion.Reason = Published)Additive onlytrue — первая публикация (Draft → Published); false — republish.

PostId — immutable ключ агрегата; AuthorUserId/AuthorType/AuthorCompanyId/CategoryId/Suffix — иммутабельны по семантике поста (правки роли/атрибуции пост не меняют). Остальные поля переиздаются при каждом republish с новым VersionNumber.

Когда публикуется

УсловиеПереход состоянияСогласованность
Первичная публикация (Reason = Published, IsFirstPublish = true; выставляет PublishedAt и FirstPublishedAt)Draft → PublishedPost, 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 по собственному стору подписок notificationsAuthorSubscription из 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, IsFirstPublishPublicinclude (метаданные опубликованного поста публичны)
Title, Subtitle, Suffix, CategorySlug, CoverFileIdPublicinclude (заголовок/подпись/обложка/slug публикуются в ленте)
ProjectRefsPublicinclude (публичные привязки проектов застройщика)

Все поля — Public: событие фиксирует уже опубликованный (общедоступный) контент. PII нет — ФИО автора в payload не входит (берётся потребителем из реплики identity). Событие не содержит тела блоков (PostContentDocument), содержимого подборок, secrets, tokens, платёжных данных, unbounded вложений.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • Источник (операция): 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 (масштаб).