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

UC-MSG-004. Заблокировать пользователя (чёрный список)

Назначение

Описывает сценарий: пользователь добавляет другого в чёрный список (blacklist.md), запрещая обеим сторонам писать и звонить друг другу. Эффект CommunicationPolicyвзаимный: одной записи любой стороны достаточно, чтобы CanMessage/CanCall вернули deny в обе стороны (INV-BL-3). Факт блокировки виден только инициатору (Owner); заблокированному не раскрывается — его сообщения тихо отклоняются общей ошибкой (приватность). Разблокировка — hard delete записи (INV-BL-4). Полный алгоритм и маппинг — в API-методах: block-user.md, unblock-user.md, list-blacklist.md. Связан с контекстом messenger.

Metadata

ПолеЗначение
IDUC-MSG-004
Контекстmessenger (см. docs/06-services/messenger/)
Типосновной (+ A1 повторная блокировка (идемпотентно), A2 разблокировка, A3 отправка/звонок при блоке, A4 блокировка при активной заявке/контакте)
Статусdraft

Актор

ПолеЗначение
РольПользователь-инициатор (Owner; User: Инвестор/Агент/Менеджер)
Permissions / scopeJWT identity; messenger.blacklist.manage (блокировать/разблокировать/видеть свой список); guest → 401
Tenantне применяется — блокировка вне компаний/tenant (отношение пользователь↔пользователь, ADR-0052)
Record-levelпользователь видит и меняет только записи с OwnerUserId = actor (blacklist.md, Безопасность)

Предусловие

  • Актор аутентифицирован; BlockedUserId — существующий пользователь (messenger-user.md), BlockedUserId != OwnerUserId (нельзя блокировать себя, INV-BL-1).

Триггер

ПолеЗначение
ИсточникUI (карточка пользователя/чата → «Заблокировать»; настройки → чёрный список) → API call
Триггерящий элементPOST /api/messenger/blacklist / DELETE /api/messenger/blacklist/{blockedUserId}

Основной Поток

Полный алгоритм и маппинг — в block-user.md; ниже — поток на уровне use-case.

  1. Отправка блокировки. Актор нажимает «Заблокировать» и отправляет { BlockedUserId, Reason? }; инициатор (OwnerUserId) берётся из токена, не из тела.
  2. Проверка входа. Reason≤ 256; BlockedUserId != OwnerUserId, иначе MSG_BLACKLIST_SELF (INV-BL-1); блокируемый пользователь существует, иначе MSG_USER_NOT_FOUND.
  3. Идемпотентность. Если запись (Owner, Blocked) уже есть (A1) → вернуть существующую (no-op, INV-BL-1); иначе продолжить.
  4. Создание записи. Создаётся факт блокировки (CreatedByUserId = OwnerUserId, check ck_blacklist_not_self); поднимается доменное UserBlocked. Активный ContactEntity пары не удаляется (INV-BL-2), но политика станет deny.
  5. Что меняется. Появляется запись BlacklistEntity под unique ux_blacklist_owner_blocked (гонка двух блокировок → конфликт → идемпотентная обработка).
  6. События. Доменное UserBlocked → строка realtime-outbox BlacklistChanged, адресованная только Owner (user:{OwnerUserId}) — UI обновит список и скроет кнопки общения. Заблокированному ничего не отправляется (приватность). Kafka-событий нет (блокировка приватна, кросс-сервисных потребителей нет).
  7. Согласованность. Запись blacklist и строка realtime-outbox фиксируются в одном согласованном изменении.
  8. Ответ и realtime. Возврат { BlockedUserId, CreatedAt }201 (A1 → 200); realtime-dispatch пушит blacklist_changed только инициатору. С этого момента CommunicationPolicy.CanMessage(owner, blocked) = deny в обе стороны (A3).

Диаграмма

Альтернативные Потоки

A1. Повторная блокировка (идемпотентно)

  1. Запись (Owner, Blocked) уже существует (ux_blacklist_owner_blocked) → идемпотентный no-op, возвращает существующую (200); новой строки/события нет (INV-BL-1).

A2. Разблокировка (hard delete)

  1. DELETE /api/messenger/blacklist/{blockedUserId} (unblock-user.md): найденная запись (owner, blocked) удаляется (hard delete, INV-BL-4); отсутствие строки — идемпотентный no-op. Поднимается доменное UserUnblocked.
  2. Фиксация + строка realtime-outbox BlacklistChanged (только Owner). Политика восстанавливается по существующему ContactEntity: если встречной блокировки нет — CanMessage снова allow. 204 No Content.

A3. Попытка написать/позвонить при активной блокировке

  1. Любая сторона шлёт сообщение паре → CommunicationPolicy.CanMessage = deny (INV-BL-3) → отказ MSG_COMMUNICATION_BLOCKED (409, общая ошибка — без указания, кто/когда заблокировал). Звонок → CanCall = denyMSG_CALL_BLOCKED (UC-MSG-005). Realtime-подписка чата не рвётся, но новые сообщения отклоняются на отправке.

A4. Блокировка при активной заявке или подтверждённом контакте

  1. Pending-заявка. Если между сторонами висит Pending-заявка в контакты (contact.md), блокировка её не отменяет и Kafka-события не шлёт — но политика становится deny, поэтому подтвердить заявку и общаться нельзя, пока блок активен. Встречная новая заявка отклоняется гейтом INV-CT-4 (blacklist).
  2. Accepted-контакт. Активный контакт пары не удаляется (INV-BL-2) — раскрытие профиля сохраняется, но CanMessage/CanCall возвращают deny. После разблокировки (A2, при отсутствии встречного блока) политика восстанавливается по тому же контакту без повторного подтверждения.

Постусловие

  • Существует запись BlacklistEntity (Owner=actor, Blocked=target) (ux_blacklist_owner_blocked); активный контакт пары не удалён (INV-BL-2), но CommunicationPolicy отдаёт deny обеим сторонам.
  • Realtime blacklist_changed доставлен только инициатору; заблокированный не уведомлён (приватность).
  • A2: запись удалена (hard delete), политика восстановлена по существующему контакту.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
VALIDATION_FAILED400Reason > 256Показать ошибку формы
MSG_BLACKLIST_SELF400BlockedUserId == OwnerUserId (INV-BL-1)«Нельзя заблокировать себя»
MSG_USER_NOT_FOUND404BlockedUserId не найден«Пользователь не найден»
MSG_COMMUNICATION_BLOCKED409отправка сообщения при активном блоке (A3, INV-BL-3)Общая ошибка, скрыть «написать»
MSG_CALL_BLOCKED409попытка звонка при блоке (A3, INV-CALL-1)Скрыть «позвонить»
FORBIDDEN403доступ к чужой записи blacklist (OwnerUserId != actor)«Недостаточно прав»

Доменные События

СобытиеКогда поднимаетсяSubscribers
UserBlockedсоздание записи (блокировка)realtime_outbox BlacklistChanged → только Owner
UserUnblockedудаление записи (разблокировка, A2)realtime_outbox BlacklistChanged → только Owner

Оба события обрабатываются в той же транзакции; заблокированному realtime не шлётся.

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
блокировка/разблокировка Kafka-событий не порождают (blacklist.md)

Чёрный список приватен и локален для messenger: outbox не используется, кросс-сервисных потребителей нет. Сервис звонков (при появлении) читает CommunicationPolicy синхронно, а не по событию.

Acceptance Criteria

#КритерийПроверка
AC1Блокировка взаимна: CanMessage/CanCall = deny обеим сторонам, пока есть хотя бы одна записьТест CommunicationPolicy: блок A→BA и B не пишут/не звонят
AC2Приватность: событие/список видны только Owner; заблокированному не раскрываетсяТест: list-blacklist только для Owner; отправка блокированного → общая MSG_COMMUNICATION_BLOCKED
AC3Идемпотентность блокировки: повтор не создаёт дубль (ux_blacklist_owner_blocked)Тест A1: два POST → одна строка, 201 затем 200
AC4Разблокировка — hard delete; политика восстановлена по существующему контактуТест A2: DELETE → строки нет; при отсутствии встречного блока CanMessage=allow
AC5Блокировка не удаляет активный контакт (INV-BL-2), но глушит общениеТест: Accepted-контакт остаётся, звонок/сообщение отклоняются
AC6Kafka-событий нет; всё локально + realtime только OwnerТест: Kafka-outbox пуст, realtime_outbox адресован user:{Owner}
AC7Дано висит Pending-заявка пары; когда Owner блокирует; тогда заявка не удалена, но подтверждение/общение deny, встречная заявка отклонена (A4, INV-CT-4)Тест: Pending остаётся, accept/sendMSG_COMMUNICATION_BLOCKED, новая заявка → отказ
AC8Дано Accepted-контакт под блоком; когда Owner разблокирует без встречного блока; тогда CanMessage/CanCall = allow по тому же контакту без повторного подтверждения (A2/A4)Тест: unblockallow, профиль сохранён

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

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

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