Entity / Aggregate: ConferenceSessionEntity — групповой звонок в чате сделки
Дом полей агрегата «Сессия конференции» контекста messenger. Групповой аудио/видео звонок нескольких
участников поверх Deal-чата (chat.md). Все проекции (карточка конференции, история звонков сделки,
payload доменных событий, hub-инвайты) ссылаются сюда и перечисляют только дельту. Дочерний ростер —
call-participant.md; запись — call-recording.md.
Назначение
ConferenceSessionEntity — жизненный цикл группового звонка внутри чата сделки: Provisioning → Active → Ended (плюс Failed). В отличие от 1:1 (call-session.md, пара Caller/Callee), конференция
имеет динамический ростер участников (call-participant.md) с пофамильным join/leave,
одного модератора и авто-завершение по опустошению. Медиа (звук/видео/демонстрация экрана) идёт через внешний
media-стек (SFU, ADR-0062) — агрегат хранит только факты жизненного цикла и ростер; SDP/ICE/медиа/токены доступа
не персистятся (INV-CONF-7, ср. INV-CALL-4). Доступ к конференции = активное членство в её Deal-чате
(record-level, INV-CHAT-4), отдельного контактного гейта нет. Запись — по политике RecordingPolicy (для
звонков в сделке, как правило, включена; call-recording.md).
Классификация
| Поле | Значение |
|---|
| Контекст | messenger |
| Kind | AggregateRoot (дочерний CallParticipantEntity — Entity внутри агрегата, call-participant.md) |
| Source of truth | messenger (жизненный цикл + ростер); DealId/DealTenantId — реплики из crm через chat.md |
| Таблица | messenger.conference_sessions (+ messenger.call_participants) |
| Tenant isolation | Не применяется строково: доступ = членство в Deal-чате (INV-CHAT-4). DealTenantId — только scope арбитража/маршрутизации событий, не фильтр строк. |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|
Id | Guid | Да | PK | app (v7) | Идентификатор конференции. |
ChatId | Guid | Да | FK fk_conference_sessions_chat_id → messenger.chats(id) restrict; index; часть partial ix_conf_active | app | Deal-чат конференции (chat.md, Kind = Deal). |
DealId | Guid? | Нет | index ix_conf_deal_id; логическая ссылка crm.deals(id) | реплика из chat.md | Денорм сделки для маршрутизации события conference-ended и истории звонков сделки. |
DealTenantId | Guid? | Нет | index | реплика из chat.md | DeveloperCompanyId сделки; scope арбитража + ключ Kafka. |
MediaType | CallMediaType | Да | enum (text), varchar(8) | app | Стартовая модальность (аудио/видео); enum — call-session.md. |
Room | MediaRoom (owned VO) | Да | см. Value objects | app | Идентификатор внешней медиа-комнаты (SFU, ADR-0062). |
Status | ConferenceStatus | Да | enum (text), varchar(16); partial ix_conf_active where status in ('Provisioning','Active') | app / worker | См. enum и stateDiagram. |
StartedByUserId | Guid | Да | index; логическая ссылка identity.users(id) | app | Инициатор (стартовый модератор). |
Title | string? | Нет | varchar(120) | app | Необязательное название встречи (по умолчанию — из контекста сделки). |
StartedAt | DateTimeOffset | Да | — | app | Момент старта (переход → Active). |
EndedAt | DateTimeOffset? | Нет | — | app / worker | Момент завершения (→ Ended/Failed). |
EndReason | ConferenceEndReason? | Нет | enum (text), varchar(24) | app / worker | Причина завершения; обязателен при терминальном статусе. |
DurationSeconds | int? | Нет | >= 0 (check) | domain | Вычисляется EndedAt − StartedAt при завершении. |
ActiveParticipantCount | int | Да | >= 0, default 0 | domain | Денорм число активных участников (для авто-завершения и карточки). |
PeakParticipantCount | int | Да | >= 0, default 0 | domain | Максимум одновременно активных за сессию (для аналитики/события). |
LastActivityAt | DateTimeOffset | Да | index ix_conf_last_activity | app / webhook | Последняя presence-активность (join/leave/heartbeat); основа stale-детекции reaper-а. |
CreatedAt | DateTimeOffset | Да | — | app | Создание записи (= StartedAt при старте). |
UpdatedAt | DateTimeOffset | Да | — | app | Последний переход/изменение. |
Enum ConferenceStatus
| Значение | Описание |
|---|
Provisioning | Комната зарезервирована, ждём подтверждения media-стека (admission-вебхук). Переходное. |
Active | Идёт конференция, ≥ 1 активный участник. |
Ended | Штатно завершена (опустошение / модератор / закрытие чата). Терминальный. |
Failed | Комната не поднялась / фатальный сбой media/записи. Терминальный. |
Enum ConferenceEndReason
| Значение | Описание |
|---|
LastParticipantLeft | Вышел последний активный участник (штатное авто-завершение). |
EmptyTimeout | Reaper закрыл осиротевшую пустую комнату по истечении grace. |
ModeratorEnded | Модератор завершил для всех. |
ChatClosed | Deal-чат перешёл в Closed (сделка Completed/Cancelled, INV-CHAT-8). |
Timeout | Reaper закрыл «зависшую» активную сессию без активности дольше cap. |
Error | Фатальный сбой media/инфраструктуры (сопровождает Failed). |
Value objects
Ссылка на внешнюю медиа-комнату SFU (ADR-0062). Не медиа и не PII: имя комнаты — маршрутный идентификатор,
нужный для реконнекта участников и привязки записи к сессии; персистится. Токены доступа, SDP/ICE, TURN-креды —
эфемерны, минтятся при входе, не хранятся (INV-CONF-7).
| Поле | Тип | Обязательность | Ограничения | Колонка |
|---|
RoomName | string | Да | varchar(128); детерминирован из Id + nonce; уникален среди активных сессий | room_name |
Domain | string | Да | varchar(128) | room_domain |
Модели JSONB
JSONB-модели не используются. Медиа/сигналинг (SDP/ICE) и токены доступа в БД не хранятся (INV-CONF-7).
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|
ChatEntity | reference (родитель контекста) | ChatId | гейт доступа по членству (chat.md, INV-CHAT-4) | restrict |
CallParticipantEntity | owned-child | call_participants.conference_session_id | ростер конференции (call-participant.md) | cascade |
CallRecordingEntity | reference (0..1 активная) | call_recordings.conference_session_id | запись сессии (call-recording.md); связь по XOR-FK со стороны записи | restrict |
Инициатор (messenger-user) | replica (логическая) | StartedByUserId | адресация hub-групп; без FK | — |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|
| INV-CONF-1 | Конференция привязана ровно к одному чату с Kind = Deal; для Direct-переписки используется 1:1 (call-session.md) | старт конференции | MSG_CONFERENCE_CHAT_INVALID |
| INV-CONF-2 | Не более одной активной (Provisioning/Active) конференции на чат (partial unique ix_conf_active where status in ('Provisioning','Active')) | guard старта | MSG_CONFERENCE_ALREADY_ACTIVE |
| INV-CONF-3 | Старт/присоединение = активный Participant этого чата (record-level, INV-CHAT-4); отдельного контактного гейта нет | guard доступа | MSG_CHAT_ACCESS_DENIED |
| INV-CONF-4 | Переходы: Provisioning → Active/Failed; Active → Ended/Failed. Ended при опустошении / модераторе / закрытии чата; reaper обеспечивает EmptyTimeout/Timeout | командные методы + reaper | MSG_CONFERENCE_INVALID_STATE |
| INV-CONF-5 | Room.RoomName уникален среди активных сессий; токены/ICE/медиа не персистятся | старт/присоединение | — |
| INV-CONF-6 | Ровно один активный участник с ConferenceRole = Moderator; при выходе модератора роль передаётся (старейший активный) | join/leave/end (call-participant.md) | — |
| INV-CONF-7 | В БД нет медиа/сигналинга/PII звонка и токенов доступа — только факты жизненного цикла и ростер | сигналинг-слой (не БД) | — |
| INV-CONF-8 | ActiveParticipantCount/PeakParticipantCount — производные от активных строк ростера; Active с ActiveParticipantCount = 0 дольше grace → reaper закрывает (EmptyTimeout) | пересчёт при join/leave + reaper | — |
Провизия, авто-завершение и гонки. Между StartConference и admission-вебхуком media-стека конференция в
Provisioning; вошедший до подтверждения участник учитывается активным ростером после admission (→ Active).
Не поднявшаяся в срок комната — Provisioning → Failed (ProvisionTimeout); ConferenceEndedEvent
(интеграционное conference-ended) публикуется и для Failed — downstream получает факт завершения
независимо от причины. Авто-завершение: выход последнего активного участника → LastParticipantLeft; Active
c ActiveParticipantCount = 0 дольше grace либо stale-сессия дольше cap → reaper (EmptyTimeout/Timeout),
идемпотентно (обновляет только Provisioning/Active, INV-CONF-8). Единственная активная конференция на чат
страхуется ix_conf_active: гонка двух стартов — победитель создаёт, проигравший получает
MSG_CONFERENCE_ALREADY_ACTIVE (INV-CONF-2).
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|
ConferenceStartedEvent | доменное | [*] → Provisioning/Active | push ConferenceInvite (комната + токен минтится при доставке) в hub-группу chat:{ChatId} |
ConferenceParticipantJoinedEvent | доменное | активный участник вошёл (call-participant.md) | push MediaParticipant(Joined) в chat:{ChatId} |
ConferenceParticipantLeftEvent | доменное | участник вышел | push MediaParticipant(Left) в chat:{ChatId} |
ConferenceScreenShareChangedEvent | доменное | старт/стоп демонстрации экрана участником | push ScreenShareState в chat:{ChatId} |
ConferenceModeratorChangedEvent | доменное | передача роли модератора (INV-CONF-6) | push ConferenceSignal в chat:{ChatId} |
ConferenceEndedEvent | доменное + интеграционное | → Ended/Failed | доменный push ConferenceSignal(Ended); интеграционное pin.messenger.conference-ended.v1 (ключ DealId), консюмеры crm/notifications — контракт ../events/conference-ended.md |
Доменные события инициируют hub-push (eventName в snake_case, только non-PII). Интеграционное
ConferenceEndedEvent публикуется атомарно со сменой состояния через транзакционный outbox (persist-then-push).
Индексы, хранение, безопасность
| Аспект | Значение |
|---|
| Индексы | PK; ix_conf_deal_id (DealId); partial unique ix_conf_active where status in ('Provisioning','Active') — guard одной активной на чат (INV-CONF-2) + выборка reaper-а; ix_conf_last_activity (LastActivityAt) — stale-детекция; FK-index по ChatId |
| Партиционирование / retention | none (ADR-0056; групповые звонки редки на целевой нагрузке) |
| Concurrency | оптимистическая блокировка; единственная активная конференция на чат — command-guard (INV-CONF-2); reaper идемпотентен (обновляет только Provisioning/Active) |
| Permissions / PII | Permission messenger.conferences.use; доступ = членство в Deal-чате (INV-CHAT-4). Медиа/SDP/ICE/токены не персистятся (INV-CONF-7). Media/запись за feature-флагами CONFERENCE_CALLS_ENABLED/RECORDINGS_ENABLED |
| Audit | UpdatedAt + фазовые метки (StartedAt/EndedAt) — иммутабельные факты; ростер участников — бизнес-аудит состава (call-participant.md) |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Entity / Aggregate: CallRecordingEntity — запись звонка (хранение в file storage), Entity / Aggregate: CallSessionEntity — сигналинг и лог звонка (media отложен), Entity: CallParticipantEntity (owned-child ConferenceSessionEntity)
- API (17): GET /api/messenger/chats/{chatId}/conferences — История конференций сделки, GET /api/messenger/conferences/{conferenceId} — Карточка конференции, GET /api/messenger/conferences/{conferenceId}/recordings — Записи конференции, GET /api/messenger/recordings/{recordingId} — Карточка записи, POST /api/messenger/chats/{chatId}/conferences — Начать конференцию в чате сделки, POST /api/messenger/conferences/{conferenceId}/decline — Отклонить приглашение в конференцию, POST /api/messenger/conferences/{conferenceId}/end — Завершить конференцию для всех, POST /api/messenger/conferences/{conferenceId}/invite — Пригласить участников в конференцию, POST /api/messenger/conferences/{conferenceId}/join — Присоединиться к конференции, POST /api/messenger/conferences/{conferenceId}/leave — Выйти из конференции, POST /api/messenger/conferences/{conferenceId}/media-token — Обновить токен входа в конференцию, POST /api/messenger/conferences/{conferenceId}/participants/{userId}/remove — Удалить участника из конференции (+5)
- Use Cases (2): UC-MSG-006. Групповой звонок-конференция в чате сделки, UC-MSG-007. Авто-запись звонка по сделке и её выдача участнику
- События (1): Event Contract: pin.messenger.conference-ended (ConferenceEndedEvent)
- Фоновые задачи (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): Жизненный цикл сессии звонка — 1:1 и конференция (messenger), Медиа-звонки и запись на Jitsi — интеграционный гид (messenger), Медиа-звонки на Jitsi: план поставки (playbook аналитик → разработчик), Backend-реализация медиа-звонков: спека → код (messenger), Jitsi: именование комнат, admission и presence, Jitsi: нативный клиент lib-jitsi-meet на Next.js 16
- registries (1): Реестр сущностей PIN
Связанные документы