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

GET /api/messenger/chats/{chatId} — Карточка чата (участники + read-state)

Назначение

Отдать активному участнику чата (ChatEntity) метаданные чата (Kind/Status/ заголовок/сделку), список активных участников (participant.md) с отображаемыми ФИО (реплика messenger-user.md) и read-receipt: в Direct — пофамильно (2 участника), в групповом Deal-чате — без пофамильного раскрытия по умолчанию (INV-PART-5).

Metadata

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

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

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

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаRequired; permission messenger.chats.viewrecord-level (членство)
ChatIdrouteGuidДа!= Guid.Empty; существующий чат-участник актора (шаг 2)

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

Проекция chat.md + participant.md; дом полей — там, ниже только форма карточки.

ПолеТипОбязательностьИсточникMaskingОписание
ChatIdGuidДаChat.IdИдентификатор чата.
KindChatKind (enum→string)ДаChat.KindDirect / Deal.
StatusChatStatus (enum→string)ДаChat.StatusActive/Archived/Closed.
TitlestringДаChat.Title (Deal) / резолв (Direct)Direct → ФИО собеседника (per-reader); Deal → заголовок сделки.
DealIdGuid?НетChat.DealIdСделка Deal-чата; null для Direct.
ProjectIdGuid?НетChat.ProjectIdПроект сделки; null для Direct.
LastMessageSeqlongДаChat.LastMessageSeqSeq последнего сообщения (граница read-receipt).
LastMessageAtDateTimeOffset?НетChat.LastMessageAtВремя последнего сообщения.
CreatedAtDateTimeOffsetДаChat.CreatedAtМомент создания чата.
MyRoleParticipantRole (enum→string)ДаParticipant(actor).RoleРоль актора в чате.
MyLastReadSeqlongДаParticipant(actor).LastReadSeqКурсор прочитанного актора.
IsMutedboolДаParticipant(actor).IsMutedПер-участниковая тишина актора.
ParticipantsIReadOnlyList<ChatParticipantModel>Даактивные ParticipantEntityсм. нижеСостав чата (только RemovedAt is null).

ChatParticipantModel — участник (проекция participant.md + messenger-user.md)

ПолеТипОбязательностьИсточникMaskingОписание
UserIdGuidДаParticipant.UserIdПользователь-участник.
DisplayNamestringДаMessengerUser (ФИО)Отображаемое ФИО (реплика; лаг → placeholder).
RoleParticipantRole (enum→string)ДаParticipant.RoleMember/Investor/Agent/DeveloperManager/PinManager.
JoinedAtDateTimeOffsetДаParticipant.JoinedAtМомент вступления.
IsSelfboolДаcomputedUserId == ActorUserId.
LastReadSeqlong?НетParticipant.LastReadSeqDeal→nullRead-receipt: заполнен только в Direct (INV-PART-5); в Dealnull.
LastReadAtDateTimeOffset?НетParticipant.LastReadAtDeal→null«Прочитано» собеседником в Direct; в Dealnull.

В Deal-чате пофамильный read-receipt не раскрывается (INV-PART-5); агрегированный счётчик прочитавших — на уровне сообщения (list-messages, вне карточки).

Алгоритм

  1. Контекст и доступ. Операцию выполняет участник чата (User); ActorUserId — из claims; требуется право messenger.chats.view. Доступ — по членству (не по scope/tenant).
  2. Проверка входа и членство. ChatId непустой, иначе VALIDATION_FAILED. Чат существует и актор — его активный участник (RemovedAt is null, INV-CHAT-4); иначе MESSENGER_CHAT_NOT_FOUND — не-участник неотличим от несуществующего (privacy, без enumeration). Карточка доступна в любом статусе чата (Active/Archived/Closed).
  3. Загрузка данных. Чат с активными участниками (RemovedAt is null) и реплики пользователей за display-имена. Сообщения/лента не грузятся (отдельная операция list-messages).
  4. Маппинг. Title: для Deal — заголовок сделки; для Direct — ФИО собеседника из реплики (per-reader, PII в колонке не оседает). Read-receipt: в Direct заполняются LastReadSeq/LastReadAt собеседника, в Deal оба поля null (INV-PART-5; пофамильный read-receipt в групповом чате не раскрывается). Enum передаются строкой; IsSelf вычисляется.
  5. Результат. Карточка ChatCardModel (метаданные чата, активные участники, read-state) → HTTP 200; чат не найден/не виден → MESSENGER_CHAT_NOT_FOUND (404).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400ChatId = Guid.Empty«Некорректный идентификатор чата»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»re-login
MESSENGER_CHAT_NOT_FOUND404чат не существует, soft-deleted, либо актор не активный участник (INV-CHAT-4, privacy)«Чат не найден»no retry

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

  • Новые optional-поля карточки/участника — additive; удаление/переименование/смена типа или значения enum ChatKind/ChatStatus/ParticipantRole — breaking (migration + rollout). Unknown ParticipantRole при чтении реплики — сохраняется как есть + warning (fallback, participant.md).
  • Logs: chat-card requested (ChatId, ActorUserId, Kind, число участников; ФИО — не в логи); metrics: messenger_chat_card_requests_total{result}, latency p95 (< 500 мс, README); traces — сквозной trace (traceparent); dashboard pin-messenger.

Acceptance Criteria

  • Happy path (Direct). Given актор — активный участник Direct-чата; When GET /{chatId}; Then 200, Title = ФИО собеседника (per-reader), read-receipt собеседника (LastReadSeq/LastReadAt) заполнен, в списке — только активные участники (RemovedAt is null, INV-CHAT-4).
  • Deal read-receipt скрыт. Given Deal-чат; When GET; Then per-участник LastReadSeq/LastReadAt = null (INV-PART-5; пофамильный read-receipt в групповом чате не раскрывается).
  • Скрытие не-участника. Given актор не участник или чат soft-deleted; When GET; Then 404 MESSENGER_CHAT_NOT_FOUND (privacy, без enumeration — неотличимо от несуществующего).
  • Любой статус чата. Given чат Archived/Closed; When GET; Then 200 (карточка доступна в любом статусе).
  • Лаг реплики. Given реплика участника отстаёт; When GET; Then DisplayName = placeholder, строка не отбрасывается, не ошибка.
  • Валидация. Given ChatId = Guid.Empty; When GET; Then 400 VALIDATION_FAILED.
  • PII. На любом исходе ФИО участников/собеседника в логи не пишутся.

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

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

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