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

POST /api/messenger/calls/{callId}/decline — Отклонить звонок

Вызываемый (Callee) отклоняет входящий звонок. Переход CallSessionEntity Ringing → Declined (терминальный, метки EndedAt + EndReason = Declined, INV-CALL-3). Дом полей, статусы и инварианты — call-session.md.

Media-стек — внешний SFU (ADR-0062). Отклонение соединения не устанавливает: только переход статуса + push отбоя обеим сторонам. Медиа-комната не выдаётся; SDP/ICE/токены не персистятся (INV-CALL-4/6). Медиа-звонки за флагом CALLS_MEDIA_ENABLED.

Назначение

Дать callee отклонить приглашение до соединения: зафиксировать терминальный Declined в журнале и уведомить caller об отбое (UI прекращает набор). Owning-сервис — messenger, feature group calls, сценарий «звонок поверх контактов».

Metadata

ПолеЗначение
Сервис/контекстmessenger
API areacalls (сигналинг)
Feature groupcalls
Статусdraft
Документdocs/06-services/messenger/api/calls/decline-call.md

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

ПроверкаЗначение
Actor typeUserтолько вызываемый (actor.UserId == CallSession.CalleeUserId)
Auth policy / Permissionspermission messenger.calls.use
Scope (RBAC)платформа (person-level)
Record-levelдоступ по membership: отклонить может только CalleeUserId; иначе MSG_CALL_FORBIDDEN
Tenant isolationне применяется (ADR-0052); изоляция — по Caller/Callee
Auditupdate: переход → Declined, метки EndedAt/EndReason = Declined (иммутабельный факт лога)

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
CallIdrouteGuidДа!= Empty; существующая сессия в Ringing, где актор — Callee (шаги 3–4)
ActorUserIdidentity claimsGuidДаиз JWT; должен == CalleeUserId

Тела запроса нет (SDP не передаётся — соединение не устанавливается). ICE/медиа-каналы для этой сессии не открываются.

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

Проекция CallSessionEntity (дом — сущность). Дельта: терминальный Declined.

ПолеТипОбязательностьИсточникMaskingОписание
CallIdGuidДаCallSessionEntity.IdИдентификатор сессии.
CallerUserIdGuidДаCallSessionEntity.CallerUserIdИнициатор.
CalleeUserIdGuidДаCallSessionEntity.CalleeUserIdВызываемый (актор).
MediaTypeCallMediaTypeДаCallSessionEntity.MediaTypeAudio/Video.
StatusCallStatusДаCallSessionEntity.StatusDeclined на успехе.
StartedAtDateTimeOffsetДаCallSessionEntity.StartedAtМомент StartCall.
EndedAtDateTimeOffsetДаCallSessionEntity.EndedAtМомент отклонения.
EndReasonCallEndReasonДаCallSessionEntity.EndReasonDeclined.
DurationSecondsint?НетCallSessionEntity.DurationSecondsnull — соединение не было установлено.

Алгоритм

  1. Контекст и доступ. Операцию выполняет вызываемый (User); ActorUserId — из JWT; требуется право messenger.calls.use. Медиа-звонки включены флагом CALLS_MEDIA_ENABLED, иначе MSG_CALLS_DISABLED.
  2. Проверка входа. CallId непустой; иначе ошибка валидации. Тела запроса нет.
  3. Загрузка сессии. Сессия по CallId; нет → MSG_CALL_NOT_FOUND. Контакт/чёрный список не проверяются (отбой гейта общения не требует).
  4. Право и переход. Отклонить может только вызываемый: ActorUserId != CalleeUserIdMSG_CALL_FORBIDDEN. Переход Ringing → Declined (терминальный, INV-CALL-3): если статус не RingingMSG_CALL_INVALID_STATE. При успехе — Status = Declined, EndReason = Declined, EndedAt; DurationSeconds остаётся null (соединения не было).
  5. Намерение доставки. В той же транзакции пишется строка realtime_outbox CallDeclined (адресаты user:{Caller}, user:{Callee}) — persist-then-push: обеим сторонам уходит CallSignal(Declined) (без PII: CallId/Status) — caller прекращает набор, callee убирает входящий экран. Медиа-канал не открывался. Kafka-событий нет.
  6. Согласованность. Переход → Declined фиксируется атомарно; если call-timeout-reaper уже перевёл сессию в Missed, guard Status == Ringing не выполнится на перечитанном состоянии → MSG_CALL_INVALID_STATE.
  7. Результат. Терминальная карточка CallSessionModel (Status = Declined) → HTTP 200 OK.

Диаграмма

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

ТипДетали
Integration eventsnone — звонок Kafka-события не публикует
Realtime push (hub)CallSignal(Declined) → обеим сторонам (non-PII: CallId/Status)
External callsnone (медиа-комната не выдавалась)
Cache / projections / searchсессия выходит из partial ix_call_active (терминальный статус)
Notificationsпропущенный/отклонённый — системная строка чата отложена вместе с media-стеком

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400CallId = Empty«Проверьте параметры запроса»no retry
MSG_CALL_NOT_FOUND404сессии нет или актор не участник (скрытие)«Звонок не найден»no retry
MSG_CALL_FORBIDDEN403актор не Callee (отклонить может только вызываемый)«Недостаточно прав для отклонения звонка»no retry
MSG_CALL_INVALID_STATE409Status != Ringing (уже Answered/Declined/Missed/Ended, INV-CALL-3)«Звонок уже завершён»no retry (перечитать)
MSG_CALLS_DISABLED503CALLS_MEDIA_ENABLED = false«Звонки временно недоступны»retry позже

Совместимость и наблюдаемость

  • Новые optional поля ответа — additive; смена маршрута/семантики статуса — breaking. Kafka-контракта нет.
  • Logs: call decline requested/declined/invalid-state (CallId, участники); metrics: messenger_call_decline_total{result}, latency p95; traces — сквозной trace (traceparent); dashboard pin-messenger.

Acceptance Criteria

  • Happy path. Given сессия в Ringing, актор — Callee, CALLS_MEDIA_ENABLED; When POST /decline; Then 200, переход Ringing → Declined (терминальный, INV-CALL-3), EndReason = Declined, EndedAt, DurationSeconds = null, CallSignal(Declined) обеим сторонам.
  • Отклонить может только Callee. Given актор — Caller; When POST; Then 403 MSG_CALL_FORBIDDEN (инициатор отменяет через end-call.md, не decline).
  • Уже терминален. Given статус не Ringing; When POST; Then 409 MSG_CALL_INVALID_STATE (INV-CALL-3).
  • Гонка с reaper. Given call-timeout-reaper уже перевёл Ringing → Missed; When POST; Then 409 MSG_CALL_INVALID_STATE.
  • Нет сессии / скрытие. Given несуществующий CallId или актор не участник; When POST; Then 404 MSG_CALL_NOT_FOUND.
  • Флаг выключен. Given CALLS_MEDIA_ENABLED = false; When POST; Then 503 MSG_CALLS_DISABLED.
  • Без медиа. На любом исходе медиа-комната не выдаётся, SDP/ICE/токены не персистятся (INV-CALL-4/6).

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

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

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