UC-NWS-001. Создать черновик и опубликовать пост
Назначение
Сквозной сценарий авторинга ленты новостей: автор создаёт черновик поста, наполняет его типизированными
блоками (текст / галерея / embed-подборка) и публикует пост в общую и подписную ленты. Каждая публикация
фиксирует полный иммутабельный snapshot поста отдельной версией PostVersion
(полный слепок поста, а не diff) и порождает интеграционное событие post-published для рассылки
подписчикам. Связан с контекстом news; алгоритмы отдельных операций живут в api-документах области
authoring (ссылки в шагах).
| Поле | Значение |
|---|
| ID | UC-NWS-001 |
| Контекст | news (см. docs/06-services/news/); интеграции — selections (embed-snapshot, m2m), notifications/user-activity (событие) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|
| Роль | Автор — Investor / Agent / AgencyManager / DeveloperManager / PinEmployee (PostAuthorType, дом — ../domain/post-category.md) |
| Permissions / scope | news.posts.publish (eligibility публикации, INV-NWS-002); каталог разбивает по scope — news.posts.create-personal (self: Investor/Agent/PinEmployee), create-company (AgencyManager/DeveloperManager, scope AuthorCompanyId), create-project (DeveloperManager + ProjectRefs), manage-own — identity/domain/permission.md |
| Tenant | TenantId = AuthorCompanyId; null для Investor/Agent/PinEmployee (ADR-0052 п.4) |
| Record-level | мутации — только AuthorUserId, либо менеджер компании в scope AuthorCompanyId, либо permission news.posts.manage (INV-NWS-007) |
Предусловие
- Автор аутентифицирован;
AuthorType = snapshot типа пользователя на момент создания.
- Целевая категория существует и
Status = Active; AuthorType ∈ Category.AllowedAuthorTypes (INV-NWS-002 / INV-CAT-002, ../domain/post-category.md).
- Для embed-блоков доступен
selections (иначе добавление блока-подборки блокируется — ../README.md, «Зависимости»).
Триггер
| Поле | Значение |
|---|
| Источник | UI (редактор поста news) → API calls |
| Триггерящий элемент | Кнопка «Создать пост» → редактор блоков → «Опубликовать» (area authoring) |
Основной Поток
- Создать черновик.
POST /api/v1/posts с телом { name, suffix, title, subtitle?, categoryId, authorCompanyId?/projectRefs? } (последние два — только для застройщика). Система проверяет eligibility
по типу автора (INV-NWS-002), уникальность suffix (kebab-case, INV-NWS-001) и attribution
(INV-NWS-003) и создаёт Post в Status = Draft (CurrentVersionNumber = 0,
пустой Content). Алгоритм — ../api/authoring/create-post-draft.md.
Ответ — { PostId, Suffix, Status = Draft }.
- Наполнить блоки.
PUT /api/v1/posts/{id}/blocks заменяет весь PostContentDocument (typed-jsonb):
блоки Text / ImageGallery / SelectionEmbed. Для каждого нового embed-блока news синхронно
снимает иммутабельную копию подборки в selections (m2m) и сохраняет SnapshotId в блоке
(UC-SEL-004); пересчитывается
SearchText — источник Elasticsearch-проекции поиска (INV-NWS-008). Алгоритм —
../api/authoring/set-post-blocks.md.
- Публикация.
POST /api/v1/posts/{id}/publish. Проверка INV-NWS-004: непустой Title ∧ >= 1 блок ∧
все SelectionEmbed-блоки ссылаются на Active-snapshot. В одном согласованном изменении: переход
Draft → Published; append-only PostVersion (VersionNumber = 1,
Reason = Published) с полным слепком PostSnapshotDocument (INV-NWS-005 / INV-PV-002/003);
Status = Published, PublishedAt = FirstPublishedAt = now; порождается PostPublishedKafkaEvent.
Алгоритм — ../api/authoring/publish-post.md. Событие
pin.news.post-published.v1 доставляется в notifications (подписчики автора) и user-activity
(таймлайн автора, опц., ADR-0054).
Альтернативные Потоки
A1. Повторная публикация после правок (republish)
- Опубликованный пост правится (
PUT /posts/{id}/blocks или метаданные) — остаётся Published; версий
правки черновика не создают (INV-PV-004).
- Повторный
POST /posts/{id}/publish: переход Published/Unpublished → Published, пишется новый
PostVersion (VersionNumber = CurrentVersionNumber + 1, Reason = Republished) полным snapshot-ом;
PublishedAt = now, FirstPublishedAt не меняется; порождается новое post-published. Возврат к
основному потоку на шаг 3.
A2. Embed-snapshot недоступен при публикации
- На шаге 3
SelectionEmbed-блок ссылается на не-Active/отсутствующий snapshot (проверка статуса в
selections, INV-NWS-004) → ошибка NEWS_SNAPSHOT_INVALID (422); пост остаётся Draft.
- Клиент предлагает автору обновить блок-подборку (замена целиком — новый snapshot, INV-SEL-001). Если
недоступен сам сервис
selections — NEWS_SELECTIONS_UNAVAILABLE (503, retry). Публикация повторяется
после исправления (возврат на шаг 2–3).
A3. Категория не разрешена типу автора
- На шаге 1 (создание) или шаге 3 (публикация)
AuthorType ∉ Category.AllowedAuthorTypes ∨ нет
news.posts.publish (INV-NWS-002 / INV-CAT-002) → ошибка NEWS_CATEGORY_NOT_ALLOWED (403).
- Клиент показывает eligibility-фильтр категорий под тип автора (
GET /api/v1/categories); автор
выбирает разрешённую категорию. Возврат к основному потоку на шаг 1.
A4. Конкурентная публикация / republish (version-guard)
- Два параллельных
POST /posts/{id}/publish (либо publish во время правки блоков) претендуют на один
пост. VersionNumber монотонен в пределах поста и присваивается в согласованном изменении — две версии с
одинаковым номером возникнуть не могут: побеждает первое зафиксированное изменение.
- Проигравший видит конфликт версий (
CONCURRENCY_CONFLICT, 409) и повторяет операцию поверх уже
обновлённого состояния — при повторе пишется следующая версия (Reason = Republished,
VersionNumber + 1) и переиздаётся post-published. «Потерянных» публикаций и дублей PostVersion с
одним номером не возникает. Возврат к основному потоку на шаг 3.
Постусловие
- Пост в
Status = Published: попадает в ленты global и subscriptions и в Elasticsearch-проекцию
поиска news-posts (upsert по PostVersionCapturedEvent, INV-NWS-008, search/posts.md).
- Зафиксирована append-only версия
PostVersion (VersionNumber = 1, Reason = Published) с полным
иммутабельным слепком поста; embed-блоки заморожены по SnapshotId (цепочка иммутабельности).
PublishedAt/FirstPublishedAt заданы; событие pin.news.post-published.v1 порождено (доставка
подписчикам — notifications).
- Видимость по каналам различается по свежести: в лентах
global/subscription (PostgreSQL-keyset) и по
прямой ссылке пост виден сразу после коммита; в поиске news-posts — после применения upsert проекции
(eventual, секунды); подписчики уведомляются асинхронно (notifications, < 1 мин, at-least-once). Republish
переиздаёт эти проекции с новым VersionNumber.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|
VALIDATION_FAILED | 400 | формат Name/Suffix/Title, границы блоков, дубли BlockId | исправить поля — не ретраить |
NEWS_SUFFIX_TAKEN | 409 | Suffix занят (INV-NWS-001, шаг 1) | предложить другой суффикс |
NEWS_CATEGORY_NOT_FOUND | 404 | категория не найдена / Archived (шаг 1) | выбрать активную категорию |
NEWS_CATEGORY_NOT_ALLOWED | 403 | AuthorType ∉ AllowedAuthorTypes ∨ нет news.posts.publish (A3, INV-NWS-002) | показать eligibility-фильтр категорий |
NEWS_ATTRIBUTION_NOT_ALLOWED | 403 | AuthorCompanyId/ProjectRefs не по типу автора/scope (INV-NWS-003) | убрать недопустимую привязку |
NEWS_POST_EMPTY | 422 | пустой Title ∨ 0 блоков при публикации (INV-NWS-004) | заполнить заголовок и добавить блок |
NEWS_SNAPSHOT_INVALID | 422 | embed-блок ссылается на не-Active/отсутствующий snapshot (A2, INV-NWS-004) | обновить блок-подборку |
NEWS_SELECTIONS_UNAVAILABLE | 503 | selections недоступен при снятии/проверке snapshot (A2) | повторить позже (retry) |
403 (record-level) | 403 | не автор / не менеджер scope / нет news.posts.manage (INV-NWS-007) | скрыть действие |
NEWS_POST_NOT_FOUND | 404 | пост не найден / Archived (шаг 2–3) | обновить список |
CONCURRENCY_CONFLICT | 409 | конкурентная публикация/republish одного поста (A4, version-guard) | повторить публикацию поверх обновлённого состояния |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|
PostContentChangedEvent | create (шаг 1) / set-blocks (шаг 2) | внутренний пересчёт SearchText в том же изменении (INV-NWS-008); для Published-поста — upsert Elasticsearch-проекции поиска |
PostVersionCapturedEvent | захват версии при → Published (шаг 3 / A1) | внутренние подписчики (метрики/аудит истории версий) |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|
PostPublishedKafkaEvent | pin.news.post-published.v1 | Publish/Republish (→ Published), атомарно с переходом; ContractVersion = 1, key = PostId | notifications (подписчики автора); user-activity (таймлайн автора, опц.) |
Payload: { PostId, AuthorUserId, AuthorType, AuthorCompanyId, CategoryId, CategorySlug, Suffix, Title, Subtitle, CoverFileId, ProjectRefs, PublishedAt, VersionNumber, IsFirstPublish } (полный контракт, типы и
семантика полей — дом ../events/post-published.md; ФИО автора в payload не
входит — берётся потребителем из реплики identity). Черновик и set-blocks событий не порождают.
Acceptance Criteria
| # | Критерий | Проверка |
|---|
| AC1 | Eligibility соблюдается: автор публикует только в категорию своего AuthorType (INV-NWS-002); иначе NEWS_CATEGORY_NOT_ALLOWED | негативный тест A3 по каждому типу автора |
| AC2 | Каждая публикация пишет полный PostVersion-snapshot (не diff), атомарно с переходом (INV-NWS-005 / INV-PV-003) | интеграционный тест: слепок содержит все поля поста; правка Draft версий не создаёт |
| AC3 | Событие post-published идемпотентно: доставка at-least-once, консюмер дедуплицирует по (PostId, VersionNumber) — повторная доставка не даёт дублей уведомлений | тест повторной доставки |
| AC4 | Republish (A1) создаёт новый PostVersion (Reason = Republished, VersionNumber++); FirstPublishedAt неизменен | сквозной тест правка → republish |
| AC5 | Suffix уникален среди неудалённых (INV-NWS-001) — гонку снимает ux_posts_suffix | конкурентный тест create |
| AC6 | Публикация блокируется при пустом Title / 0 блоков / не-Active-snapshot (INV-NWS-004) → NEWS_POST_EMPTY / NEWS_SNAPSHOT_INVALID | негативные тесты шага 3 |
| AC7 | Конкурентная публикация (A4): два параллельных publish не создают две версии с одним VersionNumber; проигравший получает CONCURRENCY_CONFLICT, при повторе пишет следующий номер | конкурентный тест publish/republish |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные Документы
- API-методы (дома контрактов и алгоритмов): ../api/authoring/create-post-draft.md,
../api/authoring/set-post-blocks.md,
../api/authoring/publish-post.md.
- Домены: ../domain/post.md (агрегат, INV-NWS-001/002/003/004/005/007/008, статусная
машина), ../domain/post-version.md (снимок, INV-PV-*),
../domain/post-block.md (typed-jsonb блоки),
../domain/post-category.md (
AllowedAuthorTypes, PostAuthorType).
- Событие: ../events/post-published.md (
pin.news.post-published.v1).
- Смежный сценарий embed-snapshot: UC-SEL-004;
дом копии — ../../selections/domain/selection-snapshot.md.
- Permission:
identity/domain/permission.md; контекст — ../README.md.
- ADR: ADR-0052 (tenancy),
ADR-0054 (view/activity),
ADR-0056 (simplicity-first),
ADR-0059 (Elasticsearch-проекция поиска).