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

POST /api/messenger/contacts/requests — Отправить заявку в контакты

Назначение

Инициатор отправляет адресату заявку в контакты. Создаётся связь ContactEntity со статусом Pending, детерминированным ключом пары PairKey (для дедупликации). Публикуется событие ContactRequestCreated в топик pin.messenger.contact-request-created.v1 (потребитель — notifications), адресату уходит realtime-push ContactRequestReceived. Писать сообщение не-контакту разрешено и без заявки; заявка нужна для звонка и раскрытия профиля. Заблокированному пользователю (чёрный список любой стороны) заявку отправить нельзя (INV-CT-4).

Metadata

ПолеЗначение
Сервис/контекстmessenger
API areapublic
Feature groupcontacts
Статусdraft
Документdocs/06-services/messenger/api/contacts/send-contact-request.md

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

ПроверкаЗначение
Actor typeUser
Auth policy / Permissionsmessenger.contacts.manage (каталог — ../../../identity/domain/permission.md)
Scope (RBAC)платформа (person-level, вне tenant, ADR-0052)
Record-levelинициатор RequesterUserId = actor; видимость строки — участникам пары
Tenant isolationне применяется — контакты вне tenant
Auditcreate (CreatedByUserId = actor = RequesterUserId); RequestMessage в логи не пишется

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/messenger/contacts/requests
Success status201 (создано); идемпотентный повтор при активной паре → 200 (существующая связь)
Idempotency headerне применяется (бизнес-дедуп по ограничению активной пары ux_contacts_active_pair)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДа (вычисляется)= RequesterUserIdrecord-level
AddresseeUserIdbodyGuidДа!= Guid.Empty; != ActorUserId (INV-CT-1)
RequestMessagebodystring?Нет0..512PII: в логи не пишется

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

Проекция ../../domain/contact.md; состав возвращается сразу после подачи заявки.

ПолеТипОбязательностьИсточникMaskingОписание
ContactIdGuidДаContactEntity.IdИдентификатор связи.
StatusContactStatus (string)ДаContactEntity.StatusPending (или существующий статус при идемпотентном повторе).
AddresseeUserIdGuidДаContactEntity.AddresseeUserIdАдресат заявки.
AlreadyExistedboolДаcomputedtrue — вернули существующую активную связь (идемпотентный 200).
CreatedAtDateTimeOffsetДаContactEntity.CreatedAtМомент подачи.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь-инициатор (actor = RequesterUserId); требуется право messenger.contacts.manage (иначе — FORBIDDEN).
  2. Проверка входа. AddresseeUserId непуст и != ActorUserId (INV-CT-1, иначе — MSG_CONTACT_SELF); RequestMessage длиной 0..512 — иначе VALIDATION_FAILED.
  3. Проверка адресата, дедуп и чёрный список. Адресат должен существовать и быть активным, иначе — MSG_CONTACT_USER_NOT_FOUND (INV-CT-6). По ключу пары PairKey(actor, Addressee) проверить дедуп (INV-CT-2): если уже есть активная связь (Pending/Accepted) — вернуть её (AlreadyExisted = true, идемпотентный 200). Проверить чёрный список (INV-CT-4): если любая сторона заблокировала другую — MSG_CONTACT_BLOCKED (общая формулировка ради приватности ../../domain/blacklist.md).
  4. Создание связи. Завести ContactEntity со статусом Pending, RequesterUserId = actor, заданным AddresseeUserId, RequestMessage, ключом пары PairKey и CreatedByUserId = actor.
  5. Что меняется. Появляется единственный агрегат связи. Создание и факт события фиксируются в одном согласованном изменении. Гонка INV-CT-2 (параллельная заявка той же пары) закрывается ограничением активной пары ux_contacts_active_pair — при конфликте возвращается существующая связь (200).
  6. Факты и события. Публикуется интеграционное событие ContactRequestCreated в топик pin.messenger.contact-request-created.v1 (ContractVersion = 1, ключ AddresseeUserId) — контракт ../../events/contact-request-created.md. Адресату уходит realtime-push ContactRequestReceived.
  7. Результат. ContactRequestResultModel; 201 (создано) или 200 (идемпотентный повтор, AlreadyExisted = true).

Диаграмма

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

ТипДетали
Интеграционные событияContactRequestCreated / pin.messenger.contact-request-created.v1 / key AddresseeUserId
Уведомленияnotifications (консюмер события) — уведомить адресата о заявке; realtime-push ContactRequestReceived

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Формат тела (AddresseeUserId, RequestMessage)«Проверьте корректность формы»no retry
MSG_CONTACT_SELF400INV-CT-1: заявка самому себе«Нельзя добавить в контакты себя»no retry
FORBIDDEN403Нет permission messenger.contacts.manage«Недостаточно прав»no retry
MSG_CONTACT_USER_NOT_FOUND404INV-CT-6: адресат не найден/неактивен«Пользователь не найден»no retry
MSG_CONTACT_BLOCKED409INV-CT-4: пара в чёрном списке (любая сторона)«Отправка заявки недоступна»no retry

INV-CT-2 (активная пара уже есть) — не ошибка: идемпотентный возврат существующей связи (200).

Acceptance Criteria

  • Новая заявка. Дано активный адресат, между парой нет активной связи и нет блокировки. Когда POST. Тогда создаётся ContactEntity со статусом Pending (RequesterUserId = actor), ответ 201 (AlreadyExisted = false), публикуется ContactRequestCreated в pin.messenger.contact-request-created.v1 (ключ AddresseeUserId), адресату уходит realtime-push ContactRequestReceived.
  • Идемпотентный повтор. Дано на пару уже есть активная связь (Pending/Accepted). Когда повторная заявка. Тогда 200, AlreadyExisted = true, возвращается существующая связь, дубля нет (INV-CT-2).
  • Заявка самому себе. Дано AddresseeUserId = ActorUserId. Когда POST. Тогда MSG_CONTACT_SELF (400) (INV-CT-1).
  • Адресат не найден/неактивен. Дано AddresseeUserId не существует или неактивен. Когда POST. Тогда MSG_CONTACT_USER_NOT_FOUND (404) (INV-CT-6).
  • Чёрный список. Дано любая сторона заблокировала другую. Когда POST. Тогда MSG_CONTACT_BLOCKED (409) общей формулировкой ради приватности (INV-CT-4) — какая именно сторона заблокировала, не раскрывается.
  • Гонка параллельных заявок пары. Дано две одновременные заявки той же пары. Когда обе доходят. Тогда ограничение ux_contacts_active_pair пропускает одну (создаёт Pending), вторая возвращает существующую связь (200, AlreadyExisted = true).
  • Валидация сообщения. Дано RequestMessage длиннее 512. Когда POST. Тогда VALIDATION_FAILED (400); текст сообщения в логи не пишется.

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

  • Новые optional поля тела/ответа — additive; удаление/переименование/смена типа — breaking (migration).
  • Контракт события версионируется (ContractVersion); новые поля payload — additive.
  • Logs: actor, contactId, addresseeHash, alreadyExisted (без ФИО/сообщения); metrics messenger_contact_requests_created_total{result}, latency p95; traces через traceparent.

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

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

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