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.
| Поле | Значение |
|---|
| Сервис/контекст | selections |
| API area | internal |
| Feature group | snapshots |
| Статус | draft |
| Документ | docs/06-services/selections/api/internal/create-selection-snapshot.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | ExternalSystem — m2m-клиент news (client-credentials), INV-SNP-003 |
| Auth policy / Permissions | authorization 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: контент публичного поста) |
| Audit | append-only snapshot: CreatedAt, OwnerUserId (копия), PostId, RequestedByUserId в логах |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/internal/selections/{id:guid}/snapshots |
| Success status | 201 (повторный вызов на ту же пару — 200 с существующим Active-snapshot-ом) |
| Idempotency header | не нужен: бизнес-идемпотентность по паре (PostId, SourceSelectionId, Status = Active) — шаг 1 алгоритма |
| Correlation | сквозной trace через заголовок traceparent, из news |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
SourceSelectionId | route ({id}) | Guid | Да | != Guid.Empty; существует и DeletedAt == null (INV-SNP-002) | — |
PostId | body | Guid | Да | != Guid.Empty; существование поста не проверяется (source of truth — news, доверенный m2m) | trusted (m2m) |
RequestedByUserId | body | Guid | Да | != Guid.Empty — автор поста, прокидывает news | аудит-поле, в ответ не возвращается |
Модель ответа SelectionSnapshotResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
SnapshotId | Guid | Да | SelectionSnapshot.Id | — | news сохраняет в typed-jsonb блоке поста. |
SourceSelectionId | Guid | Да | SelectionSnapshot.SourceSelectionId | — | Подборка-источник. |
PostId | Guid | Да | SelectionSnapshot.PostId | — | Echo. |
ItemsCount | int | Да | Content.Items.Count | — | Элементов в слепке (1..200). |
TakenAt | DateTimeOffset | Да | Content.TakenAt | — | Момент снятия слепка. |
Алгоритм
Канонический алгоритм снятия snapshot — ../domain/selection-snapshot.md,
раздел «Алгоритм». Сводка:
- Контекст и доступ. Доверенный m2m
news (INV-SNP-003). Идемпотентность: если на пару (PostId,
SourceSelectionId) уже есть snapshot в статусе Active, он возвращается как успех без изменений,
HTTP 200.
- Проверка входа и предусловий. Вызыватель —
news (INV-SNP-003), иначе 403; Guid-поля непустые.
Подборка-источник существует (иначе SELECTIONS_NOT_FOUND) и не удалена — DeletedAt == null
(INV-SNP-002), иначе SELECTIONS_SOURCE_DELETED; в ней ≥ 1 элемента (INV-SNP-004), иначе
SELECTIONS_SNAPSHOT_EMPTY.
- Что читаем. Подборка-источник целиком (с элементами, шаг 2); объекты недвижимости не проверяются
(INV-SEL-008).
- Бизнес-правила и маппинг. Собрать
SelectionSnapshotContent v1: Title/Description/
SourceSelectionPublicCode — из подборки; Items — её элементы в порядке SortOrder
(PropertyObjectId, SortOrder, Comment); TakenAt — момент снятия. Создать snapshot в статусе
Active, OwnerUserId — копия владельца подборки; фиксируется факт SelectionSnapshotCreatedEvent.
- Что меняется. Появляется запись snapshot-а; исходная подборка не изменяется (в одном
согласованном изменении).
- События. Интеграционные не публикуются (синхронный ответ
news; обоснование — домен snapshot-а).
- Результат.
SnapshotId, SourceSelectionId, PostId, число элементов слепка, момент снятия →
201 (или 200 из шага 1).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_ERROR | 400 | Пустые Guid-поля | «Некорректный запрос» | no retry |
SELECTIONS_NOT_FOUND | 404 | SourceSelectionId не существует | «Подборка не найдена» | no retry |
SELECTIONS_SOURCE_DELETED | 400 | Подборка soft-deleted (INV-SNP-002) | «Подборка удалена владельцем» | no retry |
SELECTIONS_SNAPSHOT_EMPTY | 400 | В подборке 0 элементов (INV-SNP-004) | «Пустую подборку нельзя добавить в пост» | no retry |
FORBIDDEN | 403 | Вызыватель не m2m news (INV-SNP-003) | — | no retry |
Сетевой сбой/5xx: news повторяет вызов — идемпотентность шага 1 гарантирует один Active-snapshot
на пару (PostId, SourceSelectionId).
Кросс-сервисный контракт (selections ↔ news)
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).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы