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

Entity / Aggregate: CallRecordingEntity — запись звонка (хранение в file storage)

Дом полей агрегата «Запись звонка» контекста messenger. Факт и жизненный цикл записи 1:1-звонка (call-session.md) или конференции (conference-session.md) с policy-driven авто-стартом. Бинарь записи хранится во внешнем file storage (S3), в messenger — только FileId + метаданные (по образцу вложений, message.md, INV-MSG-7). Все проекции (карточка записи, история звонков, событие «запись готова») ссылаются сюда.

Назначение

CallRecordingEntity — отдельный агрегат жизненного цикла записи: Pending → Recording → Uploading → Available (плюс Failed, Deleted). Отдельный (не owned-child), потому что запись переживает звонок: захват, выгрузка в file storage и будущая транскрипция (transcription-roadmap) выполняются асинхронно и после завершения сессии. Старт — по типизированной политике RecordingPolicy (авто-запись для звонков в контексте сделки; Trigger = AutoPolicy, инициатор-пользователь отсутствует). Готовый бинарь выгружается в file storage сервис (S3), запись хранит стабильный FileId — по нему позже выдаётся ссылка на скачивание. Даже при авто-записи участники уведомляются индикатором записи (INV-REC-4) — согласие в модели фиксируется, но старт не гейтит (пер-участниковое согласие — роадмап). file storage событий не публикует — «доступность записи» наружу отдаёт messenger (событие ../events/call-recording-available.md).

Классификация

ПолеЗначение
Контекстmessenger
KindAggregateRoot (не owned-child — асинхронный жизненный цикл, переживает сессию)
Source of truthmessenger (факт + метаданные записи); бинарь — file storage (S3), ссылка по FileId
Таблицаmessenger.call_recordings
Tenant isolationНе применяется строково: доступ — по участию в сессии-владельце. DealTenantId — scope выгрузки/арбитража.

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKapp (v7)Идентификатор записи.
SessionKindSessionKindДаenum (text), varchar(12)appТип сессии-владельца (дискриминатор XOR-связи).
CallSessionIdGuid?Нет (XOR)FK fk_call_recordings_call_session_id → messenger.call_sessions(id) restrict; CHECK exactly-one с ConferenceSessionId по SessionKind; часть partial ix_recording_activeapp1:1-звонок-владелец (SessionKind = Direct).
ConferenceSessionIdGuid?Нет (XOR)FK fk_call_recordings_conference_session_id → messenger.conference_sessions(id) restrict; CHECK exactly-one; часть partial ix_recording_activeappКонференция-владелец (SessionKind = Conference).
ChatIdGuid?Нетindex ix_recording_chatapp (денорм)Deal-чат (для конференций) — scope доступа/хранения. null для 1:1.
DealIdGuid?НетindexрепликаСделка — маршрутизация события call-recording-available (только конференции).
DealTenantIdGuid?НетрепликаDeveloperCompanyId — tenant выгрузки в file storage + ключ Kafka.
MediaTypeCallMediaTypeДаenum (text), varchar(8)appМодальность записываемой сессии; enum — call-session.md.
StatusRecordingStatusДаenum (text), varchar(12); partial ix_recording_active where status in ('Pending','Recording','Uploading')app / webhook / workerСм. enum и stateDiagram.
TriggerRecordingTriggerДаenum (text), varchar(12)appПричина старта (см. enum).
InitiatedByUserIdGuid?Нетлогическая ссылка identity.users(id)appИнициатор для Manual; null для AutoPolicy (INV-REC-3).
NoticeStateRecordingNoticeStateДаenum (text), varchar(12), default PendingappСостояние уведомления участников (согласие/индикатор; INV-REC-4).
AnnouncedAtDateTimeOffset?НетappМомент широковещания индикатора записи.
StartedAtDateTimeOffset?НетwebhookМомент подтверждённого старта захвата (→ Recording).
StoppedAtDateTimeOffset?Нетapp / webhookМомент остановки захвата (→ Uploading).
AvailableAtDateTimeOffset?НетappМомент готовности (→ Available).
FileIdGuid?Нетобязателен при Available; логическая ссылка на file storage (без FK)appСсылка на бинарь в file storage (S3), выданная при выгрузке (INV-REC-6).
FileNamestring?Нетvarchar(256)appИмя файла в file storage (уникально в категории: call-{sessionId}-{ts}).
ContentTypestring?Нетvarchar(128)appMIME записи (напр. video/mp4).
SizeByteslong?Нет> 0 при AvailablewebhookРазмер файла.
DurationSecondsint?Нет>= 0webhookДлительность записи.
FormatRecordingFormat?Нетenum (text), varchar(8); обязателен при AvailablewebhookФормат контейнера (см. enum).
ExternalRecordingIdstring?Нетvarchar(128); index ix_recording_externalwebhookКорреляционный идентификатор сессии записи во внешнем рекордере (ADR-0062).
FailureReasonRecordingFailureReason?Нетenum (text), varchar(24)app / workerПричина сбоя (при Failed).
RetentionUntilDateTimeOffset?Нетindex ix_recording_retentionpolicyСрок хранения; после — переход в Deleted (ADR-0032).
TranscriptFileIdGuid?Нетлогическая ссылка на file storage (роадмап)futureТранскрипт (будущая фаза; см. роадмап).
SummaryFileIdGuid?Нетлогическая ссылка на file storage (роадмап)futureКонспект встречи (будущая фаза).
CreatedAtDateTimeOffsetДаappСоздание записи (= провизия при старте сессии).
UpdatedAtDateTimeOffsetДаappПоследний переход.

Enum SessionKind

ЗначениеОписание
DirectЗапись 1:1-звонка (call-session.md).
ConferenceЗапись конференции (conference-session.md).

Enum RecordingStatus

ЗначениеОписание
PendingЗапись решена политикой/пользователем; рекордер ещё не подтвердил старт.
RecordingИдёт захват (рекордер подтвердил).
UploadingЗахват завершён, идёт выгрузка файла в file storage (S3).
AvailableФайл сохранён; заданы FileId + метаданные; доступен для скачивания. Терминальный (до Deleted).
FailedСбой рекордера/выгрузки. Терминальный.
DeletedУдалён по retention — FileId очищен, файл удалён из file storage. Терминальный.

Enum RecordingTrigger

ЗначениеОписание
AutoPolicyАвто-запись по политике (звонки в контексте сделки). InitiatedByUserId = null.
ManualИнициирована пользователем (будущее / вне сделки).

Enum RecordingFormat

ЗначениеОписание
Mp4Видео (микс аудио/видео).
WebmВидео (WebM).
MkvВидео (raw).
Mp3Только аудио.

Enum RecordingNoticeState

ЗначениеОписание
PendingИндикатор записи ещё не разослан.
AnnouncedИндикатор разослан участникам; задан AnnouncedAt.

Enum RecordingFailureReason

ЗначениеОписание
RecorderUnavailableНет свободного рекордера.
UploadFailedСбой выгрузки в file storage.
TimeoutФинализация/callback рекордера не пришли в срок.
ErrorИной фатальный сбой.

Value objects

Value objects не используются.

Модели JSONB

JSONB-модели не используются. Бинарь записи — в file storage (S3), в messenger только FileId + метаданные (INV-REC-6).

Связи

СвязьТипКлючПравило загрузкиDelete behavior
CallSessionEntityreference (XOR-владелец)CallSessionIdпри SessionKind = Directrestrict
ConferenceSessionEntityreference (XOR-владелец)ConferenceSessionIdпри SessionKind = Conferencerestrict
Файл записи (file storage)внешняя логическаяFileIdпресайн download; бинарь вне БД (INV-REC-6)— (регламент file storage)
Транскрипт/конспект (file storage)внешняя логическая (роадмап)TranscriptFileId / SummaryFileIdбудущая фаза

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-REC-1Ровно один из CallSessionId/ConferenceSessionId задан и согласован с SessionKind (DB CHECK exactly-one, XOR)создание записиMSG_RECORDING_LINK_INVALID
INV-REC-2Переходы: Pending → Recording → Uploading → Available; любой → Failed; Available → Deleted. FileId/SizeBytes/DurationSeconds/Format обязательны в Available, null до негосмена статусаMSG_RECORDING_INVALID_STATE
INV-REC-3Авто-запись: сессии в контексте сделки создают запись с Trigger = AutoPolicy, InitiatedByUserId = null при старте сессиистарт сессии (RecordingPolicy.ShouldAutoRecord)
INV-REC-4NoticeState должен достичь Announced (индикатор разослан) до входа в Recording; Recording/Available при NoticeState = Pending — нарушениестарт захватаMSG_RECORDING_NOTICE_REQUIRED
INV-REC-5DurationSeconds/SizeBytes/Format/FileId проставляются в Available из результата рекордера+выгрузки и далее иммутабельныпереход → Available
INV-REC-6Бинарь не хранится в messenger: только FileId (file storage) + метаданные (ср. INV-MSG-7)выгрузка/переход → AvailableMSG_RECORDING_FILE_INVALID
INV-REC-7Не более одной активной (Pending/Recording/Uploading) записи на сессию (partial unique ix_recording_active)создание/стартMSG_RECORDING_ALREADY_ACTIVE

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
RecordingNoticeAnnouncedEventдоменноеNoticeState → Announcedpush индикатора записи в hub-группу (chat:{ChatId} / user:{party})
RecordingStartedEventдоменное→ Recordingpush RecordingState(Started)
RecordingStoppedEventдоменное→ Uploadingpush RecordingState(Stopped)
RecordingAvailableEventдоменное + интеграционное→ Availableдоменный push RecordingState(Available) (без URL); интеграционное pin.messenger.call-recording-available.v1 (ключ SessionId), консюмеры crm/notifications — контракт ../events/call-recording-available.md
RecordingFailedEventдоменное→ Failedpush RecordingState(Failed) + внутренний алерт
RecordingDeletedEventдоменное→ Deleted (retention)запрос удаления в file storage; очистка FileId

Доменные события инициируют hub-push (snake_case, non-PII, без ссылок/URL). Интеграционное RecordingAvailableEvent публикуется атомарно с переходом → Available через транзакционный outbox (из ingest-эндпоинта, ../api/internal/jibri-recording-ingest.md).

Наружу — только готовность. Интеграционное pin.messenger.call-recording-available.v1 публикуется исключительно на → Available; Failed и Deleted наружу не транслируются (при Failed — внутренний алерт, при retention-Deleted — очистка FileId). Downstream (crm/notifications) не должен ждать запись для каждого звонка: её может не быть (авто-запись выключена политикой, сбой рекордера, отсутствие рекордера). Version-guard по Status: webhook/worker обновляют только неконечные статусы идемпотентно — поздний callback на уже терминальной записи (Available/Failed/Deleted) — no-op.

Роадмап: CallMeetingInsightEntity (транскрипция/конспект — будущая фаза)

Транскрипт и конспект встречи — отдельная будущая сущность CallMeetingInsightEntity (AggregateRoot, 1:1 к записи), а не колонки здесь (поля TranscriptFileId/SummaryFileId зарезервированы как шов). Обоснование: STT/суммаризация выполняются отдельным пайплайном по событию call-recording-available, могут переобрабатываться и давать несколько артефактов/языков — иной жизненный цикл, чем у захвата. Событие call-recording-available уже несёт достаточный контекст (FileId, SessionId, DealId?, DurationSeconds) для будущего консюмера. Детали — ../calls/transcription-roadmap.md. Никакой части текущей модели это не блокирует.

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK; partial unique ix_recording_active where status in ('Pending','Recording','Uploading') — одна активная запись на сессию (INV-REC-7); ix_recording_chat (ChatId); ix_recording_external (ExternalRecordingId); ix_recording_retention (RetentionUntil) — выборка retention-джобы
Партиционирование / retentionRetentionUntil → переход Deleted + удаление файла в file storage (ADR-0032); строка записи сохраняется как факт (метаданные без бинаря)
Concurrencyоптимистическая блокировка + version-guard по Status (webhook/worker обновляют только неконечные статусы, идемпотентно)
Permissions / PIImessenger.recordings.view — участник сессии; messenger.recordings.manage — модератор (остановка). Ссылка на скачивание — пресайн file storage (короткоживущий), выдаётся отдельным методом (../api/recordings/get-recording-download.md), аудит ReadSensitive. Бинарь/URL в события/логи не попадают
AuditUpdatedAt + фазовые метки (StartedAt/StoppedAt/AvailableAt) — иммутабельные факты; доступ к скачиванию — аудируемое чтение

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

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

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