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 |
| Kind | AggregateRoot (не owned-child — асинхронный жизненный цикл, переживает сессию) |
| Source of truth | messenger (факт + метаданные записи); бинарь — file storage (S3), ссылка по FileId |
| Таблица | messenger.call_recordings |
| Tenant isolation | Не применяется строково: доступ — по участию в сессии-владельце. DealTenantId — scope выгрузки/арбитража. |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|
Id | Guid | Да | PK | app (v7) | Идентификатор записи. |
SessionKind | SessionKind | Да | enum (text), varchar(12) | app | Тип сессии-владельца (дискриминатор XOR-связи). |
CallSessionId | Guid? | Нет (XOR) | FK fk_call_recordings_call_session_id → messenger.call_sessions(id) restrict; CHECK exactly-one с ConferenceSessionId по SessionKind; часть partial ix_recording_active | app | 1:1-звонок-владелец (SessionKind = Direct). |
ConferenceSessionId | Guid? | Нет (XOR) | FK fk_call_recordings_conference_session_id → messenger.conference_sessions(id) restrict; CHECK exactly-one; часть partial ix_recording_active | app | Конференция-владелец (SessionKind = Conference). |
ChatId | Guid? | Нет | index ix_recording_chat | app (денорм) | Deal-чат (для конференций) — scope доступа/хранения. null для 1:1. |
DealId | Guid? | Нет | index | реплика | Сделка — маршрутизация события call-recording-available (только конференции). |
DealTenantId | Guid? | Нет | — | реплика | DeveloperCompanyId — tenant выгрузки в file storage + ключ Kafka. |
MediaType | CallMediaType | Да | enum (text), varchar(8) | app | Модальность записываемой сессии; enum — call-session.md. |
Status | RecordingStatus | Да | enum (text), varchar(12); partial ix_recording_active where status in ('Pending','Recording','Uploading') | app / webhook / worker | См. enum и stateDiagram. |
Trigger | RecordingTrigger | Да | enum (text), varchar(12) | app | Причина старта (см. enum). |
InitiatedByUserId | Guid? | Нет | логическая ссылка identity.users(id) | app | Инициатор для Manual; null для AutoPolicy (INV-REC-3). |
NoticeState | RecordingNoticeState | Да | enum (text), varchar(12), default Pending | app | Состояние уведомления участников (согласие/индикатор; INV-REC-4). |
AnnouncedAt | DateTimeOffset? | Нет | — | app | Момент широковещания индикатора записи. |
StartedAt | DateTimeOffset? | Нет | — | webhook | Момент подтверждённого старта захвата (→ Recording). |
StoppedAt | DateTimeOffset? | Нет | — | app / webhook | Момент остановки захвата (→ Uploading). |
AvailableAt | DateTimeOffset? | Нет | — | app | Момент готовности (→ Available). |
FileId | Guid? | Нет | обязателен при Available; логическая ссылка на file storage (без FK) | app | Ссылка на бинарь в file storage (S3), выданная при выгрузке (INV-REC-6). |
FileName | string? | Нет | varchar(256) | app | Имя файла в file storage (уникально в категории: call-{sessionId}-{ts}). |
ContentType | string? | Нет | varchar(128) | app | MIME записи (напр. video/mp4). |
SizeBytes | long? | Нет | > 0 при Available | webhook | Размер файла. |
DurationSeconds | int? | Нет | >= 0 | webhook | Длительность записи. |
Format | RecordingFormat? | Нет | enum (text), varchar(8); обязателен при Available | webhook | Формат контейнера (см. enum). |
ExternalRecordingId | string? | Нет | varchar(128); index ix_recording_external | webhook | Корреляционный идентификатор сессии записи во внешнем рекордере (ADR-0062). |
FailureReason | RecordingFailureReason? | Нет | enum (text), varchar(24) | app / worker | Причина сбоя (при Failed). |
RetentionUntil | DateTimeOffset? | Нет | index ix_recording_retention | policy | Срок хранения; после — переход в Deleted (ADR-0032). |
TranscriptFileId | Guid? | Нет | логическая ссылка на file storage (роадмап) | future | Транскрипт (будущая фаза; см. роадмап). |
SummaryFileId | Guid? | Нет | логическая ссылка на file storage (роадмап) | future | Конспект встречи (будущая фаза). |
CreatedAt | DateTimeOffset | Да | — | app | Создание записи (= провизия при старте сессии). |
UpdatedAt | DateTimeOffset | Да | — | app | Последний переход. |
Enum SessionKind
Enum RecordingStatus
| Значение | Описание |
|---|
Pending | Запись решена политикой/пользователем; рекордер ещё не подтвердил старт. |
Recording | Идёт захват (рекордер подтвердил). |
Uploading | Захват завершён, идёт выгрузка файла в file storage (S3). |
Available | Файл сохранён; заданы FileId + метаданные; доступен для скачивания. Терминальный (до Deleted). |
Failed | Сбой рекордера/выгрузки. Терминальный. |
Deleted | Удалён по retention — FileId очищен, файл удалён из file storage. Терминальный. |
Enum RecordingTrigger
| Значение | Описание |
|---|
AutoPolicy | Авто-запись по политике (звонки в контексте сделки). InitiatedByUserId = null. |
Manual | Инициирована пользователем (будущее / вне сделки). |
| Значение | Описание |
|---|
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 |
|---|
CallSessionEntity | reference (XOR-владелец) | CallSessionId | при SessionKind = Direct | restrict |
ConferenceSessionEntity | reference (XOR-владелец) | ConferenceSessionId | при SessionKind = Conference | restrict |
| Файл записи (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-4 | NoticeState должен достичь Announced (индикатор разослан) до входа в Recording; Recording/Available при NoticeState = Pending — нарушение | старт захвата | MSG_RECORDING_NOTICE_REQUIRED |
| INV-REC-5 | DurationSeconds/SizeBytes/Format/FileId проставляются в Available из результата рекордера+выгрузки и далее иммутабельны | переход → Available | — |
| INV-REC-6 | Бинарь не хранится в messenger: только FileId (file storage) + метаданные (ср. INV-MSG-7) | выгрузка/переход → Available | MSG_RECORDING_FILE_INVALID |
| INV-REC-7 | Не более одной активной (Pending/Recording/Uploading) записи на сессию (partial unique ix_recording_active) | создание/старт | MSG_RECORDING_ALREADY_ACTIVE |
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|
RecordingNoticeAnnouncedEvent | доменное | NoticeState → Announced | push индикатора записи в hub-группу (chat:{ChatId} / user:{party}) |
RecordingStartedEvent | доменное | → Recording | push RecordingState(Started) |
RecordingStoppedEvent | доменное | → Uploading | push RecordingState(Stopped) |
RecordingAvailableEvent | доменное + интеграционное | → Available | доменный push RecordingState(Available) (без URL); интеграционное pin.messenger.call-recording-available.v1 (ключ SessionId), консюмеры crm/notifications — контракт ../events/call-recording-available.md |
RecordingFailedEvent | доменное | → Failed | push 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-джобы |
| Партиционирование / retention | RetentionUntil → переход Deleted + удаление файла в file storage (ADR-0032); строка записи сохраняется как факт (метаданные без бинаря) |
| Concurrency | оптимистическая блокировка + version-guard по Status (webhook/worker обновляют только неконечные статусы, идемпотентно) |
| Permissions / PII | messenger.recordings.view — участник сессии; messenger.recordings.manage — модератор (остановка). Ссылка на скачивание — пресайн file storage (короткоживущий), выдаётся отдельным методом (../api/recordings/get-recording-download.md), аудит ReadSensitive. Бинарь/URL в события/логи не попадают |
| Audit | UpdatedAt + фазовые метки (StartedAt/StoppedAt/AvailableAt) — иммутабельные факты; доступ к скачиванию — аудируемое чтение |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: CallSessionEntity — сигналинг и лог звонка (media отложен), Entity / Aggregate: ConferenceSessionEntity — групповой звонок в чате сделки
- API (16): GET /api/messenger/calls — Журнал звонков (входящие / исходящие / пропущенные), GET /api/messenger/calls/{callId}/recordings — Записи 1:1-звонка, GET /api/messenger/chats/{chatId}/conferences — История конференций сделки, GET /api/messenger/conferences/{conferenceId} — Карточка конференции, GET /api/messenger/conferences/{conferenceId}/recordings — Записи конференции, GET /api/messenger/recordings/{recordingId} — Карточка записи, GET /api/messenger/recordings/{recordingId}/download — Ссылка на скачивание записи, POST /api/messenger/calls — Начать звонок, POST /api/messenger/calls/{callId}/end — Завершить звонок (hangup, call-лог), POST /api/messenger/chats/{chatId}/conferences — Начать конференцию в чате сделки, POST /api/messenger/conferences/{conferenceId}/end — Завершить конференцию для всех, POST /api/messenger/conferences/{conferenceId}/recording/stop — Остановить запись конференции (+4)
- Use Cases (2): UC-MSG-006. Групповой звонок-конференция в чате сделки, UC-MSG-007. Авто-запись звонка по сделке и её выдача участнику
- События (1): Event Contract: pin.messenger.call-recording-available (CallRecordingAvailableEvent)
- Фоновые задачи (3): Background Job: ConferenceRoomReaperJob, Background Job: RealtimeDispatchJob, Background Job: RecordingFinalizeRetryJob
- adr (1): ADR-0062: Медиа-стек звонков и запись на Jitsi (JVB SFU + coturn + jibri → file storage; JWT из messenger; нативный lib-jitsi-meet)
- calls (6): Медиа-звонки и запись на Jitsi — интеграционный гид (messenger), Медиа-звонки на Jitsi: план поставки (playbook аналитик → разработчик), Предусловия в file storage для хранения записей звонков, Backend-реализация медиа-звонков: спека → код (messenger), Jitsi: роадмап транскрипции и конспектов (будущая фаза), Jitsi: сквозной пайплайн авто-записи
- registries (1): Реестр сущностей PIN
Связанные документы