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

GET /api/messenger/chats — Список чатов пользователя

Назначение

Отдать авторизованному пользователю постранично его чаты (ChatEntity) — личные Direct и Deal-чаты сделок, в которых он активный участник (participant.md) — с денормализованным превью/временем последнего сообщения и счётчиком непрочитанных (unread = Chat.LastMessageSeq − Participant.LastReadSeq, решение №7 README). Точка входа экрана чатов портала (ADR-0055).

Metadata

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

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

ПроверкаЗначение
Actor typeUser (инвестор / агент / руководитель агентства / менеджер застройщика / менеджер ПИН)
Auth policy / Permissionspermission messenger.chats.view — каталог ../../../identity/domain/permission.md
Scope (RBAC)не применяется — доступ по членству, не по scope/tenant (ADR-0052 п.3, README реш. №6)
Record-levelвидны только чаты, где есть активный ParticipantEntity (UserId = actor, RemovedAt is null, INV-CHAT-4) — предикат выборки, не пост-фильтр
Tenant isolationнет (кросс-стороннее общение; messenger не tenanted строково, chat.md)
Auditread; ФИО собеседника/превью в логи не пишутся (PII)

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаRequired; permission messenger.chats.viewmembership-предикат
KindqueryChatKind?Нетenum строкой Direct/Deal; unknown → ошибка валидации — дом chat.md
OnlyUnreadquerybool?Нетtrue → только чаты с unread > 0; null/false — все
PagingqueryPagingQueryНетPage >= 1 (default 1), PageSize 1..100 (default 20)

Фильтры комбинируются по AND (логические предикаты: отсутствие совпадений → пустая страница, не 404). Сортировка фиксирована — LastMessageAt desc (свежие сверху), tiebreak Chat.Id desc.

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

Постраничный ответ: элементы страницы + метаданные пагинации (Page, PageSize, TotalCount).

ПолеТипОбязательностьИсточникОписание
ItemsIReadOnlyList<ChatListItemModel>Да (может быть пустым)страница выборкиЭлементы страницы.
PageintДавходТекущая страница.
PageSizeintДавходРазмер страницы.
TotalCountlongДаCount по предикатуВсего чатов под фильтром.

Вложенная модель ChatListItemModel (проекция chat.md + participant.md)

ПолеТипОбязательностьИсточникMaskingОписание
ChatIdGuidДаChat.IdПереход в карточку чата (get-chat).
KindChatKind (enum→string)ДаChat.KindDirect / Deal.
StatusChatStatus (enum→string)ДаChat.StatusActive/Archived/Closed.
TitlestringДаChat.Title (Deal) / резолв (Direct)Для Direct — ФИО собеседника (per-reader, шаг 4); для Deal — денорм-заголовок сделки.
CounterpartyUserIdGuid?Нетактивный Participant.UserId != actorСобеседник в Direct; null для Deal.
DealIdGuid?НетChat.DealIdСделка Deal-чата (crm/deal.md); null для Direct.
ProjectIdGuid?НетChat.ProjectIdПроект сделки (навигация/группировка); null для Direct.
LastMessageIdGuid?НетChat.LastMessageIdПоследнее сообщение (денорм).
LastMessagePreviewstring?НетChat.LastMessagePreviewPII (не в логи)Короткое превью (≤256; без тела вложения).
LastMessageAtDateTimeOffset?НетChat.LastMessageAtВремя последнего сообщения (ключ сортировки).
LastMessageSeqlongДаChat.LastMessageSeqSeq последнего сообщения.
UnreadCountlongДаcomputed (шаг 4)max(0, Chat.LastMessageSeq − Participant.LastReadSeq).
MyLastReadSeqlongДаParticipant.LastReadSeqКурсор прочитанного актора.
IsMutedboolДаParticipant.IsMutedПер-участниковая тишина уведомлений.

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь (User); ActorUserId — из claims; требуется право messenger.chats.view (иначе MESSENGER_FORBIDDEN). Доступ — по членству, не по scope/tenant.
  2. Проверка входа. Paging.Page >= 1, PageSize ∈ 1..100; Kind (если задан) — известное значение enum Direct/Deal (unknown → VALIDATION_FAILED). Record-level реализуется предикатом выборки.
  3. Выборка. Чаты, где есть активный участник-актор (UserId = actor, RemovedAt is null, INV-CHAT-4, индексы ix_participants_active/ix_participants_user_id); опц. фильтр Kind; исключаются soft-deleted. OnlyUnread → добавить условие Chat.LastMessageSeq > Participant.LastReadSeq. Сортировка фиксирована LastMessageAt desc, tiebreak Chat.Id desc. Для Direct-строк — реплики собеседников за ФИО (одной выборкой на страницу).
  4. Маппинг. UnreadCount = max(0, Chat.LastMessageSeq − Participant.LastReadSeq). Title: Deal — заголовок сделки; Direct — ФИО собеседника из реплики (per-reader; лаг консюмера → мягкая деградация подписи-placeholder, не ошибка). Enum передаются строкой.
  5. Результат. Постраничный список ChatListItemModel (Items/Page/PageSize/TotalCount) → HTTP 200. Пустая выдача → Items = [], TotalCount = 0 (не 404). Допустима микро-рассинхронизация unread (денорм-снимок), свежесть — на момент запроса.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Page/PageSize вне диапазона; unknown Kind«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»re-login
MESSENGER_FORBIDDEN403нет permission messenger.chats.view«Недостаточно прав»no retry
INTERNAL_ERROR500инфраструктурный сбойgenericretry (backoff)

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

  • Новые optional-поля/фильтры — additive; удаление/переименование/смена типа поля или значения enum ChatKind/ChatStatus — breaking (migration + rollout). Unknown enum во входе — ошибка валидации.
  • Logs: chats listed (ActorUserId, TotalCount, hasUnreadFilter; ФИО/превью — не в логи); metrics: messenger_chats_list_total{result}, latency p95 (< 500 мс, SLO списков README); traces — сквозной trace (traceparent); dashboard pin-messenger.

Acceptance Criteria

  • Happy path. Given у актора есть чаты, где он активный участник; When GET ?kind&onlyUnread&paging; Then 200, только чаты с активным участием (INV-CHAT-4), LastMessageAt desc (tiebreak Chat.Id desc), UnreadCount = max(0, LastMessageSeq − LastReadSeq).
  • Только непрочитанные. Given OnlyUnread = true; When GET; Then только чаты с unread > 0.
  • Фильтр по типу. Given Kind = Direct|Deal; When GET; Then срез по типу; unknown Kind400 VALIDATION_FAILED.
  • Direct Title. Given Direct-строка; When GET; Then Title = ФИО собеседника (per-reader), CounterpartyUserId заполнен; для Deal — денорм-заголовок сделки, CounterpartyUserId = null.
  • Лаг реплики. Given реплика собеседника отстаёт; When GET; Then placeholder-подпись, не ошибка.
  • Пустая выдача. Given у актора нет чатов под фильтром; When GET; Then Items = [], TotalCount = 0 (не 404).
  • Свежесть unread. Given денорм-снимок; Then допустима микро-рассинхронизация UnreadCount, свежесть — на момент запроса.
  • Доступ. Given нет messenger.chats.view; When GET; Then 403 MESSENGER_FORBIDDEN; PII (ФИО/превью) в логи не пишутся.

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

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

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