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

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-метаданные — для разбора спора по сделке/личной переписке. Каждый вызов создаёт обязательную запись аудита доступа.

Metadata

ПолеЗначение
Сервис/контекстmessenger
API areaadmin (Служба заботы; /api/messenger/admin)
Feature grouparbitrage
Статусdraft
Документdocs/06-services/messenger/api/admin/get-chat-transcript.md

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

ПроверкаЗначение
Actor typeOperator(Admin) — арбитр Службы заботы / супервизор / Менеджер ПИН / администратор платформы
Auth policy / Permissionssupport.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 isolationscope-bypass tenant-интерсептора с явным scope-guard по DealTenantId (ADR-0052); строки чата — не tenanted
Auditread-sensitive (обязательный): каждый доступ пишет append-only TranscriptAccessLogEntity (шаг 5) + структурный sensitive-read log; AccessReason обязателен

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ChatIdrouteGuidДа!= Guid.Empty; существует
ArbitrageCaseIdqueryGuid?Нетесли задан — существующий кейс support (логическая ссылка)корреляция аудита
AccessReasonquerystringДа4..512, не пустое после trimпишется в аудит; в общие логи не пишется
MaskingLevelqueryTranscriptMaskingLevel?Нетenum (см. ниже); default Standard; Full требует support.arbitrage.manage (шаг 4)политика PII
IncludeEditHistoryquerybool?Нетdefault trueистория правок — Sensitive
PagingqueryPagingQueryНет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Описание
ChatIdGuidДаChatEntity.IdЧат.
KindChatKindДаChatEntity.KindDirect/Deal.
TitlestringДаChatEntity.Titleпо политикеЗаголовок; для Direct — резолв per-reader.
DealIdGuid?НетChatEntity.DealIdСделка (Deal-чат).
DealTenantIdGuid?НетChatEntity.DealTenantIdScope арбитража.
ParticipantsIReadOnlyList<TranscriptParticipantModel>ДаParticipantEntity + репликателефон по политикеСостав (вкл. вышедших: RemovedAt).
MessagesIReadOnlyList<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).

Алгоритм

  1. Контекст и доступ. Операцию выполняет арбитр Службы заботы (Operator(Admin)); требуется право support.arbitrage.read (иначе MESSENGER_ARBITRAGE_FORBIDDEN). Доступ — не по членству (арбитр не участник чата), а по праву арбитража и покрытию scope над Chat.DealTenantId (шаг 4). Каждый вызов обязательно аудируется.
  2. Проверка входа. ChatId непустой; AccessReason4..512 символов; Paging.Take ∈ 1..200; MaskingLevel — известное значение enum TranscriptMaskingLevel; иначе VALIDATION_FAILED. Уровень Full требует дополнительно права support.arbitrage.manage — иначе MESSENGER_ARBITRAGE_FORBIDDEN.
  3. Загрузка данных. Чат по ChatId (нет → MESSENGER_CHAT_NOT_FOUND); участники чата, включая вышедших (RemovedAt); страница сообщений по Seq (asc), при IncludeEditHistory — со скрытой историей правок (MessageEditEntity); реплики пользователей для display-имён.
  4. Проверка 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).
  5. Обязательный аудит доступа. Завести append-only запись TranscriptAccessLogEntity (ChatId, AccessedByUserId, ArbitrageCaseId?, AccessReason, MaskingLevel, MessageCount, AccessedAt) — tamper-evident след доступа; переписка при этом не изменяется. Для observability поднимается доменный сигнал sensitive-read ChatTranscriptAccessed. Нового Kafka-топика не вводится: аудит — персистентная запись + структурный audit-sink.
  6. Согласованность (fail-closed). Фиксируется только аудит-запись. Сбой её записи ⇒ вся операция отклоняется с MESSENGER_AUDIT_WRITE_FAILED — нет доступа без следа.
  7. Результат. ChatTranscriptModel (чат, участники, сообщения, история правок, пагинация) → HTTP 200; тела применяют выбранную маскировку.

Диаграмма

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

ТипДетали
Integration eventsnone (нового Kafka-топика не вводим; аудит — персистентная запись)
External callsnone (реплики и переписка — локальная схема messenger)
Cache / projections / searchnone (прямой запрос по ix_messages_chat_id/ux_messages_chat_seq)
Notificationsnone (участникам чата доступ арбитра не раскрывается)
Auditappend-only TranscriptAccessLogEntity (обязательно, fail-closed) + sensitive-read log

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400пустой ChatId, AccessReason вне 4..512, неизвестный MaskingLevel/Take«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
MESSENGER_ARBITRAGE_FORBIDDEN403нет support.arbitrage.read (или Full без support.arbitrage.manage)«Недостаточно прав»no retry
MESSENGER_ARBITRAGE_SCOPE_DENIED403scope арбитра не покрывает Chat.DealTenantId (или Direct без платформы)«Недостаточно прав для этого чата»no retry
MESSENGER_CHAT_NOT_FOUND404ChatId не существует«Чат не найден»no retry
MESSENGER_AUDIT_WRITE_FAILED503сбой записи аудита доступа (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.

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

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

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