Entity / Aggregate: CallSessionEntity — сигналинг и лог звонка (media отложен)
Дом полей агрегата «Сессия звонка» контекста messenger. Все проекции (карточка звонка/лог,
payload доменных событий, hub-сообщения) ссылаются сюда и перечисляют только дельту.
Назначение
CallSessionEntity — контракт сигналинга аудио/видео звонка поверх контактов (мессенджер/ВКС) и журнал
звонков (входящие/исходящие/пропущенные). Агрегат владеет только жизненным циклом и логом: StartCall → Ringing → Answered → Ended (плюс Declined/Cancelled/Missed/Failed). Полный разбор путей создания/
закрытия и подключения участников — ../calls/session-lifecycle.md.
Media-стек активен через внешний SFU (ADR-0062, feature flag CALLS_MEDIA_ENABLED; исторический
CALLS_SIGNALING_ENABLED — стадия signaling-only, поглощён медиа-фазой). 1:1-звонок соответствует
SessionKind = Direct (call-recording.md) и владеет медиа-комнатой (Room, owned VO
MediaRoom). Медиа (звук/видео/демонстрация экрана) идёт клиент ↔ SFU; бэкенд не в медиа-плоскости —
SDP/ICE и токены доступа не персистятся (INV-CALL-4/6), в БД только факты жизненного цикла. Вход в звонок
раздаётся приглашением (комната + короткоживущий токен доступа) через SignalR-hub, а не relay-ом SDP.
Демонстрация экрана в 1:1 — эфемерный сигнал, не персистится. Запись — по политике RecordingPolicy
(call-recording.md). Звонок разрешён только между Accepted-контактами
(contact.md) вне чёрного списка (blacklist.md) — сводный гейт
CommunicationPolicy.CanCall.
Классификация
| Поле | Значение |
|---|
| Контекст | messenger |
| Kind | AggregateRoot |
| Source of truth | messenger |
| Таблица | messenger.call_sessions |
| Tenant isolation | Не применяется — мессенджер платформенный (звонок между пользователями); изоляция доступа — через CallerUserId/CalleeUserId + гейт контакта |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|
Id | Guid | Да | PK | app | Идентификатор сессии. |
CallerUserId | Guid | Да | index ix_call_caller (CallerUserId,StartedAt desc) | из hub-контекста | Инициатор звонка. Логическая ссылка на пользователя (messenger-user.md; SoT — identity), без FK. |
CalleeUserId | Guid | Да | index ix_call_callee (CalleeUserId,StartedAt desc) | app | Вызываемый пользователь (messenger-user.md), без FK. |
ContactId | Guid | Да | FK fk_call_sessions_contact_id → messenger.contacts(id), index | app | Контакт-разрешение (пара Caller↔Callee, contact.md). Через него — проверка Accepted и чёрного списка. |
MediaType | CallMediaType | Да | enum (text), varchar(8) | app | Аудио/видео. |
Room | MediaRoom (owned VO) | Да | см. Value objects; минтится при → Ringing, освобождается в терминале (INV-CALL-6) | app | Идентификатор внешней медиа-комнаты (SFU, ADR-0062). |
Status | CallStatus | Да | enum (text), varchar(16); partial index ix_call_active (см. ниже) | app / worker | См. enum и stateDiagram. |
StartedAt | DateTimeOffset | Да | часть ix_call_caller/ix_call_callee (desc) | app | Момент StartCall (переход → Ringing). |
AnsweredAt | DateTimeOffset? | Нет | — | app | Момент Answer (→ Answered). |
EndedAt | DateTimeOffset? | Нет | — | app | Момент завершения (→ Ended) или фиксации Declined/Missed/Failed. |
EndReason | CallEndReason? | Нет | enum (text), varchar(24) | app / worker | Причина завершения; обязателен при терминальном статусе кроме Missed. |
DurationSeconds | int? | Нет | >= 0 (check) | domain | Вычисляется EndedAt − AnsweredAt при Answered → Ended; иначе null. |
CreatedAt | DateTimeOffset | Да | — | app | Создание записи (= StartedAt при StartCall). |
UpdatedAt | DateTimeOffset | Да | — | app | Последний переход. |
| Значение | Описание |
|---|
Audio | Голосовой звонок. |
Video | Видеозвонок (ВКС). |
Enum CallStatus
| Значение | Описание |
|---|
Ringing | Идёт вызов, ждём ответа callee (окно Missed, default 45с). |
Answered | Callee принял; активная медиа-сессия (клиент ↔ SFU). |
Declined | Callee отклонил вызов (decline-call). Терминальный. |
Cancelled | Caller отменил вызов до ответа (hangup в Ringing, end-call инициатором). Терминальный; у callee — несостоявшийся входящий. |
Missed | Не ответили в окно (worker call-timeout-reaper). Терминальный. |
Ended | Штатное завершение принятого звонка (hangup любой стороной в Answered). Терминальный. |
Failed | Ошибка сигналинга/сети до соединения. Терминальный. |
Enum CallEndReason
| Значение | Описание |
|---|
HangupCaller | Завершил инициатор (в Answered). |
HangupCallee | Завершил вызываемый (в Answered). |
Declined | Отклонён callee (сопровождает Status = Declined). |
CancelledByCaller | Caller отменил до ответа (сопровождает Status = Cancelled). |
Timeout | Истекло окно ответа (сопровождает Status = Missed). |
Error | Сбой сигналинга (сопровождает Status = Failed). |
Value objects
Ссылка на внешнюю медиа-комнату SFU (ADR-0062). Не медиа и не PII: имя комнаты — маршрутный идентификатор для
реконнекта и привязки записи к звонку; персистится. Токены доступа, SDP/ICE, TURN-креды — эфемерны, минтятся
при входе, не хранятся (INV-CALL-6).
| Поле | Тип | Обязательность | Ограничения | Колонка |
|---|
RoomName | string | Да | varchar(128); детерминирован из Id + nonce; уникален среди активных сессий | room_name |
Domain | string | Да | varchar(128) | room_domain |
Модели JSONB
JSONB-модели не используются. SDP/ICE и токены доступа не хранятся: медиа-негоциация идёт клиент ↔ SFU,
токен минтится при входе и не попадает в БД (INV-CALL-4/6 — нет PII/медиа/секретов в persist-слое).
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|
ContactEntity | reference (разрешение связи) | ContactId | в StartCall — проверка Accepted + чёрного списка (contact.md) | restrict |
Caller / Callee (messenger-user) | replica (логическая) | CallerUserId / CalleeUserId | без FK; адресация hub-групп user:{id} (messenger-user.md) | — |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|
| INV-CALL-1 | Звонок разрешён только при Accepted ContactEntity пары (contact.md) и ¬blacklist любой стороной (blacklist.md) | CommunicationPolicy.CanCall при StartCall | MSG_CALL_NOT_CONTACT / MSG_CALL_BLOCKED |
| INV-CALL-2 | Ringing без Answer дольше окна (default 45с) → Missed | worker call-timeout-reaper, идемпотентно (только записи в статусе Ringing) | — |
| INV-CALL-3 | Переходы: Ringing → Answered/Declined/Cancelled/Missed/Failed; Answered → Ended (hangup любой стороной). Declined = callee отклонил (decline-call); Cancelled = caller отменил в Ringing (end-call инициатором); Ended = hangup в Answered | командные методы агрегата | MSG_CALL_INVALID_STATE |
| INV-CALL-4 | Медиа-негоциация (SDP/ICE/RTP) идёт клиент ↔ SFU; бэкенд её не релеит и не персистит — в БД нет медиа/PII звонка | медиа-плоскость (SFU), не БД | — |
| INV-CALL-5 | Не более одного активного (Ringing/Answered) звонка на пару (Caller,Callee) одновременно | guard в StartCall (по индексу активных звонков ix_call_active) | MSG_CALL_ALREADY_ACTIVE |
| INV-CALL-6 | Room.RoomName детерминирован и уникален среди активных сессий; минтится при → Ringing, освобождается в терминале. Токен доступа/ICE/TURN — эфемерны, минтятся при входе, не персистятся и не логируются. Демонстрация экрана в 1:1 — эфемерный сигнал, не персистится | StartCall + сигналинг-слой | — |
Гонки сигналинга. Встречный вызов (одновременно A→B и B→A): ix_call_active гейтит по направленной
паре (Caller, Callee), поэтому две встречные сессии формально допустимы — UI разрешает glare, показывая обеим
сторонам входящий; принявшая сторона отменяет свой исходящий (Cancelled). Ответ callee с двух устройств:
первый Answer переводит сессию → Answered, повторный — MSG_CALL_INVALID_STATE (идемпотентно проигрывает).
Reaper (INV-CALL-2) и штатное завершение конкурируют за терминал: первый перевод побеждает (INV-CALL-3),
опоздавший переход — no-op на уже терминальной записи.
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|
CallStartedEvent | доменное | [*] → Ringing | push CallInvite (комната + токен доступа минтится при доставке, без хранения) в hub-группу user:{CalleeUserId}. |
CallAnsweredEvent | доменное | → Answered | push обеим сторонам (user:{Caller}, user:{Callee}). |
CallDeclinedEvent | доменное | → Declined | push обеим сторонам (CallSignal(Declined)). |
CallCancelledEvent | доменное | → Cancelled (caller до ответа) | push обеим сторонам (CallSignal(Cancelled)); у callee — как несостоявшийся входящий (звонок пропал). |
CallMissedEvent | доменное | → Missed (worker) | push caller; системная запись «пропущенный звонок» — отложено (ниже). |
CallEndedEvent | доменное | → Ended | push обеим сторонам; DurationSeconds. |
Доменные события агрегата инициируют hub-push через SignalR — eventName в snake_case, только non-PII
(Id/статус/тип). Медиа-негоциация (SDP/ICE) идёт клиент ↔ SFU вне БД и вне hub (INV-CALL-4); вход в звонок
раздаётся приглашением CallInvite (комната + токен минтится при доставке).
Интеграционные (Kafka) события 1:1-звонок не публикует (нет внешнего потребителя для person-to-person
жизненного цикла — лог звонков и hub-события покрывают UI). Если для 1:1 включена запись, её готовность
публикует агрегат записи (pin.messenger.call-recording-available.v1, call-recording.md),
а не сессия звонка. Уведомление о пропущенном звонке (системная строка через message.md) — вне
scope MVP.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|
| Индексы | PK; ix_call_caller (CallerUserId,StartedAt desc); ix_call_callee (CalleeUserId,StartedAt desc) — журнал/лента; partial ix_call_active (Status) where status in ('Ringing','Answered') — выборка reaper-а (INV-CALL-2) и guard активного звонка (INV-CALL-5) |
| Партиционирование / retention | none |
| Concurrency | append-only лог; единственный активный звонок на пару — command-guard (INV-CALL-5); reaper идемпотентен (обновление только записей в статусе Ringing) |
| Permissions / PII | Permission messenger.calls.use (участник — только Caller/Callee). Медиа/SDP/ICE и токены доступа не персистятся (INV-CALL-4/6). Media-стек за feature flag CALLS_MEDIA_ENABLED (ADR-0062); запись — RECORDINGS_ENABLED + RecordingPolicy |
| Audit | UpdatedAt + фазовые метки (StartedAt/AnsweredAt/EndedAt) — иммутабельные факты |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (5): Entity / Aggregate: BlacklistEntity, Entity / Aggregate: CallRecordingEntity — запись звонка (хранение в file storage), Entity / Aggregate: ConferenceSessionEntity — групповой звонок в чате сделки, Entity / Aggregate: ContactEntity, Entity: CallParticipantEntity (owned-child ConferenceSessionEntity)
- API (15): DELETE /api/messenger/contacts/{contactId} — Удалить контакт / отозвать исходящую заявку, GET /api/messenger/calls — Журнал звонков (входящие / исходящие / пропущенные), GET /api/messenger/calls/{callId}/recordings — Записи 1:1-звонка, GET /api/messenger/recordings/{recordingId} — Карточка записи, POST /api/messenger/calls — Начать звонок, POST /api/messenger/calls/{callId}/answer — Принять звонок, POST /api/messenger/calls/{callId}/decline — Отклонить звонок, POST /api/messenger/calls/{callId}/end — Завершить звонок (hangup, call-лог), POST /api/messenger/calls/{callId}/media-token — Обновить токен входа в звонок, POST /api/messenger/chats/{chatId}/conferences — Начать конференцию в чате сделки, POST /api/messenger/contacts/requests/{contactId}/accept — Принять заявку в контакты, POST /internal/call-recordings/{sessionId}/ingest — Финализация записи (jibri → file storage) (+3)
- Use Cases (3): UC-MSG-004. Заблокировать пользователя (чёрный список), UC-MSG-005. Сигналинг аудио/видео-звонка, UC-MSG-007. Авто-запись звонка по сделке и её выдача участнику
- События (3): События сервиса messenger: реестр топиков, Event Contract: pin.messenger.call-recording-available (CallRecordingAvailableEvent), Event Contract: pin.messenger.contact-request-accepted (ContactRequestAcceptedEvent)
- Фоновые задачи (3): Background Job: CallTimeoutReaperJob, Background Job: RealtimeDispatchJob, Background Job: RecordingFinalizeRetryJob
- adr (1): ADR-0062: Медиа-стек звонков и запись на Jitsi (JVB SFU + coturn + jibri → file storage; JWT из messenger; нативный lib-jitsi-meet)
- calls (6): Жизненный цикл сессии звонка — 1:1 и конференция (messenger), Медиа-звонки и запись на Jitsi — интеграционный гид (messenger), Медиа-звонки на Jitsi: план поставки (playbook аналитик → разработчик), Backend-реализация медиа-звонков: спека → код (messenger), Jitsi: именование комнат, admission и presence, Jitsi: нативный клиент lib-jitsi-meet на Next.js 16
- registries (1): Реестр сущностей PIN
Связанные документы