UC-NWS-006. История версий поста (полный snapshot)
Назначение
Автор просматривает полную неизменяемую историю публикаций своего поста — список версий и полный snapshot
конкретной версии. Версионирование — полный слепок каждой публикации (не diff), блочный контент.
Связан с контекстом news (лента новостей). Дом слепка — post-version.md, дом
агрегата — post.md.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-NWS-006 |
| Контекст | news (см. docs/06-services/news/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Автор (User): автор поста либо менеджер компании-автора (застройщик/агентство) |
| Permissions / scope | news.posts.manage-own (свои посты) + news.posts.manage (посты компаний в scope менеджера); чтение истории — не public (post-version.md, «Permissions»); каталог — permission.md |
| Tenant | TenantId = AuthorCompanyId (nullable — пост инвестора/сотрудника вне tenant, ADR-0052 п.4); история читается кросс-tenant предикатом «свои ∪ компанийные» |
| Record-level | Post.AuthorUserId == actor ИЛИ (Post.AuthorCompanyId ∈ managed-scope ∧ news.posts.manage), INV-NWS-007 (post.md); иначе 403 |
Предусловие
- Пост существует (
news.posts) и доступен актору по record-level (INV-NWS-007). - Актор аутентифицирован; гость →
401. - Пост публиковался хотя бы раз (
Post.CurrentVersionNumber >= 1) — иначе история пуста (успех, не ошибка): черновик без публикаций версий не создаёт (INV-PV-004, post-version.md).
Триггер
| Поле | Значение |
|---|---|
| Источник | API call (GET) |
| Триггерящий элемент | Вкладка «История версий» карточки поста автора (area authoring, README) |
Основной Поток
- Автор открывает карточку своего поста → вкладку «История версий»; фронт вызывает
GET /api/v1/posts/{id}/versions(keyset-пагинация). - Доступ и загрузка поста. Определить актора и его
managedCompanyIds(компании сnews.posts.manage); загрузить пост на платформенном уровне, включая снятые с публикации и архивные (автор видит историю снятых/архивных постов). Пост не найден →NEWS_POST_NOT_FOUND(404); record-level предикат (INV-NWS-007) ложен →403(A2). - Список версий. Отобрать
PostVersionпоPostId, порядокVersionNumber desc(индексix_post_versions_post, keysetLimit + 1). Маппинг поле-в-поле вPostVersionListItem(VersionNumber/Reason(enum-строкой)/CreatedByUserId+CreatedByDisplayNameизAuthorReplica, identity-replicas.md/CapturedAt) →200. - Автор выбирает версию
n→ фронтGET /api/v1/posts/{id}/versions/{versionNumber}. - Загрузка версии. Тот же record-level предикат; найти версию по (
PostId,VersionNumber == n) (uniqueux_post_versions_number); не найдена →NEWS_VERSION_NOT_FOUND(404, A1). - Маппинг детали.
PostVersionDetail(PostId/VersionNumber/Reason/CreatedByUserId/CapturedAtSnapshot = PostSnapshotDocument— все поля + блокиContent, post-version.md). Иммутабельный слепок (INV-PV-001) отдаётся как есть →200.
- Фронт рендерит историческую версию:
Text/ImageGallery— из слепка;SelectionEmbed-блоки — из иммутабельногоSelectionSnapshotпоSnapshotId(selectionsGET snapshot; читаем даже приStatus = Detached— INV-SNP-006, selection-snapshot.md) → исторический состав подборки; display-данные объектов недвижимости — актуальные батчем изcatalogпоPropertyObjectId(не заморожены, решение №3 README).
Диаграмма
Альтернативные Потоки
A1. Запрошенная версия отсутствует
GET /api/v1/posts/{id}/versions/{n}с несуществующимn(илиn > CurrentVersionNumber).- Версия по (
PostId,VersionNumber == n) не найдена →NEWS_VERSION_NOT_FOUND(404).
Завершается: фронт показывает «Версия не найдена», список версий (шаг 3) остаётся источником валидных n.
A2. Актор не автор и не менеджер scope
- Актор запрашивает историю чужого поста (
AuthorUserId != actor∧AuthorCompanyId ∉ managed-scope). - Record-level предикат ложен (INV-NWS-007) →
403.
Завершается: история не раскрывается (чтение истории — не public, post-version.md).
A3. Недоступность зависимостей при рендере исторической версии
- Рендер версии (шаг 7) добирает исторический состав embed из
selections(GET snapshot поSnapshotId) и актуальные display-данные объектов недвижимости изcatalog(батч поPropertyObjectId) — обе зависимости синхронные и внешние для чтения. - Отказ
catalog→ embed деградирует до исторического состава/порядка/комментариев из snapshot-а без живых цены/статуса (повтор при следующем рендере). Отказ/таймаутselections→ исторический состав embed временно не резолвится, но самPostVersionDetail(текст/галереи из иммутабельногоPostSnapshotDocument, INV-PV-001) отдаётся штатно — от внешних сервисов зависит только рендер embed-блоков.
Завершается: деградация чтения, не бизнес-ошибка (мутаций и событий нет; 200 с частично догидрированным
рендером).
- Система не изменена: мутаций, доменных/интеграционных событий нет.
- Актору доступна полная неизменяемая история публикаций поста: список версий (
VersionNumber desc) и полный иммутабельныйPostSnapshotDocumentлюбой версии, включая исторический состав embed-подборок.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
UNAUTHENTICATED | 401 | Нет/невалидный токен (гость) | Логин и повтор |
VALIDATION_ERROR | 400 | versionNumber < 1; повреждён keyset-cursor; limit вне 1..30 | Исправить параметры |
NEWS_POST_NOT_FOUND | 404 | Пост отсутствует | Вернуться к списку постов |
NEWS_VERSION_NOT_FOUND | 404 | Версия n не существует (A1) | Показать «версия не найдена»; свериться со списком |
403 (record-level) | 403 | Актор не автор и не менеджер scope (A2, INV-NWS-007) | Скрыть вкладку истории |
Отказ БД — инфраструктурная ошибка (5xx, ProblemDetails), а не бизнес-ошибка домена. Пустая история
(пост без публикаций) — успех 200 с пустым Items.
События
Сценарий событий не порождает (read-only, два safe-GET). Захват версии (PostVersionCapturedEvent) поднимает
агрегат Post в момент публикации (publish-post.md, INV-NWS-005), а
не при чтении истории; внешние подписчики читают событие публикации pin.news.post-published.v1
(post.md), не историю версий.
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Каждая публикация = отдельная строка PostVersion (полный snapshot, не diff); список отдаёт по одной записи на публикацию, VersionNumber монотонен | Опубликовать → править → republish; GET /versions вернёт 2 записи VersionNumber 1,2 в порядке desc |
| AC2 | Snapshot иммутабелен: повторное чтение той же версии даёт идентичный PostSnapshotDocument (INV-PV-001; мутатора нет) | Прочитать версию n дважды с правкой поста между чтениями — тело неизменно |
| AC3 | Исторические embed читаемы даже после detach (INV-SNP-006): версия со SnapshotId в статусе Detached рендерит исторический состав | Заменить блок-подборку → republish → открыть старую версию → snapshot резолвится, состав прежний |
| AC4 | Display-данные объектов недвижимости embed — актуальные из catalog, не заморожены | Изменить цену объекта недвижимости в catalog → старая версия показывает новую цену при прежнем составе |
| AC5 | Чужой актор → 403; отсутствующая версия → NEWS_VERSION_NOT_FOUND (не раскрытие) | Тесты record-level изоляции (INV-NWS-007) + запрос n = CurrentVersionNumber + 1 |
| AC6 | Недоступность зависимостей рендера (A3): при отказе catalog/selections PostVersionDetail из иммутабельного snapshot отдаётся штатно, деградирует только рендер embed | Тест: недоступный catalog/selections → 200, текст/галереи версии на месте |
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы (карта — README, area
authoring; целевые пути по конвенции):list-post-versions.md(GET /posts/{id}/versions),get-post-version.md(GET /posts/{id}/versions/{n}). - Domain entity: post-version.md (слепок
PostSnapshotDocument, INV-PV-001/002/004,PostVersionReason, permissions), post-block.md (PostContentDocument,SelectionEmbedBlock), post.md (агрегат, INV-NWS-005/007,CurrentVersionNumber), identity-replicas.md (CreatedByDisplayName). - Цепочка иммутабельности: selection-snapshot.md
(INV-SNP-006,
Detached); рендер объектов недвижимости —catalogпоPropertyObjectId(не заморожено). - Захват версии: publish-post.md (INV-NWS-005); аналог read-эндпоинта — list-my-posts.md; публичное чтение — get-post.md.
- Доступ: permission.md (
news.posts.manage-own/manage); ADR-0052 (tenancy).