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 area | public |
| Feature group | contacts |
| Статус | draft |
| Документ | docs/06-services/messenger/api/contacts/accept-contact-request.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (только адресат заявки) |
| Auth policy / Permissions | messenger.contacts.manage (каталог — ../../../identity/domain/permission.md) |
| Scope (RBAC) | платформа (person-level, вне tenant, ADR-0052) |
| Record-level | actor == AddresseeUserId (INV-CT-3) |
| Tenant isolation | не применяется — контакты вне tenant |
| Audit | update (переход статуса Accepted; RespondedByUserId = actor, RespondedAt) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/messenger/contacts/requests/{contactId}/accept |
| Success status | 200 (подтверждено); идемпотентный повтор на Accepted → 200 (та же связь) |
| Idempotency header | не применяется (идемпотентность — по целевому статусу Accepted, см. Алгоритм) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims | Guid | Да (вычисляется) | должен == AddresseeUserId | record-level (INV-CT-3) |
ContactId | route | Guid | Да | != Guid.Empty; существующая связь | — |
Модель ответа ContactCardModel
Проекция ../../domain/contact.md + полный профиль
../../domain/messenger-user.md (раскрыт, т.к. Accepted, INV-CT-5).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
ContactId | Guid | Да | ContactEntity.Id | — | Идентификатор связи. |
Status | ContactStatus (string) | Да | ContactEntity.Status | — | Accepted. |
CounterpartUserId | Guid | Да | RequesterUserId (инициатор) | — | UserId контрагента (для чата/звонка). |
FullName | FullName | Да | MessengerUserEntity | полностью (INV-CT-5) | Полное ФИО инициатора. |
PhoneNumber | string | Да | MessengerUserEntity.PhoneNumber | полностью | Телефон E.164 (PII, раскрыт). |
ConnectedAt | DateTimeOffset | Да | 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
или при перезапросе списка заявок.
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь-адресат; требуется право
messenger.contacts.manage(иначе —FORBIDDEN). Идентификатор связиcontactIdберётся из маршрута. - Проверка входа.
ContactIdнепуст — иначе ошибка валидацииVALIDATION_FAILED. - Загрузка связи и проверка прав. Найти связь по
contactId; если её нет —NOT_FOUND. Актор должен быть адресатом заявки (actor == AddresseeUserId,INV-CT-3); иначе —MSG_CONTACT_FORBIDDEN. Для ответа загрузить профиль инициатора (RequesterUserId). - Бизнес-правила и переход. Идемпотентность: если связь уже
Accepted— вернуть текущую карточку (200, без изменений). Если связь ужеDeclinedилиCancelled— заявка закрыта,MSG_CONTACT_INVALID_STATE. Иначе перевести связьPending → Accepted:RespondedAt = now,RespondedByUserId = actor. Параллельная отмена/подтверждение той же пары — конфликтMSG_CONTACT_CONFLICT(перечитать состояние). - Что меняется. Обновляется единственный агрегат связи. Переход, факт события и аудит фиксируются в одном согласованном изменении.
- Факты и события. Публикуется интеграционное событие
ContactRequestAcceptedв топикpin.messenger.contact-request-accepted.v1(ContractVersion = 1, ключRequesterUserId) — контракт ../../events/contact-request-accepted.md. Обеим сторонам уходит realtime-pushContactAccepted; звонки между сторонами разблокируются. - Результат. Полная карточка
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_FAILED | 400 | ContactId пуст/некорректен | «Проверьте запрос» | no retry |
FORBIDDEN | 403 | Нет permission | «Недостаточно прав» | no retry |
MSG_CONTACT_FORBIDDEN | 403 | INV-CT-3: актор не адресат заявки | «Подтвердить может только адресат» | no retry |
NOT_FOUND | 404 | Связь не найдена | «Заявка не найдена» | no retry |
MSG_CONTACT_INVALID_STATE | 409 | Заявка уже Declined/Cancelled | «Заявка недоступна для подтверждения» | no retry |
MSG_CONTACT_CONFLICT | 409 | Гонка 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(без ФИО/телефона); metricsmessenger_contact_requests_accepted_total{result}, latency p95; traces черезtraceparent.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): DELETE /api/messenger/contacts/{contactId} — Удалить контакт / отозвать исходящую заявку, GET /api/messenger/contacts/requests — Заявки в контакты (входящие/исходящие), POST /api/messenger/contacts/requests — Отправить заявку в контакты, POST /api/messenger/contacts/requests/{contactId}/decline — Отклонить заявку в контакты
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../../domain/contact.md (
INV-CT-3/5,ContactRequestAccepted), ../../domain/messenger-user.md (раскрытие профиля), ../../domain/call-session.md (INV-CALL-1, право звонка). - Событие: ../../events/contact-request-accepted.md.
- Соседние: decline-contact-request.md, send-contact-request.md, list-contact-requests.md.
- ADR: 0052 tenancy, 0055 own-mini-messenger.