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

DELETE /api/v1/selections/{id} — Удалить подборку (soft delete)

Мягкое удаление подборки владельцем: DeletedAt := now. Строки и связи сохраняются — snapshot-ы постов и ссылки CRM остаются разрешимыми; публичная ссылка и списки перестают её отдавать (INV-SEL-009).

Назначение

Владелец удаляет подборку. Физическое удаление невозможно by design: на SelectionId ссылаются CRM (лид/сделка) и SelectionSnapshot (FK restrict). Дом полей — ../domain/selection.md.

Metadata

ПолеЗначение
Сервис/контекстselections
API areapublic
Feature groupselections
Статусdraft
Документdocs/06-services/selections/api/public/delete-selection.md

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

ПроверкаЗначение
Actor typeUser (JWT)
Auth policy / Permissionsauthenticated user; отдельных permissions нет (self-scope)
Scope (RBAC)self
Record-levelOwnerUserId == actor.UserId в предикате загрузки (INV-SEL-002)
Tenant isolationне применяется — вне tenant (ADR-0052 п.2)
Auditdelete: DeletedAt + SelectionDeletedKafkaEventuser-activity; восстановление — runbook (ручное снятие DeletedAt, ../README.md)

HTTP-контракт

ПолеЗначение
MethodDELETE
Route/api/v1/selections/{id:guid}
Success status204
Idempotency headerне применяется; повторный DELETE возвращает 404 (после soft delete запись невидима — INV-SEL-009), фронт трактует 404 после первого 204 как успех
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
IdrouteGuidДа!= Guid.Emptyrecord-level фильтр по владельцу
UserIdidentity claims (sub)GuidДаRequired

Body отсутствует.

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

Отсутствует — 204 No Content.

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь; удалять можно только собственную подборку (по владельцу-актору).
  2. Проверка входа и предусловий. Id непустой — иначе VALIDATION_ERROR. Подборка должна существовать, принадлежать актору и быть не удалённой (DeletedAt == null); иначе — единый отказ SELECTIONS_NOT_FOUND (чужое существование не раскрывается, INV-SEL-002/009).
  3. Бизнес-правила и переходы. Переход Active → Deleted (INV-SEL-002): проставляются DeletedAt и UpdatedAt, фиксируется факт удаления. Snapshot-ы постов не отвязываются (restrict, INV-SNP-006): пост news продолжает показывать замороженную копию; живая ссылка /collection/{publicCode} начинает отвечать 404 (INV-SEL-009).
  4. Что меняется. Подборка помечается удалённой (soft delete — физического удаления нет); элементы остаются под удалённым корнем. Изменение и факт удаления фиксируются в одном согласованном изменении. Конфликт с параллельной правкой → SELECTIONS_CONCURRENCY_CONFLICT (409); фронт повторяет DELETE (попадёт в 404-ветку, если первый запрос успел).
  5. Результат. 204 No Content.

Публикуемое интеграционное событие — в разделе «Побочные эффекты».

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияSelectionDeletedKafkaEvent / pin.selections.selection-deleted.v1 / key = SelectionId
Проекции / поискпубличная ссылка /collection/{publicCode} → 404 с момента коммита (partial-index исключает строку); snapshot-ы постов живут (INV-SNP-006); новые snapshot-ы из удалённой запрещены (INV-SNP-002)

Интеграционные последствия (что видят соседи после удаления)

Удаление меняет поведение сразу нескольких границ; порядок и окно рассинхронизации важны для UX:

  1. Публичная ссылка (синхронно): с момента коммита GET /selections/{publicCode} отвечает 404 (partial-index ix_selections_public_code_active исключает строку) — INV-SEL-009. Разосланные ранее ссылки перестают открываться немедленно.
  2. CRM (синхронно, pull): ссылка в лиде/сделке остаётся разрешимой — crm читает подборку через internal GET /internal/selections/{id} (get-selection.md), который для soft-deleted резолвит запись с IsDeleted = true (не 404). CRM показывает «подборка недоступна/удалена», не теряя контекст лида. Событие selection-deleted для этого не обязано дойти — состояние виден по флагу при следующем чтении.
  3. Посты news (не затрагиваются): уже снятые SelectionSnapshot не отвязываются (FK restrict, INV-SNP-006) — опубликованный пост продолжает показывать замороженную копию состава. Снять новый snapshot из удалённой подборки нельзя (INV-SNP-002 → SELECTIONS_SOURCE_DELETED).
  4. История действий (асинхронно, через outbox): selection-deleteduser-activity фиксирует ActionEvent «удалил подборку» (окно eventual-consistency — секунды). Потребитель идемпотентен по SelectionId; при простое шины подборка уже удалена и невидима публично, отстаёт только лента действий.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_ERROR400Id == Guid.Empty«Некорректный идентификатор»no retry
SELECTIONS_NOT_FOUND404Нет записи / чужая (INV-SEL-002) / уже удалена (повтор DELETE)«Подборка не найдена»no retry
SELECTIONS_CONCURRENCY_CONFLICT409Конфликт оптимистической блокировки«Подборка изменена параллельно»retry (один раз)
UNAUTHORIZED401Нет/просрочен access-токенretry после refresh

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

  • Предусловия: актор авторизован (валидный JWT); подборка существует, принадлежит актору (INV-SEL-002) и ещё не удалена (DeletedAt == null).
  • Постусловия при 204: у подборки проставлен DeletedAt (переход Active → Deleted), UpdatedAt продвинут; строки подборки и элементов физически сохранены (soft delete); публичное чтение по коду отвечает 404 (INV-SEL-009); ссылки CRM и snapshot-ы постов остаются разрешимыми (INV-SNP-006); счётчик не удалённых подборок владельца уменьшился на 1 (освобождает место под лимит INV-SEL-007); в outbox поставлено selection-deleted (key = SelectionId).
  • Постусловия при отказе (400/401/404/409): статус подборки не изменён, событий нет.

Acceptance Criteria

  • Given не удалённая подборка владельца, When DELETE, Then 204; повторный GET /selections/{publicCode}404 (INV-SEL-009); поставлено selection-deleted (key = SelectionId).
  • Given только что удалённая подборка, When повторный DELETE, Then 404 SELECTIONS_NOT_FOUND (запись уже невидима); фронт трактует 404 после первого 204 как успех (идемпотентность на клиенте).
  • Given подборка, на которую в лиде ссылается CRM, When владелец её удалил, Then ссылка в CRM остаётся разрешимой: GET /internal/selections/{id} вернёт 200 с IsDeleted = true — CRM покажет «подборка недоступна».
  • Given подборка с уже снятым snapshot-ом поста, When владелец её удалил, Then пост news продолжает показывать замороженный snapshot (INV-SNP-006), а попытка снять новый snapshot из неё → 400 SELECTIONS_SOURCE_DELETED (INV-SNP-002).
  • Given чужая или несуществующая подборка, When DELETE, Then 404 SELECTIONS_NOT_FOUND (существование чужой не раскрывается, INV-SEL-002).
  • Given параллельные DELETE и правка состава, When обе доходят, Then удаление и правка конкурируют по оптимистической блокировке: победитель фиксируется, проигравший → 409 SELECTIONS_CONCURRENCY_CONFLICT; фронт повторяет DELETE (попадёт в 404-ветку, если удаление уже прошло).
  • Given Id == Guid.Empty, When DELETE, Then 400 VALIDATION_ERROR.
  • Given отсутствующий/просроченный токен, When DELETE, Then 401 UNAUTHORIZED.

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

  • Контракт стабилен; undelete-операции в API нет (runbook — ручное восстановление).
  • Logs: UserId, SelectionId; metrics: selections_deleted_total; traces: span DeleteSelectionCommand. Dashboard — selections-overview; runbook — восстановление случайно удалённой подборки (../README.md).

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

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

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