UC-MSG-007. Авто-запись звонка по сделке и её выдача участнику
Назначение
Описывает сценарий: звонок в контексте сделки (как правило — конференция в Deal-чате,
UC-MSG-006) авто-записывается по типизированной политике
RecordingPolicy.ShouldAutoRecord(chatKind, sessionKind) (call-recording.md,
INV-REC-3): запись провизится при старте сессии с Trigger = AutoPolicy, InitiatedByUserId = null.
Участники уведомляются индикатором записи (согласие-нотис, INV-REC-4) до старта захвата. По завершении
сессии рекордер (jibri) финализирует файл и стримит байты в ingest-эндпоинт
(jibri-recording-ingest) → file storage (S3) → стабильный FileId
→ RecordingStatus = Available → интеграционное pin.messenger.call-recording-available.v1.
После этого участник скачивает запись пресайн-ссылкой (get-recording-download,
аудит ReadSensitive). Бинарь в messenger не хранится — только FileId + метаданные (INV-REC-6, по образцу
вложений). Событие call-recording-available — заодно шов будущего пайплайна транскрипции/конспектирования
(transcription-roadmap). Связан с контекстом messenger.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-MSG-007 |
| Контекст | messenger (см. docs/06-services/messenger/) |
| Тип | основной (+ A1 ручная остановка модератором, A2 сбой рекордера→Failed→retry, A3 запись ещё не готова, A4 retention→Deleted, A5 будущая транскрипция) |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Участники сессии (Агент/МенеджерПИН/Инвестор) — просмотр/скачивание; модератор — остановка; RecordingPolicy/рекордер (ExternalSystem) — авто-старт и финализация |
| Permissions / scope | JWT identity; messenger.recordings.view (участник сессии) — просмотр/скачивание; messenger.recordings.manage (модератор) — остановка; ingest — ExternalSystem (HMAC/mTLS, не JWT) |
| Tenant | не применяется строково — доступ по участию в сессии-владельце; DealTenantId — scope выгрузки в file storage + ключ Kafka |
| Record-level | доступ = актор ∈ участников сессии (пара Caller/Callee для 1:1 или ростер конференции); остановка — ConferenceRole = Moderator |
Предусловие
- Идёт сессия в контексте сделки (UC-MSG-006), подпадающая под
RecordingPolicy.ShouldAutoRecord(Deal-чат — как правило, запись включена). - Feature flag
RECORDINGS_ENABLED = true(иначеMSG_RECORDINGS_DISABLED, 503); доступны рекордер (jibri) и file storage.
Триггер
| Поле | Значение |
|---|---|
| Источник | Старт сессии (RecordingPolicy — не UI-кнопка) → авто-провизия записи; финализация — ingest-вебхук рекордера |
| Триггерящий элемент | старт конференции/звонка → RecordingPolicy → ingest POST /api/messenger/internal/call-recordings/{sessionId}/ingest |
Основной Поток
Нить — успешная авто-запись конференции по сделке и её выдача. Полный алгоритм и маппинг — в jibri-recording-ingest.md и get-recording-download.md; ниже — поток на уровне use-case.
- Решение политики. При старте сессии (UC-MSG-006, шаг 3)
RecordingPolicy.ShouldAutoRecord(chatKind = Deal, sessionKind)возвращает «записывать»: провизитсяCallRecordingEntity—Status = Pending,Trigger = AutoPolicy,InitiatedByUserId = null(INV-REC-3), XOR-связь с сессией-владельцем поSessionKind(INV-REC-1), не более одной активной записи на сессию (INV-REC-7). - Индикатор записи (согласие-нотис). До старта захвата участникам широковещается индикатор записи:
NoticeState = Announced,AnnouncedAt = now; поднимаетсяRecordingNoticeAnnouncedEvent→ push вchat:{ChatId}. Даже при авто-записи участники обязаны видеть индикатор (INV-REC-4); пер-участниковое согласие как гейт — роадмап, старт запись не гейтит. - Старт захвата. Рекордер (jibri) подтверждает захват через presence/событие media-стека
(jitsi-events):
Pending → Recording,StartedAt = now. Вход вRecordingприNoticeState = Pending— нарушение (INV-REC-4). ПоднимаетсяRecordingStartedEvent→ pushRecordingState(Started). - Завершение сессии. Сессия завершается (UC-MSG-006, шаг 10) или
модератор останавливает запись (A1):
Recording → Uploading,StoppedAt = now; pushRecordingState(Stopped). Рекордер начинает финализацию файла. - Ingest-финализация. jibri стримит финализированные байты в ingest-эндпоинт
POST /internal/call-recordings/{sessionId}/ingest(jibri-recording-ingest):ExternalSystem-актор, подписьX-Pin-Signature(HMAC + replay-window) или mTLS, fail-closed сверка имени комнаты соMediaRoom.RoomName(иначеMSG_JITSI_ROOM_UNKNOWN/MSG_JITSI_SIGNATURE_INVALID). - Выгрузка в file storage. Байты сохраняются в file storage (S3); возвращается стабильный
FileId; заполняются метаданныеFileName/ContentType/SizeBytes/DurationSeconds/Format/ExternalRecordingId(иначеMSG_STORAGE_UNAVAILABLE, 503, A2). Бинарь вmessengerне хранится (INV-REC-6). - Готовность.
Uploading → Available,AvailableAt = now;FileId/SizeBytes/DurationSeconds/Formatстановятся обязательны и далее иммутабельны (INV-REC-2/5). ПоднимаетсяRecordingAvailableEvent. - Факт наружу. Атомарно с переходом
→ Availableпубликуется интеграционноеpin.messenger.call-recording-available.v1(ключSessionId):crmприDealId != nullприкрепляетFileIdк таймлайну/встрече сделки,notificationsуведомляет участников. Домен-pushRecordingState(Available)несётFileId, но не URL (hub не отдаёт ссылок). - Скачивание участником. Участник запрашивает
GET /recordings/{recordingId}/download(get-recording-download): гейтmessenger.recordings.view+ участие в сессии; записьAvailable(иначеMSG_RECORDING_NOT_READY, 409, A3); возвращаетсяRecordingDownloadModelс короткоживущим пресайн-Url(Sensitive) иExpiresAt; чтение аудируется как ReadSensitive. Бинарь/URL в события и логи не попадают.
Диаграмма
Альтернативные Потоки
A1. Модератор остановил запись вручную
- Модератор жмёт «Остановить запись»
POST /conferences/{id}/recording/stop(гейтmessenger.recordings.manage,ConferenceRole = Moderator, иначеMSG_RECORDING_FORBIDDEN, 403):Recording → Uploading, далее — как основной поток с шага 5. Сессия при этом может продолжаться без записи.
A2. Сбой рекордера/выгрузки → Failed → retry
- Рекордер недоступен / сбой захвата / сбой выгрузки в file storage: запись переходит в
Failed,FailureReason = RecorderUnavailable/UploadFailed/Timeout/Error(INV-REC-2); поднимаетсяRecordingFailedEvent→ pushRecordingState(Failed)+ внутренний алерт. - Транзиентные сбои выгрузки/финализации добирает фоновый
recording-finalize-retry (идемпотентно, обновляет только
неконечные/
Failed-по-транзиенту статусы); при исчерпании ретраев запись остаётсяFailed— сама сессия и еёconference-ended.v1этим не блокируются.
A3. Запись ещё не готова
- Участник запрашивает скачивание, пока запись
Pending/Recording/Uploading→MSG_RECORDING_NOT_READY(409). Клиент ждёт pushRecordingState(Available)/ событиеcall-recording-available.
A4. Истёк срок хранения → Deleted
- Наступил
RetentionUntil(ADR-0032): retention-джоба переводитAvailable → Deleted,FileIdочищается, файл удаляется из file storage; строка записи сохраняется как факт (метаданные без бинаря). Скачивание после —MSG_RECORDING_FILE_MISSING(400) /MSG_RECORDING_NOT_FOUND(404).
A5. Будущая транскрипция/конспект (шов)
- Событие
call-recording-available(несётFileId/SessionId/DealId?/DurationSeconds) — триггер будущего STT/суммаризация-пайплайна: отдельная сущностьCallMeetingInsightEntity(1:1 к записи), поляTranscriptFileId/SummaryFileIdзарезервированы как шов (transcription-roadmap). Текущую модель не блокирует.
Постусловие
- Создана
CallRecordingEntityс терминальным статусом:Available(заданыFileId+SizeBytes/DurationSeconds/Format, иммутабельны, INV-REC-5) либоFailed(FailureReason); при истечении retention —Deleted(FileIdочищен). - Бинарь — в file storage (S3), в
messengerтолькоFileId+ метаданные (INV-REC-6); индикатор записи был разослан участникам (NoticeState = Announced, INV-REC-4). - При
Availableопубликовано ровно одноpin.messenger.call-recording-available.v1(ключSessionId) —crmприкрепил запись к сделке (приDealId != null); участник может скачать пресайн-ссылкой (аудит ReadSensitive), URL/бинарь в события и логи не попали.
Инварианты, статусы и валидации
Переходы RecordingStatus (полный перечень)
| From | To | Условие перехода | Инициатор |
|---|---|---|---|
[*] | Pending | RecordingPolicy.ShouldAutoRecord = записывать (INV-REC-3) | политика (старт сессии) |
Pending | Recording | рекордер подтвердил захват; NoticeState = Announced (INV-REC-4) | media-стек (presence) |
Recording | Uploading | сессия завершена / ручная остановка модератором (A1) | сессия / модератор |
Uploading | Available | бинарь выгружен, задан FileId + метаданные (INV-REC-2/5) | ingest (jibri) |
| любой | Failed | сбой рекордера/выгрузки (FailureReason, INV-REC-2) | media-стек / worker (A2) |
Available | Deleted | наступил RetentionUntil (ADR-0032) — FileId очищен | retention-джоба (A4) |
NoticeState: Pending → Announced (индикатор разослан) — обязателен до входа в Recording
(INV-REC-4). Терминальные: Available (до Deleted), Failed, Deleted.
Инварианты (полный перечень)
| Инвариант | Смысл |
|---|---|
| INV-REC-1 | Ровно один из CallSessionId/ConferenceSessionId задан и согласован с SessionKind (XOR). |
| INV-REC-2 | Допустимы только перечисленные переходы; FileId/SizeBytes/DurationSeconds/Format обязательны в Available, null до него. |
| INV-REC-3 | Авто-запись сессии в контексте сделки: Trigger = AutoPolicy, InitiatedByUserId = null. |
| INV-REC-4 | NoticeState = Announced (индикатор разослан) обязателен до входа в Recording. |
| INV-REC-5 | FileId/SizeBytes/DurationSeconds/Format в Available иммутабельны. |
| INV-REC-6 | Бинарь не хранится в messenger — только FileId (file storage) + метаданные. |
| INV-REC-7 | Не более одной активной (Pending/Recording/Uploading) записи на сессию. |
Валидации границы ingest (media-стек → messenger)
- Подпись
X-Pin-Signature(HMAC над сырым телом) + свежестьX-Pin-Timestamp(окно replay ≤ 5 мин) — иначеMSG_JITSI_SIGNATURE_INVALID(401). RoomName/{sessionId}совпадает с провизированной комнатой сессии (fail-closed) — иначеMSG_JITSI_ROOM_UNKNOWN(403).Outcome = Completedтребует непустую частьfile— иначеMSG_RECORDING_FILE_MISSING(400).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
MSG_RECORDINGS_DISABLED | 503 | RECORDINGS_ENABLED = false | «Запись временно недоступна» |
MSG_RECORDING_PROVISION_FAILED | 503 | не удалось провизировать запись при старте сессии | Сессия идёт без записи; ретрай политики |
MSG_RECORDING_NOT_FOUND | 404 | запись не найдена / актор не участник сессии | «Запись не найдена» |
MSG_RECORDING_FORBIDDEN | 403 | остановка не модератором (нет recordings.manage) | Скрыть «остановить запись» |
MSG_RECORDING_NOT_READY | 409 | скачивание при Pending/Recording/Uploading (A3) | Ждать RecordingState(Available) |
MSG_STORAGE_UNAVAILABLE | 503 | file storage недоступен при выгрузке (A2) | Ретрай финализации фоном |
MSG_RECORDING_FILE_MISSING | 400 | FileId отсутствует/очищен (retention, A4) | «Файл записи недоступен» |
MSG_JITSI_ROOM_UNKNOWN | 403 | ingest: имя комнаты не совпало со MediaRoom.RoomName (fail-closed) | — (внутренний вебхук) |
MSG_JITSI_SIGNATURE_INVALID | 401 | ingest: неверная подпись X-Pin-Signature / вне replay-window | — (внутренний вебхук) |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
RecordingNoticeAnnouncedEvent | NoticeState → Announced (шаг 2) | push индикатора записи в chat:{ChatId} / user:{party} |
RecordingStartedEvent | → Recording (шаг 3) | push RecordingState(Started) |
RecordingStoppedEvent | → Uploading (шаг 4 / A1) | push RecordingState(Stopped) |
RecordingAvailableEvent | → Available (шаг 7) | доменный push RecordingState(Available) (без URL) + интеграционное событие |
RecordingFailedEvent | → Failed (A2) | push RecordingState(Failed) + внутренний алерт |
RecordingDeletedEvent | → Deleted (A4, retention) | запрос удаления в file storage; очистка FileId |
Все — доменные события; hub-push — non-PII, без ссылок/URL (RecordingState несёт FileId только при
Available).
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
RecordingAvailableEvent | pin.messenger.call-recording-available.v1 | атомарно с Uploading → Available (ключ SessionId) | crm (прикрепить FileId к сделке при DealId != null), notifications (уведомить участников) |
Публикация — через транзакционный outbox из ingest-эндпоинта (persist-then-push). В событии нет URL/секретов — потребитель берёт пресайн сам. Это же событие — триггер будущего пайплайна транскрипции (A5). Контракт — call-recording-available.md.
Конкурентность, идемпотентность и восстановление
- Ingest. Идемпотентен по
RecordingId/SessionId, version-guard поRecordingStatus: повтор финализации на терминале (Available/Failed) → no-op с текущим статусом; выгрузка в file storage не повторяется. - Старт захвата.
RecordingStatusChanged(on)(presence) идемпотентен, порядок — поOccurredAt; вход вRecordingприNoticeState = Pending— нарушение (INV-REC-4). - Одна активная запись (INV-REC-7). Партиал-уникальный индекс активных записей не даёт завести вторую
на сессию; повтор провизии →
MSG_RECORDING_ALREADY_ACTIVE. - Транзиентные сбои выгрузки. Запись остаётся
Uploading, добирается фоновым recording-finalize-retry идемпотентно; исчерпание ретраев →Failed. Сама сессия и еёconference-ended.v1этим не блокируются (decoupling: запись переживает звонок). - Доставка события.
call-recording-available.v1— атомарно сUploading → Available(транзакционный outbox), at-least-once; консюмеры (crm/notifications) идемпотентны поRecordingId(first-seen); ключSessionId. - Retention (A4).
Available → DeletedпоRetentionUntil—FileIdочищен, файл удалён; скачивание после →MSG_RECORDING_FILE_MISSING(400) /MSG_RECORDING_NOT_FOUND(404).
Кросс-сервисные контракты, порядок событий и промежуточные состояния
Ожидает (входные гарантии):
- media-стек:
RecordingStatusChanged(on/off)(presence — старт/стоп захвата), jibri finalize ingest (подписанный, room-match, jibri-recording-ingest.md). - file storage (S3): стабильный
FileIdпри выгрузке, пресайн для скачивания, удаление по retention. При недоступности —MSG_STORAGE_UNAVAILABLE(503), ретрай финализации фоном.
Предоставляет:
pin.messenger.call-recording-available.v1— топик.v1,ContractVersion = 1, 3 партиции, ключSessionId. Payload:RecordingId/SessionKind/SessionId/ChatId?/DealId?/TenantId?/MediaType/FileId/Format/DurationSeconds/SizeBytes/Trigger/StartedAt/EndedAt/AvailableAt— без URL/секретов/бинаря (call-recording-available.md). Консюмеры:crm(приDealId != nullприкрепитьFileId, SLA < 1 мин),notifications(уведомить участников).- Домен-push
RecordingState(...)вchat:{ChatId}/user:{party}— non-PII, без URL.
Порядок событий (важно для консюмеров):
conference-ended.v1(UC-MSG-006) иcall-recording-available.v1— независимые события: запись готова позже завершения сессии (время финализации/выгрузки).crmполучает карточку встречи изconference-ended, аFileIdзаписи — отдельным, более поздним событием и прикрепляет к уже существующей карточке. Консюмерconference-endedне должен ждать записи в своём обработчике.
Промежуточные состояния:
- Участник видит индикатор записи (
Announced) до старта захвата (INV-REC-4). После завершения сессии — «запись обрабатывается» (Uploading): скачивание →MSG_RECORDING_NOT_READY(409, A3), пока не придётRecordingState(Available)/ событие.
Наблюдаемость и НФТ
- Записи редки (по сделкам); выгрузка — потоковая memory-bounded; латентность готовности зависит от длительности/размера файла.
- Логи:
ingest received/uploaded/available/failed(sessionId,RecordingId,SizeBytes,Format,result); байты записи, пресайн-URL и секрет подписи — не логируются. - Метрики:
messenger_recording_ingest_total{result}, upload bytes/latency p95, доляFailed/retry; alert наFailed/storage-unavailable. - Доступ:
messenger.recordings.view(участник — просмотр/скачивание, аудит ReadSensitive),messenger.recordings.manage(модератор — остановка); ingest —ExternalSystem(HMAC/mTLS, не JWT).
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Звонок по сделке авто-записывается по RecordingPolicy: Trigger = AutoPolicy, InitiatedByUserId = null (INV-REC-3) | Тест: старт Deal-конференции → запись Pending, AutoPolicy |
| AC2 | Индикатор записи разослан (NoticeState = Announced) до входа в Recording (INV-REC-4) | Тест: Recording при NoticeState = Pending → нарушение |
| AC3 | Ingest fail-closed: неверная подпись → 401; чужая комната → 403 (сверка MediaRoom.RoomName) | Тест: bad X-Pin-Signature → 401 SIGNATURE_INVALID; unknown room → 403 ROOM_UNKNOWN |
| AC4 | При → Available заданы FileId+SizeBytes+DurationSeconds+Format (иммутабельны), бинарь не в БД (INV-REC-5/6) | Тест: call_recordings без бинаря; Available без FileId → INVALID_STATE |
| AC5 | Ровно одно call-recording-available.v1 (ключ SessionId), без URL/секретов | Тест: outbox содержит событие; payload без URL |
| AC6 | Скачивание — участнику Available-записи, пресайн-Url короткоживущий, аудит ReadSensitive; не готова → 409 | Тест A3: Uploading → 409 NOT_READY; Available → пресайн + audit-строка |
| AC7 | Сбой → Failed (FailureReason); транзиент добирает recording-finalize-retry идемпотентно; сессия не блокируется | Тест A2: сбой выгрузки → Failed; retry без дубля; conference-ended.v1 опубликован |
| AC8 | conference-ended и call-recording-available независимы: готовность записи приходит позже; консюмер conference-ended не ждёт записи | Тест: conference-ended обработан без записи; позже call-recording-available прикрепляет FileId к карточке |
| AC9 | Retention: Available → Deleted по RetentionUntil, FileId очищен; скачивание после → FILE_MISSING/NOT_FOUND | Тест A4: наступил RetentionUntil → Deleted; download → 400/404 |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-MSG-005. Сигналинг аудио/видео-звонка, UC-MSG-006. Групповой звонок-конференция в чате сделки
- calls (1): Jitsi-звонки: план верификации и приёмки
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API (дом алгоритма/маппинга): ingest — jibri-recording-ingest.md; скачивание — get-recording-download.md; карточка/список — get-recording.md, list-conference-recordings.md; остановка — stop-recording.md.
- Domain: call-recording.md (INV-REC-1…7,
RecordingStatus/RecordingTrigger/RecordingFormat/RecordingNoticeState/RecordingFailureReason,RecordingPolicy), conference-session.md / call-session.md (сессии-владельцы,SessionKind). - Realtime: messenger-hub.md (
RecordingState, без URL); presence/финализация media-стека: jitsi-events.md; фоновый ретрай: recording-finalize-retry.md. - События: call-recording-available.md; групповой звонок — UC-MSG-006; пайплайн записи: recording-pipeline.md; роадмап транскрипции (A5): transcription-roadmap.md.
- Permission-каталог: permission.md (
messenger.recordings.view,messenger.recordings.manage). - ADR: ADR-0062 (media-стек и запись), ADR-0032 retention, ADR-0056.