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

UC-SUP-005. Эскалация администратору

Назначение

Описывает эскалацию супервизором Службы заботы обращения или арбитражного дела на уровень администратора, когда вопрос не решается силами поддержки (матрица 4.5/6.5). Эскалация фиксируется иммутабельным флагом и детерминированно оповещает стороны событием; приём эскалации — вне контекста support: обрабатывает администратор платформы через свою границу (ADR-0053), синхронного вызова администратора платформы нет. Связан с контекстом support; агрегаты — TicketEntity, ArbitrageCaseEntity. Покрывает README решение №6.

Metadata

ПолеЗначение
IDUC-SUP-005
Контекстsupport (приём — администратор платформы, ADR-0053)
Типосновной (+ альтернативные потоки A1–A2)
Статусdraft

Актор

ПолеЗначение
РольСлужбаЗаботы — Супервизор
Permissions / scopeтикет — support.tickets.escalate; арбитраж — support.arbitrage.decide — platform-scope (ADR-0052)
TenantTenantId = PIN CompanyId (единый tenant платформы)
Record-levelне ограничивает: супервизор эскалирует любой тикет/дело платформы независимо от AssigneeUserId

Предусловие

  • Супервизор аутентифицирован, несёт support.tickets.escalate / support.arbitrage.decide.
  • Целевой тикет в Status ∈ {New, InWork} и ранее не эскалирован (EscalatedToAdminAt is null); либо дело в UnderInvestigation.
  • Граница приёма эскалаций администратором платформы развёрнута и активна (ADR-0053).

Гарантируется после сценария: тикет несёт EscalatedToAdminAt/EscalatedByUserId (иммутабельно, один раз), Status = InWork; либо дело в Escalated с Decision.Outcome = EscalatedToAdmin; опубликован ticket-status-changed.v1 (ChangeReason = Escalated) или arbitrage-resolved.v1 (Escalated = true); стороны/заявитель уведомлены; администратор платформы принял сигнал асинхронно на своей границе (синхронного вызова нет).

Триггер

ПолеЗначение
ИсточникUI button / API call
Триггерящий элементКарточка тикета/дела в кабинете супервизора, кнопка «Эскалировать администратору»

Основной Поток

  1. Супервизор в карточке тикета указывает причину и опц. ответственного (админ-очередь) → POST /api/support/tickets/{id}/escalate (escalate-ticket).
  2. Сервис авторизует support.tickets.escalate, проверяет Reason (1..1024) и находит тикет.
  3. Проверка: Status ∈ {New, InWork} (иначе SUPPORT_TICKET_INVALID_STATE); EscalatedToAdminAt is null (иначе SUPPORT_TICKET_ALREADY_ESCALATED).
  4. Эскалация тикета: EscalatedToAdminAt = now, EscalatedByUserId = actor, опц. переназначение AssigneeUserId; переход New → InWork при необходимости; ChangeReason = Escalated.
  5. Переход и событие TicketStatusChanged (ChangeReason = Escalated, в outbox) фиксируются в одном согласованном изменении; конкурентный доступ (гонка с SLA-worker) исключается оптимистичной блокировкой.
  6. Возврат 200; синхронного вызова администратора платформы нет.
  7. notifications (консюмер ticket-status-changed) оповещает заявителя (обращение эскалировано) и дежурного/админ-очередь.
  8. Администратор платформы принимает эскалацию на своей границе (async, по событию/флагу — ADR-0053) и продолжает разбор вне support.

Альтернативные Потоки

A1. Эскалация арбитражного дела

  1. Супервизор → POST /api/support/arbitrage-cases/{id}/escalate (escalate-arbitrage-case) с обоснованием.
  2. Эскалация дела (INV-ARB-3): Status = Escalated, Decision.Outcome = EscalatedToAdmin, EscalatedToAdminAt/By; событие ArbitrageResolved (Escalated = true, топик pin.support.arbitrage-resolved.v1) в outbox.
  3. notifications информирует стороны конфликта (6.4); приём — администратор платформы.

Завершается статусом Escalated (терминальный до Closed).

A2. Повторная/недопустимая эскалация

  1. Тикет уже эскалирован (EscalatedToAdminAt != null) → SUPPORT_TICKET_ALREADY_ESCALATED (флаг ставится один раз); либо Status ∈ {Resolved, Closed}SUPPORT_TICKET_INVALID_STATE.

Завершается ошибкой; мутаций нет.

A3. Невалидный ввод эскалации

  1. Reason пустой или > 1024; TargetAssigneeUserId = Guid.EmptyVALIDATION_FAILED (400); эскалации нет.

Завершается ошибкой валидации.

A4. Недопустимая эскалация дела

  1. Эскалация арбитражного дела не из UnderInvestigation (напр. из Registered/уже Resolved/Escalated) → SUPPORT_ARBITRAGE_STATUS_TRANSITION (409, INV-ARB-1). Resolve и Escalate взаимоисключающи — второй терминальный переход отклоняется.

Завершается ошибкой; показать допустимые действия.

A5. Гонка с SLA-worker

  1. Супервизор эскалирует одновременно с фоновым SLA-worker, помечающим SlaBreached/*DueAt: оптимистичная блокировка пропускает первого, проигравший → SUPPORT_TICKET_CONCURRENCY (409) и перечитывает свежий тикет. Флаг EscalatedToAdminAt при этом ставится ровно один раз.

Возвращается к основному потоку после перечитывания.

Постусловие

  • Тикет: EscalatedToAdminAt/EscalatedByUserId заданы (иммутабельно), Status = InWork, опц. переназначен; Version++.
  • Дело (A1): Status = Escalated, иммутабельный Decision.Outcome = EscalatedToAdmin.
  • В outbox — TicketStatusChangedEvent (Escalated) или ArbitrageResolvedEvent; стороны уведомлены; администратор платформы принял сигнал на своей границе.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
UNAUTHENTICATED401нет/невалидный JWTОтправить на вход
FORBIDDEN403нет support.tickets.escalate / support.arbitrage.decideСкрыть эскалацию
VALIDATION_FAILED400Reason пустой/>1024; TargetAssigneeUserId = Guid.EmptyПодсветить поля
SUPPORT_TICKET_NOT_FOUND404тикет не найден/вне scopeОбновить список
SUPPORT_TICKET_INVALID_STATE409Status ∉ {New, InWork}Показать «нельзя эскалировать»
SUPPORT_TICKET_ALREADY_ESCALATED409EscalatedToAdminAt уже заданПоказать «уже эскалировано»
SUPPORT_ARBITRAGE_STATUS_TRANSITION409эскалация дела не из UnderInvestigation (INV-ARB-1)Показать допустимые действия
SUPPORT_TICKET_CONCURRENCY409гонка с SLA-worker (оптимистичная блокировка)Перечитать и повторить

Доменные События

СобытиеКогда поднимаетсяSubscribers
TicketEscalatedToAdmin / TicketStatusChangedTicket.EscalateToAdminтаймлайн, сборщик TicketStatusChangedEvent
ArbitrageResolvedArbitrageCase.Escalate (A1)таймлайн, сборщик ArbitrageResolvedEvent

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
TicketStatusChangedEventpin.support.ticket-status-changed.v1эскалация тикета (ChangeReason = Escalated, шаг 5) — контрактnotifications (заявитель + админ-очередь)
ArbitrageResolvedEventpin.support.arbitrage-resolved.v1эскалация дела (Escalated = true, A1) — контрактnotifications (стороны, 6.4)

Кросс-сервисные ожидания и согласованность

Ожидает от соседей (вход)

  • identity (синхронно). JWT + support.tickets.escalate / support.arbitrage.decide (platform-scope, record-level не ограничивает — супервизор эскалирует любой тикет/дело). Недоступность → 401/403/503.
  • Администратор платформы — асинхронный приём, без синхронного вызова. support не дёргает границу администратора синхронно; приём эскалации происходит на его стороне по событию/флагу (ADR-0053). support не ожидает синхронного ответа администратора и не блокируется его доступностью.

Предоставляет соседям (выход)

  • pin.support.ticket-status-changed.v1 (ключ = TicketId, ContractVersion = 1, 3 партиции): ChangeReason = Escalated, переход New → InWork при необходимости; потребитель notifications уведомляет заявителя и админ-очередь. Публикуется атомарно с выставлением флага EscalatedToAdminAt.
  • pin.support.arbitrage-resolved.v1 (ключ = CaseId, ContractVersion = 1): Escalated = true, Outcome = EscalatedToAdmin; потребитель notifications информирует стороны (6.4). Терминальный сигнал дела; закрытие (→ Closed) события не порождает.

Окно согласованности и промежуточное состояние

  • Эскалация → уведомление сторон и приём администратором: асинхронно через outbox, окно < 1 мин. В промежутке супервизор видит тикет уже с флагом эскалации (Status = InWork), заявитель — прежнее состояние до доставки уведомления; администратор подхватывает сигнал на своей границе по мере доставки события.
  • Дальнейший разбор ведётся администратором вне support; поле EscalatedToAdminAt фиксирует момент передачи однократно и иммутабельно.

Идемпотентность и конкурентность

  • Флаг EscalatedToAdminAt ставится ровно один раз: повтор → SUPPORT_TICKET_ALREADY_ESCALATED, мутаций и нового события нет (A2).
  • Гонка супервизор ↔ SLA-worker разводится оптимистичной блокировкой (A5): проигравший → SUPPORT_TICKET_CONCURRENCY, перечитывает. Для дела Resolve/Escalate взаимоисключающи (INV-ARB-1).
  • Дубли интеграционных событий у notifications снимаются дедупликацией по MessageId (at-least-once).

Acceptance Criteria

#Given (дано)When (когда)Then (тогда)
AC1Тикет Status ∈ {New, InWork}, не эскалирован; супервизор с support.tickets.escalateсупервизор эскалируетEscalatedToAdminAt/EscalatedByUserId заданы, Status = InWork, опубликован ticket-status-changed.v1 (ChangeReason = Escalated)
AC2Тикет уже эскалирован (EscalatedToAdminAt != null)супервизор эскалирует повторноSUPPORT_TICKET_ALREADY_ESCALATED (409), мутаций и события нет (флаг один раз)
AC3Тикет Status ∈ {Resolved, Closed}супервизор эскалируетSUPPORT_TICKET_INVALID_STATE (409)
AC4Эскалация зафиксированапроверяется взаимодействие с администраторомсинхронного вызова администратора нет; приём — асинхронно по событию/флагу на его границе (ADR-0053)
AC5Дело UnderInvestigation; супервизор с support.arbitrage.decideсупервизор эскалирует делоStatus = Escalated, Outcome = EscalatedToAdmin, опубликован arbitrage-resolved.v1 (Escalated = true)
AC6Эскалация тикета/дела зафиксированасобытие доставленоnotifications информирует заявителя/стороны в окне < 1 мин
AC7Reason пуст/>1024 или TargetAssigneeUserId = Guid.Emptyсупервизор отправляет эскалациюVALIDATION_FAILED (400), мутаций нет

Наблюдаемость и НФТ

  • Латентность: операция эскалации p95 < 500 мс; администратор принимает асинхронно (не в латентности операции). Объём эскалаций мал.
  • Логи (без PII): ticket-escalated (ticketId, escalatedByUserId, reasonLen), arbitrage-escalated (caseId, escalatedByUserId); текст Reason — без PII/секретов по контракту, полностью в лог не пишется.
  • Метрики: число эскалаций тикетов/дел, доля повторных/недопустимых попыток, лаг приёма администратором (по событию), лаг консюмера notifications.
  • Доступ/безопасность: эскалация тикета — support.tickets.escalate; дела — support.arbitrage.decide (супервизор, platform-scope, record-level не ограничивает). Флаг эскалации иммутабелен (аудит передачи).

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

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

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