DELETE /api/v1/comments/{id} — Удалить/скрыть комментарий
Терминальный переход комментария из Visible в DeletedByAuthor (автор) либо HiddenByModerator
(оператор с news.comments.moderate). Узел остаётся плейсхолдером в дереве — ответы (дети) не
«отрываются» (INV-CMT-006). Дом полей и инвариантов агрегата — comment.md.
Назначение
Дать автору убрать свой комментарий, а оператору — скрыть нарушающий UGC, не разрушая ветку обсуждения и
денормализованный счётчик Post.CommentsCount.
| Поле | Значение |
|---|
| Сервис/контекст | news |
| API area | public |
| Feature group | comments |
| Статус | draft |
| Документ | docs/06-services/news/api/public/comments/delete-comment.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (автор комментария) или Operator(Admin) (модерация, m2m — ADR-0053) |
| Auth policy / Permissions | аутентифицированный User (self record-level) или permission news.comments.moderate (permission.md) |
| Scope (RBAC) | не применяется (комментарий — публичный UGC, Comment не tenanted) |
| Record-level | автор: actor.UserId == Comment.AuthorUserId; оператор: permission-гейт news.comments.moderate (INV-CMT-004) |
| Tenant isolation | не применяется (ADR-0052: публичный UGC под опубликованным постом — вне tenant) |
| Audit | delete/moderate: actor-stamp (DeletedByUserId/UpdatedByUserId) + Status + ReasonCode (модерация) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | DELETE |
| Route | /api/v1/comments/{id} |
| Success status | 204 (No Content) |
| Idempotency header | не применяется (естественная идемпотентность: повтор на терминальном статусе → 204 без мутации) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
Id | route | Guid | Да | != Guid.Empty; существующий комментарий (шаг 2) | — |
Reason | body | CommentModerationReason? | Нет | enum строкой; учитывается только для модерации (актор с news.comments.moderate); для авторского удаления игнорируется | пишется в ReasonCode audit |
ActorUserId | identity claims | Guid | Да | JWT аутентифицированного актора; определяет ветку record-level (шаг 2) | — |
| Значение | Описание |
|---|
Spam | Спам/реклама. |
Abuse | Оскорбления/травля. |
Offensive | Недопустимый/противоправный контент. |
OffTopic | Не по теме поста. |
Other | Иная причина (уточняется в audit). |
Unknown enum handling: неизвестное значение → отклоняется валидатором (VALIDATION_FAILED); новые причины
добавляются additively.
Модель ответа
Тела ответа нет — 204 No Content. Идентификаторы/статус наружу не возвращаются (клиент уже знает id;
новое состояние узла клиент получает при перечитке дерева GET /api/v1/posts/{id}/comments).
Алгоритм
- Контекст и доступ. Выполняет автор комментария (
User) либо оператор-модератор (m2m, ADR-0053).
Идемпотентность обеспечивается терминальным статусом — Idempotency-header не применяется. Comment —
публичный UGC (вне tenant).
- Проверка входа и доступа.
Id != Guid.Empty; Reason (если задан) — допустимое
CommentModerationReason, иначе VALIDATION_FAILED. Найти комментарий по Id; если не найден →
NEWS_COMMENT_NOT_FOUND (404). Ветвление доступа (INV-CMT-004): если ActorUserId == AuthorUserId —
авторская ветка; иначе актор обязан иметь permission news.comments.moderate — модераторская
ветка; ни то ни другое → FORBIDDEN (403).
- Допустимость состояния. Переход разрешён только из
Visible. Терминальные
DeletedByAuthor/HiddenByModerator — идемпотентный повтор: без мутации и без повторного декремента,
сразу 204 (INV-CMT-006 запрещает лишь обратный переход в Visible).
- Переход и декремент.
- автор:
Visible → DeletedByAuthor, DeletedAt = now, actor-stamp; дети ветки
(Path/RootCommentId) не трогаются;
- оператор:
Visible → HiddenByModerator, actor-stamp, ReasonCode = Reason; DeletedAt не
заполняется (это не удаление); Body при рендере не отдаётся (плейсхолдер).
Денормализованный Post.CommentsCount декрементируется в той же транзакции (INV-CMT-005; ночной
comment-count-reconcile страхует дрейф). Узел остаётся плейсхолдером — ветка обсуждения цела.
- Согласованность и результат. Переход комментария и декремент счётчика применяются атомарно, без
outbox — интеграционных событий нет (удаление/скрытие не рассылается;
pin.news.comment-added.v1 —
только на создание). Конкурентный переход статуса → ConcurrencyConflict (409, retry с перечиткой).
Тела ответа нет — 204 No Content; терминальный повтор — тот же 204.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none (осознанно: удаление/скрытие не публикуется в Kafka) |
| External calls | none |
| Cache / projections / search | Post.CommentsCount декремент (денорм. для отображения/сортировки; ночной comment-count-reconcile страхует дрейф, INV-CMT-005). Узел дерева сохраняется плейсхолдером — дети целы |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | Id == Guid.Empty или недопустимый Reason | «Проверьте корректность запроса» | no retry |
NEWS_COMMENT_NOT_FOUND | 404 | комментарий не найден по Id | «Комментарий не найден» | no retry |
FORBIDDEN | 403 | актор не автор и не имеет news.comments.moderate (INV-CMT-004) | «Недостаточно прав» | no retry |
ConcurrencyConflict | 409 | конкурентный переход статуса (RowVersion/xmin) | «Повторите попытку» | retry (перечитка FindById) |
Acceptance Criteria
- Авторское удаление. Дано:
Visible-комментарий автора. Когда: автор шлёт DELETE. Тогда: переход
Visible → DeletedByAuthor, DeletedAt = now, Post.CommentsCount уменьшен на 1, узел остаётся
плейсхолдером (дети целы), 204; тела ответа нет.
- Модерация. Дано:
Visible-комментарий, актор с news.comments.moderate, Reason = Spam. Когда: DELETE.
Тогда: переход Visible → HiddenByModerator, ReasonCode = Spam, DeletedAt не заполняется, Body при
рендере не отдаётся, CommentsCount уменьшен, 204.
- Идемпотентный повтор. Дано: комментарий уже в
DeletedByAuthor/HiddenByModerator. Когда: повторный
DELETE. Тогда: 204 без мутации и без повторного декремента (INV-CMT-006 запрещает возврат в Visible).
- Нет прав. Дано: актор — не автор и без
news.comments.moderate. Когда: DELETE. Тогда: 403 FORBIDDEN
(INV-CMT-004), состояние не меняется.
- Не найден. Дано:
Id не существует. Когда: DELETE. Тогда: 404 NEWS_COMMENT_NOT_FOUND.
- Недопустимая причина. Дано:
Reason вне enum CommentModerationReason. Когда: DELETE. Тогда:
400 VALIDATION_FAILED.
- Конкурентный переход. Дано: два перехода одного
Visible-узла одновременно. Когда: оба выполняются.
Тогда: один побеждает, проигравший получает 409 ConcurrencyConflict и при retry с перечиткой видит
терминальный статус → идемпотентный 204; счётчик уменьшен ровно один раз.
- Событий нет. Дано: любое успешное удаление/скрытие. Когда: переход зафиксирован. Тогда: интеграционных
событий не публикуется (в отличие от создания).
Совместимость и наблюдаемость
Reason (CommentModerationReason) — additive optional; новые значения enum добавляются additively,
неизвестное значение → VALIDATION_FAILED. Удаление/переименование поля или смена статус-семантики —
breaking (migration + rollout).
- Logs:
comment removed/hidden (CommentId, PostId, actor-kind, Reason — только для модерации);
metrics: news_comment_moderation_total{action}, news_comment_delete_total; traces — сквозной trace
через traceparent; dashboard news-overview (см. README, алерт «drift
CommentsCount»); runbook — сверка/пересчёт CommentsCount (comment-count-reconcile).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы