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
| Поле | Значение |
|---|---|
| ID | UC-MSG-004 |
| Контекст | messenger (см. docs/06-services/messenger/) |
| Тип | основной (+ A1 повторная блокировка (идемпотентно), A2 разблокировка, A3 отправка/звонок при блоке, A4 блокировка при активной заявке/контакте) |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Пользователь-инициатор (Owner; User: Инвестор/Агент/Менеджер) |
| Permissions / scope | JWT 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.
- Отправка блокировки. Актор нажимает «Заблокировать» и отправляет
{ BlockedUserId, Reason? }; инициатор (OwnerUserId) берётся из токена, не из тела. - Проверка входа.
Reason—≤ 256;BlockedUserId != OwnerUserId, иначеMSG_BLACKLIST_SELF(INV-BL-1); блокируемый пользователь существует, иначеMSG_USER_NOT_FOUND. - Идемпотентность. Если запись
(Owner, Blocked)уже есть (A1) → вернуть существующую (no-op, INV-BL-1); иначе продолжить. - Создание записи. Создаётся факт блокировки (
CreatedByUserId = OwnerUserId, checkck_blacklist_not_self); поднимается доменноеUserBlocked. АктивныйContactEntityпары не удаляется (INV-BL-2), но политика станетdeny. - Что меняется. Появляется запись
BlacklistEntityпод uniqueux_blacklist_owner_blocked(гонка двух блокировок → конфликт → идемпотентная обработка). - События. Доменное
UserBlocked→ строка realtime-outboxBlacklistChanged, адресованная толькоOwner(user:{OwnerUserId}) — UI обновит список и скроет кнопки общения. Заблокированному ничего не отправляется (приватность). Kafka-событий нет (блокировка приватна, кросс-сервисных потребителей нет). - Согласованность. Запись blacklist и строка realtime-outbox фиксируются в одном согласованном изменении.
- Ответ и realtime. Возврат
{ BlockedUserId, CreatedAt }→201(A1 →200); realtime-dispatch пушитblacklist_changedтолько инициатору. С этого моментаCommunicationPolicy.CanMessage(owner, blocked) = denyв обе стороны (A3).
Диаграмма
Альтернативные Потоки
A1. Повторная блокировка (идемпотентно)
- Запись
(Owner, Blocked)уже существует (ux_blacklist_owner_blocked) → идемпотентный no-op, возвращает существующую (200); новой строки/события нет (INV-BL-1).
A2. Разблокировка (hard delete)
DELETE /api/messenger/blacklist/{blockedUserId}(unblock-user.md): найденная запись(owner, blocked)удаляется (hard delete, INV-BL-4); отсутствие строки — идемпотентный no-op. Поднимается доменноеUserUnblocked.- Фиксация + строка realtime-outbox
BlacklistChanged(толькоOwner). Политика восстанавливается по существующемуContactEntity: если встречной блокировки нет —CanMessageсноваallow.204 No Content.
A3. Попытка написать/позвонить при активной блокировке
- Любая сторона шлёт сообщение паре →
CommunicationPolicy.CanMessage = deny(INV-BL-3) → отказMSG_COMMUNICATION_BLOCKED(409, общая ошибка — без указания, кто/когда заблокировал). Звонок →CanCall = deny→MSG_CALL_BLOCKED(UC-MSG-005). Realtime-подписка чата не рвётся, но новые сообщения отклоняются на отправке.
A4. Блокировка при активной заявке или подтверждённом контакте
- Pending-заявка. Если между сторонами висит
Pending-заявка в контакты (contact.md), блокировка её не отменяет и Kafka-события не шлёт — но политика становитсяdeny, поэтому подтвердить заявку и общаться нельзя, пока блок активен. Встречная новая заявка отклоняется гейтом INV-CT-4 (blacklist). - 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_FAILED | 400 | Reason > 256 | Показать ошибку формы |
MSG_BLACKLIST_SELF | 400 | BlockedUserId == OwnerUserId (INV-BL-1) | «Нельзя заблокировать себя» |
MSG_USER_NOT_FOUND | 404 | BlockedUserId не найден | «Пользователь не найден» |
MSG_COMMUNICATION_BLOCKED | 409 | отправка сообщения при активном блоке (A3, INV-BL-3) | Общая ошибка, скрыть «написать» |
MSG_CALL_BLOCKED | 409 | попытка звонка при блоке (A3, INV-CALL-1) | Скрыть «позвонить» |
FORBIDDEN | 403 | доступ к чужой записи 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→B → A и 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-контакт остаётся, звонок/сообщение отклоняются |
| AC6 | Kafka-событий нет; всё локально + realtime только Owner | Тест: Kafka-outbox пуст, realtime_outbox адресован user:{Owner} |
| AC7 | Дано висит Pending-заявка пары; когда Owner блокирует; тогда заявка не удалена, но подтверждение/общение deny, встречная заявка отклонена (A4, INV-CT-4) | Тест: Pending остаётся, accept/send → MSG_COMMUNICATION_BLOCKED, новая заявка → отказ |
| AC8 | Дано Accepted-контакт под блоком; когда Owner разблокирует без встречного блока; тогда CanMessage/CanCall = allow по тому же контакту без повторного подтверждения (A2/A4) | Тест: unblock → allow, профиль сохранён |
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API (дом алгоритма/маппинга): block-user.md, unblock-user.md, list-blacklist.md.
- Domain: blacklist.md (INV-BL-1…4,
CommunicationPolicy), contact.md (INV-CT-4 гейт заявки), messenger-user.md, call-session.md (INV-CALL-1CanCall). - Realtime: realtime-dispatch.md (
blacklist_changed, адресный push). - Permission-каталог: permission.md (
messenger.blacklist.manage). - ADR: ADR-0055, ADR-0052.