UC-SUP-005. Эскалация администратору
Назначение
Описывает эскалацию супервизором Службы заботы обращения или арбитражного дела на уровень администратора,
когда вопрос не решается силами поддержки (матрица 4.5/6.5). Эскалация фиксируется иммутабельным флагом и
детерминированно оповещает стороны событием; приём эскалации — вне контекста support: обрабатывает
администратор платформы через свою границу (ADR-0053),
синхронного вызова администратора платформы нет. Связан с контекстом support; агрегаты —
TicketEntity, ArbitrageCaseEntity. Покрывает
README решение №6.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-SUP-005 |
| Контекст | support (приём — администратор платформы, ADR-0053) |
| Тип | основной (+ альтернативные потоки A1–A2) |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | СлужбаЗаботы — Супервизор |
| Permissions / scope | тикет — support.tickets.escalate; арбитраж — support.arbitrage.decide — platform-scope (ADR-0052) |
| Tenant | TenantId = 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 |
| Триггерящий элемент | Карточка тикета/дела в кабинете супервизора, кнопка «Эскалировать администратору» |
Основной Поток
- Супервизор в карточке тикета указывает причину и опц. ответственного (админ-очередь) →
POST /api/support/tickets/{id}/escalate(escalate-ticket). - Сервис авторизует
support.tickets.escalate, проверяетReason(1..1024) и находит тикет. - Проверка:
Status ∈ {New, InWork}(иначеSUPPORT_TICKET_INVALID_STATE);EscalatedToAdminAt is null(иначеSUPPORT_TICKET_ALREADY_ESCALATED). - Эскалация тикета:
EscalatedToAdminAt = now,EscalatedByUserId = actor, опц. переназначениеAssigneeUserId; переходNew → InWorkпри необходимости;ChangeReason = Escalated. - Переход и событие
TicketStatusChanged(ChangeReason = Escalated, в outbox) фиксируются в одном согласованном изменении; конкурентный доступ (гонка с SLA-worker) исключается оптимистичной блокировкой. - Возврат
200; синхронного вызова администратора платформы нет. notifications(консюмерticket-status-changed) оповещает заявителя (обращение эскалировано) и дежурного/админ-очередь.- Администратор платформы принимает эскалацию на своей границе (async, по событию/флагу — ADR-0053) и продолжает разбор вне
support.
Альтернативные Потоки
A1. Эскалация арбитражного дела
- Супервизор →
POST /api/support/arbitrage-cases/{id}/escalate(escalate-arbitrage-case) с обоснованием. - Эскалация дела (INV-ARB-3):
Status = Escalated,Decision.Outcome = EscalatedToAdmin,EscalatedToAdminAt/By; событиеArbitrageResolved(Escalated = true, топикpin.support.arbitrage-resolved.v1) в outbox. notificationsинформирует стороны конфликта (6.4); приём — администратор платформы.
Завершается статусом Escalated (терминальный до Closed).
A2. Повторная/недопустимая эскалация
- Тикет уже эскалирован (
EscalatedToAdminAt != null) →SUPPORT_TICKET_ALREADY_ESCALATED(флаг ставится один раз); либоStatus ∈ {Resolved, Closed}→SUPPORT_TICKET_INVALID_STATE.
Завершается ошибкой; мутаций нет.
A3. Невалидный ввод эскалации
Reasonпустой или> 1024;TargetAssigneeUserId = Guid.Empty→VALIDATION_FAILED(400); эскалации нет.
Завершается ошибкой валидации.
A4. Недопустимая эскалация дела
- Эскалация арбитражного дела не из
UnderInvestigation(напр. изRegistered/ужеResolved/Escalated) →SUPPORT_ARBITRAGE_STATUS_TRANSITION(409, INV-ARB-1).ResolveиEscalateвзаимоисключающи — второй терминальный переход отклоняется.
Завершается ошибкой; показать допустимые действия.
A5. Гонка с SLA-worker
- Супервизор эскалирует одновременно с фоновым 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 | Условие | Поведение клиента |
|---|---|---|---|
UNAUTHENTICATED | 401 | нет/невалидный JWT | Отправить на вход |
FORBIDDEN | 403 | нет support.tickets.escalate / support.arbitrage.decide | Скрыть эскалацию |
VALIDATION_FAILED | 400 | Reason пустой/>1024; TargetAssigneeUserId = Guid.Empty | Подсветить поля |
SUPPORT_TICKET_NOT_FOUND | 404 | тикет не найден/вне scope | Обновить список |
SUPPORT_TICKET_INVALID_STATE | 409 | Status ∉ {New, InWork} | Показать «нельзя эскалировать» |
SUPPORT_TICKET_ALREADY_ESCALATED | 409 | EscalatedToAdminAt уже задан | Показать «уже эскалировано» |
SUPPORT_ARBITRAGE_STATUS_TRANSITION | 409 | эскалация дела не из UnderInvestigation (INV-ARB-1) | Показать допустимые действия |
SUPPORT_TICKET_CONCURRENCY | 409 | гонка с SLA-worker (оптимистичная блокировка) | Перечитать и повторить |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
TicketEscalatedToAdmin / TicketStatusChanged | Ticket.EscalateToAdmin | таймлайн, сборщик TicketStatusChangedEvent |
ArbitrageResolved | ArbitrageCase.Escalate (A1) | таймлайн, сборщик ArbitrageResolvedEvent |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
TicketStatusChangedEvent | pin.support.ticket-status-changed.v1 | эскалация тикета (ChangeReason = Escalated, шаг 5) — контракт | notifications (заявитель + админ-очередь) |
ArbitrageResolvedEvent | pin.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 мин |
| AC7 | Reason пуст/>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 не ограничивает). Флаг эскалации иммутабелен (аудит передачи).
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-SUP-003. Арбитраж конфликта через аудит
- Дашборды (1): Панель Наблюдаемости: support-overview (эксплуатация)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API: escalate-ticket, escalate-arbitrage-case.
- Domain: ticket.md (
EscalatedToAdminAt, INV-TKT-7), arbitrage-case.md (INV-ARB-3,ArbitrageOutcome.EscalatedToAdmin). - События: ticket-status-changed.md, arbitrage-resolved.md.
- Смежный сценарий: UC-SUP-003.
- Границы: приём эскалации администратором платформы — ADR-0053; tenancy — ADR-0052; контекст — README.