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

POST /api/public/notifications/inbox/{notificationId}/read — Отметить прочитанным

Назначение

Авторизованный пользователь отмечает своё in-app уведомление (NotificationEntity) прочитанным (Sent → Read, проставляется ReadAt) и получает обновлённый счётчик непрочитанных. Поддерживает одиночную отметку, батч (несколько id) и «отметить все». Owning-контекст — notifications, feature group inbox.

Metadata

ПолеЗначение
Сервис/контекстnotifications
API areapublic (JWT пользователя)
Feature groupinbox
Статусdraft
Документdocs/06-services/notifications/api/public/mark-notification-read.md

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

ПроверкаЗначение
Actor typeUser
Auth policy / Permissionsnotifications.inbox.read (own)
Record-levelвладелец = RecipientUserId (актор); чужое уведомление недоступно (INV-NTF-6)
Tenant isolationдоступ по RecipientUserId
Auditupdate (ReadAt, Version)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/public/notifications/inbox/{notificationId}/read (или /inbox/read с телом-списком / all=true)
Success status200
Idempotency headerне применяется (повтор — no-op, уже Read)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
RecipientUserIdidentity claims (JWT sub)GuidДане Guid.Emptyвладелец
NotificationIdrouteGuid?Условнообязателен, если MarkAll = falseown record
NotificationIdsbodyIReadOnlyList<Guid>?Нет≤ 200 элементов (батч)own records
MarkAllqueryboolНетdefault false; true — отметить все непрочитанные

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

ПолеТипОбязательностьИсточникMaskingОписание
MarkedCountintДачисло переведённых в ReadСколько уведомлений отмечено.
UnreadCountintДачисло Status != Read при Channel = InAppОстаток непрочитанных (бейдж).

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь (RecipientUserId из JWT; нет валидного JWT → UNAUTHENTICATED); доступ — только к собственным уведомлениям.
  2. Проверка входа. Задан NotificationId/NotificationIds или MarkAll = true; размер батча ≤ 200. Иначе → VALIDATION_FAILED.
  3. Выбор целевых уведомлений. При MarkAll = true — все in-app уведомления получателя в статусе Sent. Иначе — уведомления по переданным id; каждое должно принадлежать актору и быть каналом InApp (INV-NTF-6): чужое или не-InAppNTF_READ_FORBIDDEN; несуществующий NotificationIdNTF_NOTIFICATION_NOT_FOUND.
  4. Переход. Каждое выбранное уведомление переводится Sent → Read (ReadAt = now, INV-NTF-5); уже Read — no-op (в MarkedCount не учитывается).
  5. Согласованность. Изменения применяются батчем в одном согласованном изменении; правка не должна затирать конкурентную. После фиксации бейдж непрочитанных обновляется realtime (сигнал NotificationStatusChanged → SignalR).
  6. Результат. Возвращается MarkReadResultModel (MarkedCount, UnreadCount), HTTP 200.

Диаграмма

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

ТипДетали
Уведомленияrealtime-обновление бейджа непрочитанных (SignalR)

Семантика батча и конкурентность

  • Атомарная валидация батча. Перед переходом проверяется весь набор целей: любой id, не принадлежащий актору или не InApp (INV-NTF-6) → NTF_READ_FORBIDDEN и ничего не отмечается; любой несуществующий id → NTF_NOTIFICATION_NOT_FOUND, батч отклонён целиком. Так частичное «полу-применение» исключено.
  • Уже Read — no-op. В валидном батче записи, уже находящиеся в Read, пропускаются и не входят в MarkedCount (идемпотентность повтора). MarkAll = true берёт только in-app в статусе Sent.
  • Только Sent → Read. Отмечается уведомление в Sent (INV-NTF-5). Ещё не доставленный in-app (Pending) как непрочитанный в бейдже учитывается, но целью явной отметки не является (перейдёт в Sent доставкой).
  • Конкурентность. Гонка mark-read ↔ dispatcher/retry (смена статуса той же записи) разрешается оптимистично по версии записи: правка не должна затирать более свежее состояние; проигравший перечитывает и повторяет. Две параллельные отметки одного id → одна отмечает, вторая видит Read (no-op).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
UNAUTHENTICATED401нет/невалидный JWT«Требуется авторизация»no retry
VALIDATION_FAILED400ни id, ни MarkAll; батч > 200«Проверьте параметры»no retry
NTF_READ_FORBIDDEN403уведомление не принадлежит актору или не InApp (INV-NTF-6)«Недоступно»no retry
NTF_NOTIFICATION_NOT_FOUND404NotificationId не найден«Уведомление не найдено»no retry

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

  • Предусловие. Пользователь авторизован; цели — его собственные in-app уведомления; батч ≤ 200.
  • Постусловие (успех). Валидные Sent-цели переведены в Read (ReadAt = now, Version ++); бейдж непрочитанных пересчитан и разослан realtime; уже Read и Pending не тронуты.

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

  • Батч/MarkAll — additive поверх одиночной отметки. Повтор — идемпотентный no-op.
  • Logs: marked-read (RecipientUserId, MarkedCount); metrics: notifications_marked_read_total.

Acceptance criteria

  • Given непрочитанное in-app уведомление актора, when отметка по id, then 200, Sent → Read, MarkedCount = 1, UnreadCount уменьшен, бейдж обновлён realtime.
  • Given уже Read уведомление, when повторная отметка, then 200, MarkedCount = 0 (no-op).
  • Given MarkAll = true, when отметка, then все Sent-in-app актора → Read, UnreadCount = 0.
  • Given батч с чужим или не-InApp id, when отметка, then 403 NTF_READ_FORBIDDEN, ничего не отмечено.
  • Given несуществующий NotificationId, when отметка, then 404 NTF_NOTIFICATION_NOT_FOUND.
  • Given ни id, ни MarkAll, либо батч > 200, when запрос, then 400 VALIDATION_FAILED.

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

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

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