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

DELETE /api/v1/subscriptions/{id} — Отписаться от источника

Назначение

Отписка пользователя-подписчика от источника новостей (автора или компании) в сервисе news. Отписка — soft-delete записи AuthorSubscription (DeletedAt = now): цель исчезает из выборки целей ленты subscription, история сохраняется для аналитики оттока, повторная подписка reactivate-ит ту же строку (author-subscription.md, переходы Active ↔ Deleted). Операция идемпотентна: повторная отписка (или отписка уже soft-удалённой строки) — no-op, тот же 204.

Metadata

ПолеЗначение
Сервис/контекстnews
API areapublic
Feature groupsubscriptions
Статусdraft
Документdocs/06-services/news/api/public/subscriptions/unsubscribe.md

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

ПроверкаЗначение
Actor typeUser (аутентифицированный подписчик, JWT identity)
Auth policy / PermissionsJWT обязателен; отдельного permission нет — self-scope подписок (README)
Record-levelвладелец-подписчик: subscription.SubscriberUserId == actor.SubscriberUserId (INV-SUB-004) → иначе 403
Tenant isolationне применяется — вне tenant; изоляция по SubscriberUserId из JWT, не по TenantId
Auditdelete (soft): DeletedByUserId = SubscriberUserId, DeletedAt/UpdatedAt

HTTP-контракт

ПолеЗначение
MethodDELETE
Route/api/v1/subscriptions/{id}
Success status204 No Content (в т.ч. идемпотентный повтор — уже отписан)
Idempotencyоперация естественно идемпотентна (повтор = no-op, шаг 2)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
IdrouteGuidДаId != Guid.Empty → иначе VALIDATION_FAILED
SubscriberUserIdidentity claims (JWT)GuidДаRequired; берётся из контекста актора, не из тела (INV-SUB-004)record-level owner check

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

Успех возвращает 204 No Content без тела. Ошибки — ProblemDetails (см. «Ошибки»).

Алгоритм

  1. Контекст и доступ. Актор — аутентифицированный User; SubscriberUserId берётся из JWT-claims identity (не из тела запроса). Операция вне tenant. Идемпотентность обеспечивается состоянием строки (шаг 2), отдельного idempotency-ключа не требуется.
  2. Проверка входа и предусловий.
    • формат: Id != Guid.Empty → иначе VALIDATION_FAILED (400);
    • существование: искомая подписка — по Id, включая soft-удалённые (чтобы поймать уже отписанную строку для идемпотентности); нет строки → NEWS_SUBSCRIPTION_NOT_FOUND (404);
    • record-level (INV-SUB-004): subscription.SubscriberUserId == SubscriberUserId; иначе NEWS_SUBSCRIPTION_FORBIDDEN (403) — чужую подписку трогать нельзя;
    • допустимость состояния: если подписка уже soft-удалена (DeletedAt != null) — идемпотентный no-op, сразу шаг 4 (204).
  3. Переход. Active → Deleted: DeletedAt = now, UpdatedAt = now, DeletedByUserId = SubscriberUserId (actor-stamping). Физического удаления нет — retention истории отписок для аналитики оттока. Подписка не публикует ни доменных, ни интеграционных событий (author-subscription.md, «Событий нет»); аналитический клик отписки шлёт фронт напрямую в user-activity как ActionEvent (ADR-0054), не write-путём news.
  4. Результат. 204 No Content без тела. Идемпотентный повтор (no-op из шага 2) даёт тот же 204.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Id == Guid.Empty«Некорректный идентификатор подписки»no retry
NEWS_SUBSCRIPTION_NOT_FOUND404подписки с Id не существует«Подписка не найдена»no retry
NEWS_SUBSCRIPTION_FORBIDDEN403subscription.SubscriberUserId != actor (INV-SUB-004, чужая подписка)«Недостаточно прав»no retry

Уже soft-удалённая своя подписка — не ошибка: идемпотентный 204 (шаг 2).

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

  • Контракт стабилен: маршрут и 204-семантика не меняются без версии API; идемпотентность гарантирована (клиент может безопасно повторять DELETE). Feature flag news.subscriptions.enabled (README).
  • Logs: subscription-unsubscribed (Id, SubscriberUserId) / subscription-unsubscribe-noop (повтор); metrics: news_subscriptions_unsubscribe_total{result}; traces — сквозной span через traceparent; dashboard news-overview. PII нет — только id-ссылки.

Acceptance Criteria

  • Отписка. Дано: у актора есть активная подписка Id. Когда: User шлёт DELETE /api/v1/subscriptions/{id}. Тогда: переход Active → Deleted (soft, DeletedAt = now, DeletedByUserId = SubscriberUserId), 204 без тела; цель исчезает из выборки ленты subscription, история сохраняется.
  • Идемпотентный повтор. Дано: своя подписка уже soft-удалена. Когда: повторный DELETE. Тогда: 204 no-op — без повторного эффекта.
  • Не найдена. Дано: подписки с Id не существует. Когда: DELETE. Тогда: 404 NEWS_SUBSCRIPTION_NOT_FOUND.
  • Чужая подписка. Дано: строка принадлежит другому подписчику. Когда: DELETE. Тогда: 403 NEWS_SUBSCRIPTION_FORBIDDEN (INV-SUB-004) — состояние не меняется.
  • Некорректный id. Дано: Id == Guid.Empty. Когда: DELETE. Тогда: 400 VALIDATION_FAILED.
  • Гость. Дано: запрос без JWT. Когда: DELETE. Тогда: 401.
  • Конкурентность. Дано: два параллельных DELETE одной строки — или DELETE одновременно с reactivate (subscribe). Когда: оба выполняются. Тогда: каждый DELETE идемпотентен (204), soft-delete фиксируется один раз; при гонке с reactivate побеждает последний писатель DeletedAt, строка сходится в детерминированное состояние без дубля.
  • Событий нет. Дано: успешная отписка. Когда: строка soft-удалена. Тогда: ни доменных, ни интеграционных событий не публикуется; аналитический клик отписки шлёт фронт → user-activity (ADR-0054).

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

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

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