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

POST /api/messenger/contacts/requests/{contactId}/accept — Принять заявку в контакты

Назначение

Адресат подтверждает входящую заявку в контакты (Pending → Accepted). После подтверждения связь становится двусторонней: обе стороны видят полный профиль друг друга (ФИО + телефон, ../../domain/messenger-user.md) и получают право звонка (../../domain/call-session.md, INV-CALL-1). Вызывает пользователь-адресат из раздела «Заявки»; на выходе — карточка подтверждённого контакта. Соседям по контексту сообщается фактом ContactRequestAccepted (топик pin.messenger.contact-request-accepted.v1): notifications уведомляет инициатора, звонки между сторонами разблокируются; обеим сторонам уходит realtime-обновление.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (только адресат заявки)
Auth policy / Permissionsmessenger.contacts.manage (каталог — ../../../identity/domain/permission.md)
Scope (RBAC)платформа (person-level, вне tenant, ADR-0052)
Record-levelactor == AddresseeUserId (INV-CT-3)
Tenant isolationне применяется — контакты вне tenant
Auditupdate (переход статуса Accepted; RespondedByUserId = actor, RespondedAt)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/messenger/contacts/requests/{contactId}/accept
Success status200 (подтверждено); идемпотентный повтор на Accepted200 (та же связь)
Idempotency headerне применяется (идемпотентность — по целевому статусу Accepted, см. Алгоритм)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДа (вычисляется)должен == AddresseeUserIdrecord-level (INV-CT-3)
ContactIdrouteGuidДа!= Guid.Empty; существующая связь

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

Проекция ../../domain/contact.md + полный профиль ../../domain/messenger-user.md (раскрыт, т.к. Accepted, INV-CT-5).

ПолеТипОбязательностьИсточникMaskingОписание
ContactIdGuidДаContactEntity.IdИдентификатор связи.
StatusContactStatus (string)ДаContactEntity.StatusAccepted.
CounterpartUserIdGuidДаRequesterUserId (инициатор)UserId контрагента (для чата/звонка).
FullNameFullNameДаMessengerUserEntityполностью (INV-CT-5)Полное ФИО инициатора.
PhoneNumberstringДаMessengerUserEntity.PhoneNumberполностьюТелефон E.164 (PII, раскрыт).
ConnectedAtDateTimeOffsetДаContactEntity.RespondedAtМомент подтверждения.

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

Предусловия. Актор авторизован и имеет messenger.contacts.manage. Связь contactId существует; актор — адресат заявки (actor == AddresseeUserId, INV-CT-3). Связь в статусе Pending (или уже Accepted — для идемпотентного повтора).

Постусловия. Связь в Accepted, RespondedAt = now, RespondedByUserId = actor. Обе стороны получают полный профиль друг друга (ФИО + телефон, INV-CT-5) и право звонка (INV-CALL-1). Опубликовано интеграционное ContactRequestAccepted (pin.messenger.contact-request-accepted.v1, ключ RequesterUserId, ContractVersion = 1) атомарно с переходом; обеим сторонам отправлен realtime ContactAccepted.

Кросс-сервисный контракт (что предоставляем). ContactRequestAccepted несёт ContactId, RequesterUserId, AddresseeUserId, RespondedAt, RespondedByUserId — ключ партиционирования RequesterUserId. Разблокировка звонка синхронна и локальна: guard создания звонка читает Contact.Status, поэтому право звонка действует сразу после коммита. Офлайн-уведомление инициатора через notifications — асинхронно (окно eventual-consistency — секунды); инициатор до его доставки уже видит подтверждение по realtime или при перезапросе списка заявок.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь-адресат; требуется право messenger.contacts.manage (иначе — FORBIDDEN). Идентификатор связи contactId берётся из маршрута.
  2. Проверка входа. ContactId непуст — иначе ошибка валидации VALIDATION_FAILED.
  3. Загрузка связи и проверка прав. Найти связь по contactId; если её нет — NOT_FOUND. Актор должен быть адресатом заявки (actor == AddresseeUserId, INV-CT-3); иначе — MSG_CONTACT_FORBIDDEN. Для ответа загрузить профиль инициатора (RequesterUserId).
  4. Бизнес-правила и переход. Идемпотентность: если связь уже Accepted — вернуть текущую карточку (200, без изменений). Если связь уже Declined или Cancelled — заявка закрыта, MSG_CONTACT_INVALID_STATE. Иначе перевести связь Pending → Accepted: RespondedAt = now, RespondedByUserId = actor. Параллельная отмена/подтверждение той же пары — конфликт MSG_CONTACT_CONFLICT (перечитать состояние).
  5. Что меняется. Обновляется единственный агрегат связи. Переход, факт события и аудит фиксируются в одном согласованном изменении.
  6. Факты и события. Публикуется интеграционное событие ContactRequestAccepted в топик pin.messenger.contact-request-accepted.v1 (ContractVersion = 1, ключ RequesterUserId) — контракт ../../events/contact-request-accepted.md. Обеим сторонам уходит realtime-push ContactAccepted; звонки между сторонами разблокируются.
  7. Результат. Полная карточка ContactCardModel (профиль раскрыт, INV-CT-5), 200.

Диаграмма

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

ТипДетали
Интеграционные событияContactRequestAccepted / pin.messenger.contact-request-accepted.v1 / key RequesterUserId
Уведомленияnotifications (консюмер события) — уведомить инициатора; realtime-push ContactAccepted обеим сторонам; разблокировка звонков (../../domain/call-session.md)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400ContactId пуст/некорректен«Проверьте запрос»no retry
FORBIDDEN403Нет permission«Недостаточно прав»no retry
MSG_CONTACT_FORBIDDEN403INV-CT-3: актор не адресат заявки«Подтвердить может только адресат»no retry
NOT_FOUND404Связь не найдена«Заявка не найдена»no retry
MSG_CONTACT_INVALID_STATE409Заявка уже Declined/Cancelled«Заявка недоступна для подтверждения»no retry
MSG_CONTACT_CONFLICT409Гонка Accept/Cancel«Повторите — состояние изменилось»retry (перечитать)

Повтор на уже Acceptedне ошибка: идемпотентный возврат карточки (200).

Acceptance Criteria

  • Подтверждение (happy). Дано: связь Pending, актор — адресат. Когда подтверждает. Тогда связь → Accepted, 200 с полной карточкой (ФИО + телефон инициатора раскрыты, INV-CT-5); опубликовано ContactRequestAccepted; обеим сторонам уходит realtime, звонки между ними разблокированы.
  • Идемпотентный повтор. Дано: связь уже Accepted. Когда актор подтверждает снова. Тогда 200 с текущей карточкой без изменений, событие повторно не публикуется.
  • Не адресат. Дано: актор — инициатор (или третье лицо). Когда пытается подтвердить. Тогда 403 MSG_CONTACT_FORBIDDEN (INV-CT-3).
  • Заявка уже закрыта. Дано: связь Declined или Cancelled. Когда актор подтверждает. Тогда 409 MSG_CONTACT_INVALID_STATE.
  • Гонка Accept/Cancel. Дано: инициатор параллельно отзывает заявку, адресат подтверждает. Тогда побеждает один переход; проигравший получает 409 MSG_CONTACT_CONFLICT и перечитывает состояние (оптимистическая блокировка).
  • Нет связи. Дано: contactId не существует. Тогда 404 NOT_FOUND.
  • Нет прав. Дано: у актора нет messenger.contacts.manage. Тогда 403 FORBIDDEN.

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

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

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

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

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