GET /api/messenger/admin/chats/{chatId}/transcript — Выгрузка переписки для арбитража
Read-only выгрузка полной переписки чата для арбитража Службы заботы
(ADR-0055). Единственная read-модель, отдающая
скрытую историю правок (MessageEditEntity, INV-MSG-2) и Sensitive-поля
IpAddress/UserAgent (INV-MSG-6). Доступ гейтится и обязательно аудируется; PII маскируется по
политике. Владелец — контекст messenger, API area admin, feature group arbitrage.
Назначение
Отдать арбитру Службы заботы постранично сообщения чата (текст/вложение/карточка), состав участников,
историю правок и origin-метаданные — для разбора спора по сделке/личной переписке. Каждый вызов создаёт
обязательную запись аудита доступа.
| Поле | Значение |
|---|
| Сервис/контекст | messenger |
| API area | admin (Служба заботы; /api/messenger/admin) |
| Feature group | arbitrage |
| Статус | draft |
| Документ | docs/06-services/messenger/api/admin/get-chat-transcript.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Operator(Admin) — арбитр Службы заботы / супервизор / Менеджер ПИН / администратор платформы |
| Auth policy / Permissions | support.arbitrage.read (read-only арбитраж; дополняет support.arbitrage.manage для вмешательств — permission.md); area-гейт /api/messenger/admin |
| Scope (RBAC) | Deal-чат — застройщик своего DealTenantId (арбитр с scope над DealTenantId); Direct-чат (DealTenantId = null) — только платформа (супервизор заботы / admin) |
| Record-level | доступ не по членству (арбитр не участник): по support.arbitrage.read + покрытие scope над Chat.DealTenantId (шаг 4) |
| Tenant isolation | scope-bypass tenant-интерсептора с явным scope-guard по DealTenantId (ADR-0052); строки чата — не tenanted |
| Audit | read-sensitive (обязательный): каждый доступ пишет append-only TranscriptAccessLogEntity (шаг 5) + структурный sensitive-read log; AccessReason обязателен |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/messenger/admin/chats/{chatId}/transcript |
| Success status | 200 |
| Idempotency header | не применяется (безопасный GET; аудит доступа пишется на каждый вызов — by design) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ChatId | route | Guid | Да | != Guid.Empty; существует | — |
ArbitrageCaseId | query | Guid? | Нет | если задан — существующий кейс support (логическая ссылка) | корреляция аудита |
AccessReason | query | string | Да | 4..512, не пустое после trim | пишется в аудит; в общие логи не пишется |
MaskingLevel | query | TranscriptMaskingLevel? | Нет | enum (см. ниже); default Standard; Full требует support.arbitrage.manage (шаг 4) | политика PII |
IncludeEditHistory | query | bool? | Нет | default true | история правок — Sensitive |
Paging | query | PagingQuery | Нет | Take 1..200 (default 100), Offset >= 0; порядок по Seq asc | защита от unbounded |
Enum TranscriptMaskingLevel
| Значение | Эффект | Право |
|---|
Standard | телефоны участников → маска +7***NNNN; в телах — токены платёжных/эскроу-реквизитов маскируются политикой; ФИО и тексты видимы | support.arbitrage.read |
Full | без маскирования (полные ФИО/телефоны/тексты) — усиленный разбор | дополнительно support.arbitrage.manage |
Модель ответа ChatTranscriptModel
Дом полей — chat.md, participant.md,
message.md. Ниже — только состав проекции арбитража.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
ChatId | Guid | Да | ChatEntity.Id | — | Чат. |
Kind | ChatKind | Да | ChatEntity.Kind | — | Direct/Deal. |
Title | string | Да | ChatEntity.Title | по политике | Заголовок; для Direct — резолв per-reader. |
DealId | Guid? | Нет | ChatEntity.DealId | — | Сделка (Deal-чат). |
DealTenantId | Guid? | Нет | ChatEntity.DealTenantId | — | Scope арбитража. |
Participants | IReadOnlyList<TranscriptParticipantModel> | Да | ParticipantEntity + реплика | телефон по политике | Состав (вкл. вышедших: RemovedAt). |
Messages | IReadOnlyList<TranscriptMessageModel> | Да | MessageEntity (страница по Seq) | тело/IP/UA по политике | Сообщения ленты. |
Paging | мета пагинации | Да | пагинация | — | Total/Take/Offset/HasMore. |
TranscriptParticipantModel: UserId, Role : ParticipantRole, DisplayName (маска по политике),
Phone? (маска), JoinedAt, RemovedAt?, LastReadSeq.
TranscriptMessageModel: Seq, Kind : MessageKind, SenderUserId?, SenderRole?, SentAt, EditedAt?,
DeletedAt?, Body?/Caption?/FileName?/CardType?/CardText? (по подтипу TPH), IpAddress?,
UserAgent? (Sensitive — только этот метод), Edits : IReadOnlyList<TranscriptEditModel>?.
TranscriptEditModel: Revision, PreviousBody?, PreviousCaption?, EditedAt, EditorUserId,
IpAddress?, UserAgent? (message.md MessageEditEntity).
Алгоритм
- Контекст и доступ. Операцию выполняет арбитр Службы заботы (
Operator(Admin)); требуется право
support.arbitrage.read (иначе MESSENGER_ARBITRAGE_FORBIDDEN). Доступ — не по членству (арбитр не
участник чата), а по праву арбитража и покрытию scope над Chat.DealTenantId (шаг 4). Каждый вызов
обязательно аудируется.
- Проверка входа.
ChatId непустой; AccessReason — 4..512 символов; Paging.Take ∈ 1..200;
MaskingLevel — известное значение enum TranscriptMaskingLevel; иначе VALIDATION_FAILED. Уровень
Full требует дополнительно права support.arbitrage.manage — иначе MESSENGER_ARBITRAGE_FORBIDDEN.
- Загрузка данных. Чат по
ChatId (нет → MESSENGER_CHAT_NOT_FOUND); участники чата, включая
вышедших (RemovedAt); страница сообщений по Seq (asc), при IncludeEditHistory — со скрытой историей
правок (MessageEditEntity); реплики пользователей для display-имён.
- Проверка scope и маскирование. Для
Deal-чата арбитр обязан покрывать Chat.DealTenantId своим
scope — иначе MESSENGER_ARBITRAGE_SCOPE_DENIED; для Direct-чата (DealTenantId = null) требуется
платформенный scope (супервизор/admin). Применить MaskingLevel: при Standard — маскировать телефоны
и токены платёжных/эскроу-реквизитов, ФИО/тексты видимы; при Full — без маскирования. IpAddress,
UserAgent и история правок отдаются как Sensitive-поля — это единственная read-модель, где они
доступны (INV-MSG-6, INV-MSG-2).
- Обязательный аудит доступа. Завести append-only запись
TranscriptAccessLogEntity (ChatId,
AccessedByUserId, ArbitrageCaseId?, AccessReason, MaskingLevel, MessageCount, AccessedAt) —
tamper-evident след доступа; переписка при этом не изменяется. Для observability поднимается доменный
сигнал sensitive-read ChatTranscriptAccessed. Нового Kafka-топика не вводится: аудит — персистентная
запись + структурный audit-sink.
- Согласованность (fail-closed). Фиксируется только аудит-запись. Сбой её записи ⇒ вся операция
отклоняется с
MESSENGER_AUDIT_WRITE_FAILED — нет доступа без следа.
- Результат.
ChatTranscriptModel (чат, участники, сообщения, история правок, пагинация) → HTTP 200;
тела применяют выбранную маскировку.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none (нового Kafka-топика не вводим; аудит — персистентная запись) |
| External calls | none (реплики и переписка — локальная схема messenger) |
| Cache / projections / search | none (прямой запрос по ix_messages_chat_id/ux_messages_chat_seq) |
| Notifications | none (участникам чата доступ арбитра не раскрывается) |
| Audit | append-only TranscriptAccessLogEntity (обязательно, fail-closed) + sensitive-read log |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | пустой ChatId, AccessReason вне 4..512, неизвестный MaskingLevel/Take | «Проверьте параметры запроса» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
MESSENGER_ARBITRAGE_FORBIDDEN | 403 | нет support.arbitrage.read (или Full без support.arbitrage.manage) | «Недостаточно прав» | no retry |
MESSENGER_ARBITRAGE_SCOPE_DENIED | 403 | scope арбитра не покрывает Chat.DealTenantId (или Direct без платформы) | «Недостаточно прав для этого чата» | no retry |
MESSENGER_CHAT_NOT_FOUND | 404 | ChatId не существует | «Чат не найден» | no retry |
MESSENGER_AUDIT_WRITE_FAILED | 503 | сбой записи аудита доступа (fail-closed) | «Сервис временно недоступен» | retry |
Совместимость и наблюдаемость
- Новые optional query-параметры и поля моделей — additive; удаление/переименование/смена типа поля или
значения enum — breaking (migration + rollout). Sensitive-поля (
IpAddress/UserAgent/Edits) — только
этот метод; в другие read-модели и события не отдаются.
- Logs:
chat transcript accessed (ChatId, AccessedByUserId, ArbitrageCaseId?, MaskingLevel,
MessageCount; AccessReason/тела/ФИО/IP не пишутся в общий лог); metrics:
messenger_transcript_access_total{result,masking}, latency p95; traces — сквозной trace (traceparent);
alert на всплеск Full-доступов; dashboard pin-messenger.
Acceptance Criteria
- Happy path (Deal, Standard). Given арбитр с
support.arbitrage.read, чей scope покрывает
Chat.DealTenantId, и существующий Deal-чат; When GET .../transcript?accessReason&masking=Standard;
Then 200, сообщения страницей по Seq asc, телефоны маскированы +7***NNNN, история правок включена
(IncludeEditHistory default true), append-only TranscriptAccessLogEntity записана.
- Full требует manage. Given
MaskingLevel = Full, а у арбитра только support.arbitrage.read; When
запрос; Then 403 MESSENGER_ARBITRAGE_FORBIDDEN, ни доступа, ни аудит-записи.
- Full со manage. Given
Full и support.arbitrage.manage; When запрос; Then 200 без маскирования
(полные ФИО/телефоны/тексты), аудит несёт MaskingLevel = Full.
- Direct — только платформа. Given
Direct-чат (DealTenantId = null) и арбитр без платформенного
scope; When запрос; Then 403 MESSENGER_ARBITRAGE_SCOPE_DENIED.
- Чужой tenant сделки. Given
Deal-чат вне scope арбитра; When запрос; Then
403 MESSENGER_ARBITRAGE_SCOPE_DENIED.
- Чат не найден. Given несуществующий
ChatId; When запрос; Then 404 MESSENGER_CHAT_NOT_FOUND.
- Валидация. Given
AccessReason вне 4..512 / неизвестный MaskingLevel / Take > 200; When запрос;
Then 400 VALIDATION_FAILED, аудит не пишется.
- Fail-closed аудит. Given сбой записи
TranscriptAccessLogEntity; When запрос; Then
503 MESSENGER_AUDIT_WRITE_FAILED, тело переписки не отдаётся — доступа без следа нет.
- Sensitive/PII. На любом успешном исходе
IpAddress/UserAgent/история правок отдаются только этим
методом (INV-MSG-6, INV-MSG-2); AccessReason/тела/ФИО/IP в общий лог не пишутся; всплеск
Full-доступов даёт alert.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы