GET /api/messenger/chats/{chatId} — Карточка чата (участники + read-state)
Назначение
Отдать активному участнику чата (ChatEntity) метаданные чата (Kind/Status/
заголовок/сделку), список активных участников (participant.md) с
отображаемыми ФИО (реплика messenger-user.md) и read-receipt: в Direct —
пофамильно (2 участника), в групповом Deal-чате — без пофамильного раскрытия по умолчанию (INV-PART-5).
| Поле | Значение |
|---|
| Сервис/контекст | messenger |
| API area | chats |
| Feature group | chats |
| Статус | draft |
| Документ | docs/06-services/messenger/api/chats/get-chat.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (участник чата любой стороны) |
| Auth policy / Permissions | permission 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 строково) |
| Audit | read; ФИО участников/собеседника в логи не пишутся (PII) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/messenger/chats/{chatId} |
| Success status | 200 |
| Idempotency header | не применяется (безопасный GET) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ActorUserId | identity claims | Guid | Да | Required; permission messenger.chats.view | record-level (членство) |
ChatId | route | Guid | Да | != Guid.Empty; существующий чат-участник актора (шаг 2) | — |
Модель ответа ChatCardModel
Проекция chat.md + participant.md; дом полей — там, ниже
только форма карточки.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
ChatId | Guid | Да | Chat.Id | — | Идентификатор чата. |
Kind | ChatKind (enum→string) | Да | Chat.Kind | — | Direct / Deal. |
Status | ChatStatus (enum→string) | Да | Chat.Status | — | Active/Archived/Closed. |
Title | string | Да | Chat.Title (Deal) / резолв (Direct) | — | Direct → ФИО собеседника (per-reader); Deal → заголовок сделки. |
DealId | Guid? | Нет | Chat.DealId | — | Сделка Deal-чата; null для Direct. |
ProjectId | Guid? | Нет | Chat.ProjectId | — | Проект сделки; null для Direct. |
LastMessageSeq | long | Да | Chat.LastMessageSeq | — | Seq последнего сообщения (граница read-receipt). |
LastMessageAt | DateTimeOffset? | Нет | Chat.LastMessageAt | — | Время последнего сообщения. |
CreatedAt | DateTimeOffset | Да | Chat.CreatedAt | — | Момент создания чата. |
MyRole | ParticipantRole (enum→string) | Да | Participant(actor).Role | — | Роль актора в чате. |
MyLastReadSeq | long | Да | Participant(actor).LastReadSeq | — | Курсор прочитанного актора. |
IsMuted | bool | Да | Participant(actor).IsMuted | — | Пер-участниковая тишина актора. |
Participants | IReadOnlyList<ChatParticipantModel> | Да | активные ParticipantEntity | см. ниже | Состав чата (только RemovedAt is null). |
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
UserId | Guid | Да | Participant.UserId | — | Пользователь-участник. |
DisplayName | string | Да | MessengerUser (ФИО) | — | Отображаемое ФИО (реплика; лаг → placeholder). |
Role | ParticipantRole (enum→string) | Да | Participant.Role | — | Member/Investor/Agent/DeveloperManager/PinManager. |
JoinedAt | DateTimeOffset | Да | Participant.JoinedAt | — | Момент вступления. |
IsSelf | bool | Да | computed | — | UserId == ActorUserId. |
LastReadSeq | long? | Нет | Participant.LastReadSeq | Deal→null | Read-receipt: заполнен только в Direct (INV-PART-5); в Deal — null. |
LastReadAt | DateTimeOffset? | Нет | Participant.LastReadAt | Deal→null | «Прочитано» собеседником в Direct; в Deal — null. |
В Deal-чате пофамильный read-receipt не раскрывается (INV-PART-5); агрегированный счётчик прочитавших —
на уровне сообщения (list-messages, вне карточки).
Алгоритм
- Контекст и доступ. Операцию выполняет участник чата (
User); ActorUserId — из claims; требуется
право messenger.chats.view. Доступ — по членству (не по scope/tenant).
- Проверка входа и членство.
ChatId непустой, иначе VALIDATION_FAILED. Чат существует и актор — его
активный участник (RemovedAt is null, INV-CHAT-4); иначе MESSENGER_CHAT_NOT_FOUND — не-участник
неотличим от несуществующего (privacy, без enumeration). Карточка доступна в любом статусе чата
(Active/Archived/Closed).
- Загрузка данных. Чат с активными участниками (
RemovedAt is null) и реплики пользователей за
display-имена. Сообщения/лента не грузятся (отдельная операция list-messages).
- Маппинг.
Title: для Deal — заголовок сделки; для Direct — ФИО собеседника из реплики
(per-reader, PII в колонке не оседает). Read-receipt: в Direct заполняются LastReadSeq/LastReadAt
собеседника, в Deal оба поля null (INV-PART-5; пофамильный read-receipt в групповом чате не
раскрывается). Enum передаются строкой; IsSelf вычисляется.
- Результат. Карточка
ChatCardModel (метаданные чата, активные участники, read-state) → HTTP 200;
чат не найден/не виден → MESSENGER_CHAT_NOT_FOUND (404).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | ChatId = Guid.Empty | «Некорректный идентификатор чата» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | re-login |
MESSENGER_CHAT_NOT_FOUND | 404 | чат не существует, 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. На любом исходе ФИО участников/собеседника в логи не пишутся.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы