POST /api/messenger/conferences/{conferenceId}/end — Завершить конференцию для всех
Модератор завершает групповой звонок. ConferenceSessionEntity переходит
Active → Ended (ModeratorEnded), считается длительность, останавливается запись, а факт видеовстречи
публикуется в crm/notifications через Kafka. Дом полей, статусы и инварианты (INV-CONF-4) —
conference-session.md.
Назначение
Дать модератору принудительно закрыть конференцию для всех участников: зафиксировать терминальный статус и
длительность, запросить остановку рекордера, разослать сигнал завершения и положить карточку «видеовстреча по
сделке» в таймлайн сделки (через интеграционное событие). Owning-сервис — messenger, feature group
conferences.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | messenger |
| API area | conferences |
| Feature group | conferences |
| Статус | draft |
| Документ | docs/06-services/messenger/api/conferences/end-conference.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User — инициатор (StartedByUserId) или участник с ролью Moderator |
| Auth policy / Permissions | permission messenger.conferences.use |
| Scope (RBAC) | не применяется — доступ по членству в чате |
| Record-level | активный участник Deal-чата (INV-CHAT-4) и роль Moderator / инициатор (INV-CONF-6) |
| Tenant isolation | не применяется (кросс-tenant); срез — по членству |
| Audit | update: → Ended, метки EndedAt/EndReason = ModeratorEnded/DurationSeconds (иммутабельный факт) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/messenger/conferences/{conferenceId}/end |
| Success status | 200 (OK) |
| Idempotency header | не применяется — повтор ловит guard состояния (шаг 4): вторая End уже терминальна → MSG_CONFERENCE_INVALID_STATE |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ConferenceId | route | Guid | Да | != Empty; существующая конференция в Active, актор — модератор (шаги 3–4) | — |
ActorUserId | identity claims | Guid | Да | из JWT; = StartedByUserId или ConferenceRole = Moderator | — |
Тела запроса нет. EndReason не передаётся клиентом — сервер ставит ModeratorEnded.
Модель ответа ConferenceModel
Проекция ConferenceSessionEntity в терминальном Ended (дом полей —
start-conference.md). Поле Room отсутствует (комната закрыта).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
ConferenceId | Guid | Да | ConferenceSessionEntity.Id | — | Идентификатор конференции. |
Status | ConferenceStatus | Да | .Status | — | Ended на успехе. |
EndedAt | DateTimeOffset? | Нет | .EndedAt | — | Момент завершения. |
DurationSeconds | int? | Нет | .DurationSeconds | — | EndedAt − StartedAt (≥ 0). |
RecordingStatus | RecordingStatus? | Нет | CallRecordingEntity.Status | — | Обычно Uploading (готовность придёт событием call-recording-available). |
RecordingId | Guid? | Нет | CallRecordingEntity.Id | — | Запись сессии, если велась. |
Participants | ConferenceParticipantEntry[] | Да | ростер (call-participant.md) | — | Итоговый состав (все — IsActive = false). |
Предусловия и постусловия
Предусловия. Актор авторизован и имеет messenger.conferences.use; активный участник Deal-чата (INV-CHAT-4).
Актор — инициатор (StartedByUserId) или несёт ConferenceRole = Moderator (INV-CONF-6). Конференция в
статусе Active (INV-CONF-4).
Постусловия. Конференция терминальна: Status = Ended, EndReason = ModeratorEnded, EndedAt проставлен,
DurationSeconds = EndedAt − StartedAt (≥ 0). Все активные строки ростера закрыты (State = Left,
LeaveReason = ConferenceEnded, с их DurationSeconds); ActiveParticipantCount = 0; сессия ушла из partial
ix_conf_active. Если велась запись — она переведена в Uploading (готовность придёт событием
call-recording-available). В канал chat:{ChatId} отправлен ConferenceSignal(Ended). В шину опубликовано
интеграционное pin.messenger.conference-ended.v1 (ключ DealId, ContractVersion) — атомарно со сменой
состояния через outbox.
Кросс-сервисный контракт (что предоставляем). pin.messenger.conference-ended.v1 несёт факт видеовстречи
(идентификатор конференции, DealId, DurationSeconds, PeakParticipantCount, признак записи) — ключ
партиционирования DealId. Потребители асинхронны: crm кладёт карточку «видеовстреча по сделке» в таймлайн,
notifications шлёт «встреча завершена / вы пропустили», dev-stats учитывает факт. Окно eventual-consistency
— секунды; инициатор видит Ended немедленно (синхронный ответ), карточка в таймлайне сделки и офлайн-пуши
подтягиваются позже. Отказ media-стека при остановке рекордера/закрытии комнаты не блокирует терминальный
переход (запись досогласуют воркеры записи).
Алгоритм
- Контекст и доступ. Операцию выполняет модератор конференции (
User);ActorUserId— из JWT; требуется правоmessenger.conferences.use. - Проверка входа.
ConferenceIdнепустой; иначе ошибка валидации.EndReasonвыводится сервером (ModeratorEnded). - Загрузка сессии. Конференция по
ConferenceId; нет →MSG_CONFERENCE_NOT_FOUND. Актор — активный участник Deal-чата (INV-CHAT-4); иначеMSG_CHAT_ACCESS_DENIED. - Право и переход. Актор должен быть инициатором (
StartedByUserId) или иметьConferenceRole = Moderator; иначеMSG_CONFERENCE_FORBIDDEN. Допустимый источник перехода —Status == Active(INV-CONF-4); прочие статусы (Provisioning/Ended/Failed) →MSG_CONFERENCE_INVALID_STATE. При успехе:Status = Ended,EndReason = ModeratorEnded,EndedAt,DurationSeconds = EndedAt − StartedAt; все активные строки ростера закрываютсяLeft/ConferenceEndedс пересчётом ихDurationSeconds. - Что меняется. Терминальный переход конференции, закрытие ростера и — если велась запись — запрос остановки
рекордера (
Recording → Uploading, call-recording.md) применяются в одном согласованном изменении. Одновременно фиксируются: намерение pushConferenceSignal(Ended)вchat:{ChatId}и интеграционное событиеpin.messenger.conference-ended.v1(ключDealId) — оба атомарно со сменой состояния (транзакционный outbox, persist-then-push). - Согласованность. Если reaper/закрытие чата уже перевели конференцию в терминал, guard источника не
выполнится на перечитанном состоянии →
MSG_CONFERENCE_INVALID_STATE(повтор — no-op на терминале). - Результат. Терминальная карточка
ConferenceModel(Status = Ended,DurationSeconds, безRoom) → HTTP200 OK. Готовность записи придёт отдельным событиемcall-recording-available.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | pin.messenger.conference-ended.v1 (ключ DealId) — атомарно с → Ended (conference-ended.md); консюмеры crm/notifications/dev-stats |
| Realtime push (hub) | ConferenceSignal(Ended) → chat:{ChatId}; если велась запись — далее RecordingState при готовности |
| External calls | запрос остановки рекордера/закрытия комнаты в media-стеке (SFU); отказ не блокирует терминальный переход |
| Cache / projections / search | сессия выходит из partial ix_conf_active; ростер закрыт |
| Notifications | «встреча завершена / вы пропустили» — консюмер notifications события conference-ended |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | ConferenceId = Empty | «Проверьте параметры запроса» | no retry |
MSG_CONFERENCE_NOT_FOUND | 404 | конференции нет | «Конференция не найдена» | no retry |
MSG_CHAT_ACCESS_DENIED | 403 | актор не участник Deal-чата | «Нет доступа к конференции» | no retry |
MSG_CONFERENCE_FORBIDDEN | 403 | актор не модератор и не инициатор | «Завершить конференцию может только модератор» | no retry |
MSG_CONFERENCE_INVALID_STATE | 409 | статус не Active (уже терминален, INV-CONF-4) | «Конференция уже завершена» | no retry (перечитать) |
MSG_CONFERENCES_DISABLED | 503 | CONFERENCE_CALLS_ENABLED = false | «Конференции временно недоступны» | retry позже |
Acceptance Criteria
- Завершение модератором (happy). Дано: конференция
Active, актор — модератор, велась запись. Когда завершает. Тогда200сConferenceModel(Status = Ended,DurationSeconds ≥ 0, безRoom); ростер закрыт; участники получаютConferenceSignal(Ended); опубликованоconference-ended.v1(ключDealId); запись →Uploading. - Не модератор. Дано: актор — участник Deal-чата, но не модератор и не инициатор. Когда завершает. Тогда
403 MSG_CONFERENCE_FORBIDDEN; состояние конференции не меняется. - Не участник чата. Дано: актор не активный участник Deal-чата. Когда завершает. Тогда
403 MSG_CHAT_ACCESS_DENIED. - Повторное завершение (идемпотентность на терминале). Дано: конференция уже
Ended(модератором или reaper-ом). Когда актор завершает снова. Тогда409 MSG_CONFERENCE_INVALID_STATE; повторного события и повторного пересчёта длительности нет. - Гонка с reaper-ом. Дано:
conference-room-reaperперевёл сессию в терминал между чтением и записью. Тогда guard источника не проходит на перечитанном состоянии →409 MSG_CONFERENCE_INVALID_STATE(единственный победитель, событие публикуется один раз). - Нет конференции. Дано:
ConferenceIdне существует. Когда актор завершает. Тогда404 MSG_CONFERENCE_NOT_FOUND. - Флаг выключен. Дано:
CONFERENCE_CALLS_ENABLED = false. Когда актор завершает. Тогда503 MSG_CONFERENCES_DISABLED.
Совместимость и наблюдаемость
- Новые optional поля ответа — additive; смена маршрута/семантики статуса — breaking. Контракт Kafka-события —
conference-ended.md (
ContractVersion, additive-only для minor). - Logs:
conference end requested/ended/invalid-state(ConferenceId,DealId,DurationSeconds,PeakParticipantCount); metrics:messenger_conference_end_total{result},messenger_conference_duration_seconds(histogram), latency p95; traces — сквозной trace (traceparent); dashboardpin-messenger.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: ConferenceSessionEntity — групповой звонок в чате сделки
- API (7): GET /api/messenger/chats/{chatId}/conferences — История конференций сделки, GET /api/messenger/conferences/{conferenceId} — Карточка конференции, POST /api/messenger/chats/{chatId}/conferences — Начать конференцию в чате сделки, 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 — Удалить участника из конференции
- Use Cases (1): UC-MSG-006. Групповой звонок-конференция в чате сделки
- События (1): Event Contract: pin.messenger.conference-ended (ConferenceEndedEvent)
- Фоновые задачи (1): Background Job: ConferenceRoomReaperJob
- calls (1): Жизненный цикл сессии звонка — 1:1 и конференция (messenger)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей, статусы,
INV-CONF-4/6,ConferenceEndReason): conference-session.md; ростер: call-participant.md; запись: call-recording.md. - Событие завершения (payload, консюмеры, ключ): conference-ended.md; готовность записи: call-recording-available.md.
- Соседние операции: start-conference.md, join-conference.md, leave-conference.md, get-conference.md; авто-завершение: conference-room-reaper.md; контекст: README.
- ADR: 0062 медиа-стек и запись, 0055 свой мини-мессенджер.