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

POST /api/v1/internal/selections/{id}/snapshots — Снять snapshot подборки для поста (m2m news)

Создание иммутабельной копии подборки (SelectionSnapshot) для блока-подборки поста news (INV-SEL-001). Синхронный internal-вызов при добавлении или замене блока в посте.

Назначение

Автор поста вставляет подборку в пост; news (проверив авторство поста и право пользователя на подборку) вызывает этот метод и получает SnapshotId, который сохраняет в typed-jsonb блоке поста. После публикации владелец подборки не может изменить содержимое блока — snapshot заморожен; замена — только блоком целиком (detach-selection-snapshot.md). Канонический дом полей и модели Content../domain/selection-snapshot.md.

Metadata

ПолеЗначение
Сервис/контекстselections
API areainternal
Feature groupsnapshots
Статусdraft
Документdocs/06-services/selections/api/internal/create-selection-snapshot.md

Актор и доступ

ПроверкаЗначение
Actor typeExternalSystem — m2m-клиент news (client-credentials), INV-SNP-003
Auth policy / Permissionsauthorization policy InternalNewsOnly (m2m-клиент = news); пользовательского пути к операции нет
Scope (RBAC)не применяется; право автора поста вставлять подборку проверяет news до вызова (assumption — будущий док docs/06-services/news/domain/post.md)
Record-levelне применяется: news — доверенный вызыватель; RequestedByUserId — аудит-параметр, не авторизация
Tenant isolationне применяется — вне tenant (ADR-0052 п.3: контент публичного поста)
Auditappend-only snapshot: CreatedAt, OwnerUserId (копия), PostId, RequestedByUserId в логах

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/internal/selections/{id:guid}/snapshots
Success status201 (повторный вызов на ту же пару — 200 с существующим Active-snapshot-ом)
Idempotency headerне нужен: бизнес-идемпотентность по паре (PostId, SourceSelectionId, Status = Active) — шаг 1 алгоритма
Correlationсквозной trace через заголовок traceparent, из news

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
SourceSelectionIdroute ({id})GuidДа!= Guid.Empty; существует и DeletedAt == null (INV-SNP-002)
PostIdbodyGuidДа!= Guid.Empty; существование поста не проверяется (source of truth — news, доверенный m2m)trusted (m2m)
RequestedByUserIdbodyGuidДа!= Guid.Empty — автор поста, прокидывает newsаудит-поле, в ответ не возвращается

Модель ответа SelectionSnapshotResult

ПолеТипОбязательностьИсточникMaskingОписание
SnapshotIdGuidДаSelectionSnapshot.Idnews сохраняет в typed-jsonb блоке поста.
SourceSelectionIdGuidДаSelectionSnapshot.SourceSelectionIdПодборка-источник.
PostIdGuidДаSelectionSnapshot.PostIdEcho.
ItemsCountintДаContent.Items.CountЭлементов в слепке (1..200).
TakenAtDateTimeOffsetДаContent.TakenAtМомент снятия слепка.

Алгоритм

Канонический алгоритм снятия snapshot — ../domain/selection-snapshot.md, раздел «Алгоритм». Сводка:

  1. Контекст и доступ. Доверенный m2m news (INV-SNP-003). Идемпотентность: если на пару (PostId, SourceSelectionId) уже есть snapshot в статусе Active, он возвращается как успех без изменений, HTTP 200.
  2. Проверка входа и предусловий. Вызыватель — news (INV-SNP-003), иначе 403; Guid-поля непустые. Подборка-источник существует (иначе SELECTIONS_NOT_FOUND) и не удалена — DeletedAt == null (INV-SNP-002), иначе SELECTIONS_SOURCE_DELETED; в ней ≥ 1 элемента (INV-SNP-004), иначе SELECTIONS_SNAPSHOT_EMPTY.
  3. Что читаем. Подборка-источник целиком (с элементами, шаг 2); объекты недвижимости не проверяются (INV-SEL-008).
  4. Бизнес-правила и маппинг. Собрать SelectionSnapshotContent v1: Title/Description/ SourceSelectionPublicCode — из подборки; Items — её элементы в порядке SortOrder (PropertyObjectId, SortOrder, Comment); TakenAt — момент снятия. Создать snapshot в статусе Active, OwnerUserId — копия владельца подборки; фиксируется факт SelectionSnapshotCreatedEvent.
  5. Что меняется. Появляется запись snapshot-а; исходная подборка не изменяется (в одном согласованном изменении).
  6. События. Интеграционные не публикуются (синхронный ответ news; обоснование — домен snapshot-а).
  7. Результат. SnapshotId, SourceSelectionId, PostId, число элементов слепка, момент снятия → 201 (или 200 из шага 1).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_ERROR400Пустые Guid-поля«Некорректный запрос»no retry
SELECTIONS_NOT_FOUND404SourceSelectionId не существует«Подборка не найдена»no retry
SELECTIONS_SOURCE_DELETED400Подборка soft-deleted (INV-SNP-002)«Подборка удалена владельцем»no retry
SELECTIONS_SNAPSHOT_EMPTY400В подборке 0 элементов (INV-SNP-004)«Пустую подборку нельзя добавить в пост»no retry
FORBIDDEN403Вызыватель не m2m news (INV-SNP-003)no retry

Сетевой сбой/5xx: news повторяет вызов — идемпотентность шага 1 гарантирует один Active-snapshot на пару (PostId, SourceSelectionId).

Кросс-сервисный контракт (selectionsnews)

  • selections ожидает от news: валидный m2m токен (policy InternalNewsOnly); PostId и RequestedByUserId (автор поста). news до вызова проверяет авторство поста и право пользователя вставлять эту подборку — selections этого не перепроверяет (INV-SNP-003, news — доверенный вызыватель). PostId не валидируется на существование (source of truth — news).
  • selections предоставляет news: SnapshotId для сохранения в typed-jsonb блоке поста плюс ItemsCount/TakenAt. Гарантия: на пару (PostId, SourceSelectionId) существует не более одного Active-snapshot (бизнес-идемпотентность) — повторный вызов при retry возвращает тот же SnapshotId с 200, дубля не создаёт.
  • Порядок в сценарии замены блока: news оркестрирует «снять новый snapshot → переключить блок → отвязать прежний» (detach-selection-snapshot.md); этот метод — первый шаг цепочки.

Предусловия и постусловия

  • Предусловия: вызыватель — доверенный m2m news; подборка-источник существует, не удалена (DeletedAt == null, INV-SNP-002/INV-SEL-009) и содержит 1..200 элементов (INV-SNP-004).
  • Постусловия при 201: создан новый SelectionSnapshot в статусе Active с иммутабельным Content (ContractVersion = 1) — копией состава/порядка/комментариев подборки на момент снятия (INV-SEL-001); OwnerUserId скопирован; зафиксирован доменный факт SelectionSnapshotCreatedEvent; исходная подборка не изменена; интеграционные события не публикуются.
  • Постусловия при 200 (идемпотентный повтор): возвращён существующий Active-snapshot пары, новых записей и фактов нет.
  • Постусловия при отказе (400/403/404): snapshot не создан, состояние не изменено.

Acceptance Criteria

  • Given активная подборка с 3 элементами, When news впервые снимает snapshot для PostId, Then 201 с SnapshotId, ItemsCount = 3, TakenAt; snapshot в статусе Active, Content заморожен.
  • Given snapshot этой пары (PostId, SourceSelectionId) уже существует и Active, When news повторяет вызов (retry), Then 200 с тем же SnapshotId, второй snapshot не создаётся.
  • Given подборка-источник soft-deleted, When news снимает snapshot, Then 400 SELECTIONS_SOURCE_DELETED (INV-SNP-002).
  • Given подборка без элементов, When news снимает snapshot, Then 400 SELECTIONS_SNAPSHOT_EMPTY (INV-SNP-004).
  • Given SourceSelectionId, которого нет, When снятие, Then 404 SELECTIONS_NOT_FOUND.
  • Given вызыватель не news, When снятие, Then 403 FORBIDDEN (INV-SNP-003).
  • Given пустые Guid-поля, When снятие, Then 400 VALIDATION_ERROR.
  • Given подборку после снятия snapshot-а переименовали, When пост рендерится, Then он показывает состав на момент снятия (правка живой подборки snapshot не меняет, INV-SEL-001).

Совместимость и наблюдаемость

  • Изменения SelectionSnapshotContent — additive в рамках ContractVersion = 1; breaking — новый ContractVersion (правила — домен snapshot-а).
  • Logs: SnapshotId, SourceSelectionId, PostId, RequestedByUserId, ItemsCount; metrics: selections_snapshot_created_total{idempotent_hit}; traces: сквозной span из news. Alert: рост 5xx internal-области блокирует публикации news (../README.md).

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

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

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