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, сценарий «звонок
поверх контактов».
| Поле | Значение |
|---|
| Сервис/контекст | messenger |
| API area | calls (сигналинг) |
| Feature group | calls |
| Статус | draft |
| Документ | docs/06-services/messenger/api/calls/decline-call.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User — только вызываемый (actor.UserId == CallSession.CalleeUserId) |
| Auth policy / Permissions | permission messenger.calls.use |
| Scope (RBAC) | платформа (person-level) |
| Record-level | доступ по membership: отклонить может только CalleeUserId; иначе MSG_CALL_FORBIDDEN |
| Tenant isolation | не применяется (ADR-0052); изоляция — по Caller/Callee |
| Audit | update: переход → Declined, метки EndedAt/EndReason = Declined (иммутабельный факт лога) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/messenger/calls/{callId}/decline |
| Success status | 200 (OK) |
| Idempotency header | не применяется — повтор ловит guard состояния (шаг 4): второй Decline уже терминален → MSG_CALL_INVALID_STATE |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
CallId | route | Guid | Да | != Empty; существующая сессия в Ringing, где актор — Callee (шаги 3–4) | — |
ActorUserId | identity claims | Guid | Да | из JWT; должен == CalleeUserId | — |
Тела запроса нет (SDP не передаётся — соединение не устанавливается). ICE/медиа-каналы для этой сессии не
открываются.
Модель ответа CallSessionModel
Проекция CallSessionEntity (дом — сущность). Дельта: терминальный Declined.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
CallId | Guid | Да | CallSessionEntity.Id | — | Идентификатор сессии. |
CallerUserId | Guid | Да | CallSessionEntity.CallerUserId | — | Инициатор. |
CalleeUserId | Guid | Да | CallSessionEntity.CalleeUserId | — | Вызываемый (актор). |
MediaType | CallMediaType | Да | CallSessionEntity.MediaType | — | Audio/Video. |
Status | CallStatus | Да | CallSessionEntity.Status | — | Declined на успехе. |
StartedAt | DateTimeOffset | Да | CallSessionEntity.StartedAt | — | Момент StartCall. |
EndedAt | DateTimeOffset | Да | CallSessionEntity.EndedAt | — | Момент отклонения. |
EndReason | CallEndReason | Да | CallSessionEntity.EndReason | — | Declined. |
DurationSeconds | int? | Нет | CallSessionEntity.DurationSeconds | — | null — соединение не было установлено. |
Алгоритм
- Контекст и доступ. Операцию выполняет вызываемый (
User); ActorUserId — из JWT; требуется право
messenger.calls.use. Медиа-звонки включены флагом CALLS_MEDIA_ENABLED, иначе MSG_CALLS_DISABLED.
- Проверка входа.
CallId непустой; иначе ошибка валидации. Тела запроса нет.
- Загрузка сессии. Сессия по
CallId; нет → MSG_CALL_NOT_FOUND. Контакт/чёрный список не проверяются
(отбой гейта общения не требует).
- Право и переход. Отклонить может только вызываемый:
ActorUserId != CalleeUserId →
MSG_CALL_FORBIDDEN. Переход Ringing → Declined (терминальный, INV-CALL-3): если статус не Ringing
→ MSG_CALL_INVALID_STATE. При успехе — Status = Declined, EndReason = Declined, EndedAt;
DurationSeconds остаётся null (соединения не было).
- Намерение доставки. В той же транзакции пишется строка
realtime_outbox CallDeclined (адресаты
user:{Caller}, user:{Callee}) — persist-then-push: обеим сторонам уходит CallSignal(Declined)
(без PII: CallId/Status) — caller прекращает набор, callee убирает входящий экран. Медиа-канал не
открывался. Kafka-событий нет.
- Согласованность. Переход
→ Declined фиксируется атомарно; если call-timeout-reaper уже перевёл
сессию в Missed, guard Status == Ringing не выполнится на перечитанном состоянии →
MSG_CALL_INVALID_STATE.
- Результат. Терминальная карточка
CallSessionModel (Status = Declined) → HTTP 200 OK.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none — звонок Kafka-события не публикует |
| Realtime push (hub) | CallSignal(Declined) → обеим сторонам (non-PII: CallId/Status) |
| External calls | none (медиа-комната не выдавалась) |
| Cache / projections / search | сессия выходит из partial ix_call_active (терминальный статус) |
| Notifications | пропущенный/отклонённый — системная строка чата отложена вместе с media-стеком |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | CallId = Empty | «Проверьте параметры запроса» | no retry |
MSG_CALL_NOT_FOUND | 404 | сессии нет или актор не участник (скрытие) | «Звонок не найден» | no retry |
MSG_CALL_FORBIDDEN | 403 | актор не Callee (отклонить может только вызываемый) | «Недостаточно прав для отклонения звонка» | no retry |
MSG_CALL_INVALID_STATE | 409 | Status != Ringing (уже Answered/Declined/Missed/Ended, INV-CALL-3) | «Звонок уже завершён» | no retry (перечитать) |
MSG_CALLS_DISABLED | 503 | CALLS_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).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы