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

GET /api/messenger/contacts — Список подтверждённых контактов

Назначение

Табличный список контактов пользователя (страница «Контакты» портала): все связи актора со статусом Accepted, где актор — инициатор или адресат. Для Accepted профиль контрагента раскрыт полностью (ФИО + телефон, INV-CT-5), доступен звонок. Опционально — поиск по ФИО и нормализованному телефону контрагента через поисковую проекцию MessengerContactSearchDocument (../../search/contacts.md, ADR-0059), сужённую до контактов актора. Вызывает любой пользователь; ответ постраничный, сортировка — по ФИО.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Инвестор/Агент/Руководитель/Менеджер застройщика/Менеджер ПИН)
Auth policy / Permissionsmessenger.contacts.manage (каталог — ../../../identity/domain/permission.md)
Scope (RBAC)платформа (граф контактов — person-level, вне tenant, ADR-0052)
Record-levelтолько связи, где actor ∈ {RequesterUserId, AddresseeUserId}
Tenant isolationне применяется — контакты вне tenant (person-level)
Auditread (список PII: ФИО/телефон контактов); access-log без значений телефона/поиска

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/messenger/contacts
Success status200
Idempotency headerне применяется (GET)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДа (вычисляется)!= Guid.Emptyопределяет record-level
Searchquery (search)string?Нетtrim, 1..128; пустой после trim → игнор; передаётся в полнотекстовый поискPII: значение в логи не пишется
PagequeryintНет (default 1)>= 1
PageSizequeryintНет (default 20)1..100

Сортировка фиксирована — LastName asc, FirstName asc; тай-брейк ContactId asc (стабильная пагинация).

Модель ответа — постраничный список ContactListItemModel

Постраничный контейнер: Items, Page, PageSize, TotalCount.

Вложенная модель ContactListItemModel (проекция ../../domain/contact.md + ../../domain/messenger-user.md)

ПолеТипОбязательностьИсточникMaskingОписание
ContactIdGuidДаContactEntity.IdИдентификатор связи (для remove-contact).
CounterpartUserIdGuidДасторона пары ≠ акторUserId контрагента (открыть чат/звонок).
FullNameFullNameДаMessengerUserEntity (FirstName/MiddleName?/LastName)полностью (Accepted, INV-CT-5)Полное ФИО контрагента.
PhoneNumberstringДаMessengerUserEntity.PhoneNumberполностью (Accepted)Телефон E.164 (PII).
IsCounterpartActiveboolДаMessengerUserEntity.IsActivefalse — контрагент заблокирован/архивирован в identity.
ConnectedAtDateTimeOffsetДаContactEntity.RespondedAtМомент подтверждения контакта.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь; требуется право messenger.contacts.manage (иначе — FORBIDDEN, fail-closed). Актор берётся из идентификационных данных.
  2. Проверка входа. Search — trim, длина 1..128; Page >= 1; PageSize в 1..100 — иначе VALIDATION_ERROR. Видимость строк ограничена членством актора в паре (шаг 3).
  3. Что читаем. Выбрать связи со статусом Accepted, где актор — одна из сторон (индексы ix_contacts_requester/ix_contacts_addressee по (UserId, Status)). Для страницы CounterpartUserId — противоположная сторона; профили контрагентов подгружаются пакетно (без N+1). При заданном Search — запрос к поисковой проекции messenger-contacts, сужённой до множества CounterpartUserId актора (record-level, ../../search/contacts.md), с fuzzy-матчем по FullNameSearch/NameTranslit/PhoneDigits; результат — множество совпавших UserId, которым фильтруется страница. Без Search полнотекстовый поиск не вызывается (чистый постраничный запрос к PostgreSQL).
  4. Маппинг. Все строки Accepted, поэтому маскирование не применяется (полный профиль, INV-CT-5). Сортировка LastName asc, FirstName asc (тай-брейк ContactId asc).
  5. Результат. Постраничный список; пустая выдача → Items = [], TotalCount = 0, 200.

Диаграмма

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

ТипДетали
Проекции / поискпоисковая проекция MessengerContactSearchDocument (messenger-contacts) при заданном search, scope-filter к контактам актора (ADR-0059); source of truth — MessengerUserEntity/ContactEntity в PostgreSQL

Свежесть и деградация. Базовый листинг без search идёт напрямую к PostgreSQL — авторитетен и всегда актуален: только что подтверждённый контакт виден немедленно. Поиск с search использует Elasticsearch-проекцию (ADR-0059), которая пополняется по событию pin.identity.users.v1 и при подтверждении контакта — окно eventual-consistency секунды; недавний контакт/смена ФИО могут кратко не находиться поиском, оставаясь видимыми в базовом листинге. Недоступность проекции — деградация: поиск не работает/отстаёт, листинг без search продолжает работать; восстановление — воркер contact-search-rebuild. IsCounterpartActive отражает реплику identity (монотонный version-guard) и может кратко отставать от блокировки/архивации пользователя в identity.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_ERROR400Нарушение таблицы входных (Search длина, Page/PageSize)«Некорректные параметры запроса»no retry
FORBIDDEN403Нет permission messenger.contacts.manage«Недостаточно прав»no retry
INTERNAL_ERROR500Инфраструктурный сбойgenericretry (backoff)

Acceptance Criteria

  • Список (happy). Дано: у актора есть подтверждённые контакты. Когда запрашивает без search. Тогда 200 со страницей LastName asc, FirstName asc; профиль раскрыт полностью (ФИО + телефон, INV-CT-5); показаны только Accepted-связи, где актор — сторона пары.
  • Поиск по ФИО/телефону. Дано: задан search (2+ значимых символа). Когда запрашивает. Тогда страница сужена до контактов актора, совпавших по FullNameSearch/NameTranslit/PhoneDigits (fuzzy); значение search в логи не пишется.
  • Свежесть базового листинга. Дано: контакт только что подтверждён. Когда актор запрашивает без search. Тогда контакт присутствует немедленно (PostgreSQL — source of truth), даже если поисковая проекция ещё не переиндексирована.
  • Только свои контакты. Дано: существуют чужие связи. Когда актор запрашивает. Тогда видны только связи, где actor ∈ {RequesterUserId, AddresseeUserId} (record-level).
  • Пустая выдача. Дано: контактов нет (или ни один не совпал с search). Когда запрашивает. Тогда 200 c Items = [], TotalCount = 0.
  • Некорректный вход. Дано: Search длиннее 128, Page < 1 или PageSize вне 1..100. Когда запрашивает. Тогда 400 VALIDATION_ERROR.
  • Нет прав. Дано: у актора нет messenger.contacts.manage. Тогда 403 FORBIDDEN (fail-closed).

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

  • Новые optional поля ответа — additive; удаление/переименование/смена типа — breaking (migration + rollout).
  • Logs: actor, hasSearch, TotalCount (без значений телефона/поиска); metrics messenger_contacts_list_requests_total, latency p95 (SLO списки < 500 мс); traces через traceparent.

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

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

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