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

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 areaconferences
Feature groupconferences
Статусdraft
Документdocs/06-services/messenger/api/conferences/end-conference.md

Актор и доступ

ПроверкаЗначение
Actor typeUser — инициатор (StartedByUserId) или участник с ролью Moderator
Auth policy / Permissionspermission messenger.conferences.use
Scope (RBAC)не применяется — доступ по членству в чате
Record-levelактивный участник Deal-чата (INV-CHAT-4) и роль Moderator / инициатор (INV-CONF-6)
Tenant isolationне применяется (кросс-tenant); срез — по членству
Auditupdate: → Ended, метки EndedAt/EndReason = ModeratorEnded/DurationSeconds (иммутабельный факт)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/messenger/conferences/{conferenceId}/end
Success status200 (OK)
Idempotency headerне применяется — повтор ловит guard состояния (шаг 4): вторая End уже терминальна → MSG_CONFERENCE_INVALID_STATE
Correlationсквозной trace через traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ConferenceIdrouteGuidДа!= Empty; существующая конференция в Active, актор — модератор (шаги 3–4)
ActorUserIdidentity claimsGuidДаиз JWT; = StartedByUserId или ConferenceRole = Moderator

Тела запроса нет. EndReason не передаётся клиентом — сервер ставит ModeratorEnded.

Модель ответа ConferenceModel

Проекция ConferenceSessionEntity в терминальном Ended (дом полей — start-conference.md). Поле Room отсутствует (комната закрыта).

ПолеТипОбязательностьИсточникMaskingОписание
ConferenceIdGuidДаConferenceSessionEntity.IdИдентификатор конференции.
StatusConferenceStatusДа.StatusEnded на успехе.
EndedAtDateTimeOffset?Нет.EndedAtМомент завершения.
DurationSecondsint?Нет.DurationSecondsEndedAt − StartedAt (≥ 0).
RecordingStatusRecordingStatus?НетCallRecordingEntity.StatusОбычно Uploading (готовность придёт событием call-recording-available).
RecordingIdGuid?НетCallRecordingEntity.IdЗапись сессии, если велась.
ParticipantsConferenceParticipantEntry[]Даростер (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-стека при остановке рекордера/закрытии комнаты не блокирует терминальный переход (запись досогласуют воркеры записи).

Алгоритм

  1. Контекст и доступ. Операцию выполняет модератор конференции (User); ActorUserId — из JWT; требуется право messenger.conferences.use.
  2. Проверка входа. ConferenceId непустой; иначе ошибка валидации. EndReason выводится сервером (ModeratorEnded).
  3. Загрузка сессии. Конференция по ConferenceId; нет → MSG_CONFERENCE_NOT_FOUND. Актор — активный участник Deal-чата (INV-CHAT-4); иначе MSG_CHAT_ACCESS_DENIED.
  4. Право и переход. Актор должен быть инициатором (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.
  5. Что меняется. Терминальный переход конференции, закрытие ростера и — если велась запись — запрос остановки рекордера (Recording → Uploading, call-recording.md) применяются в одном согласованном изменении. Одновременно фиксируются: намерение push ConferenceSignal(Ended) в chat:{ChatId} и интеграционное событие pin.messenger.conference-ended.v1 (ключ DealId) — оба атомарно со сменой состояния (транзакционный outbox, persist-then-push).
  6. Согласованность. Если reaper/закрытие чата уже перевели конференцию в терминал, guard источника не выполнится на перечитанном состоянии → MSG_CONFERENCE_INVALID_STATE (повтор — no-op на терминале).
  7. Результат. Терминальная карточка ConferenceModel (Status = Ended, DurationSeconds, без Room) → HTTP 200 OK. Готовность записи придёт отдельным событием call-recording-available.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventspin.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_FAILED400ConferenceId = Empty«Проверьте параметры запроса»no retry
MSG_CONFERENCE_NOT_FOUND404конференции нет«Конференция не найдена»no retry
MSG_CHAT_ACCESS_DENIED403актор не участник Deal-чата«Нет доступа к конференции»no retry
MSG_CONFERENCE_FORBIDDEN403актор не модератор и не инициатор«Завершить конференцию может только модератор»no retry
MSG_CONFERENCE_INVALID_STATE409статус не Active (уже терминален, INV-CONF-4)«Конференция уже завершена»no retry (перечитать)
MSG_CONFERENCES_DISABLED503CONFERENCE_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); dashboard pin-messenger.

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

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

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