UC-SUP-003. Арбитраж конфликта через аудит
Назначение
Описывает разбор конфликта/жалобы (приоритет агента, конфликт бронирований, спор о комиссии, жалоба на
контент) арбитром Службы заботы: регистрация дела, расследование по ссылкам на аудит/чаты/историю чужих
сервисов (support не владеет их данными — только ссылается, INV-ARB), вынесение иммутабельного решения и
информирование сторон событием. Связан с контекстом support; агрегат —
ArbitrageCaseEntity (+ append-only ArbitrageNoteEntity).
Покрывает матрицу 6.1 (регистрация), 6.2 (расследование), 6.3 (решение), 6.4 (информирование).
Metadata
| Поле | Значение |
|---|---|
| ID | UC-SUP-003 |
| Контекст | support (см. docs/06-services/support/) |
| Тип | основной (+ альтернативные потоки A1–A3) |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | СлужбаЗаботы — Оператор/Арбитр (регистрация, расследование) + Супервизор (решение) |
| Permissions / scope | Оператор — support.arbitrage.manage (register/investigate/add-note); Супервизор — support.arbitrage.decide (resolve/escalate); чтение — support.arbitrage.view; кросс-сервисный просмотр аудита — *.view (2.x) — все platform-scope (ADR-0052) |
| Tenant | TenantId = PIN CompanyId (единый tenant платформы) |
| Record-level | по AssigneeUserId (арбитр ведёт назначенное дело); супервизор не ограничен |
Предусловие
- Есть конфликт между сторонами платформы; заявитель (
ComplainantUserId) и ответчики (Respondents) известны. - Арбитр/супервизор аутентифицированы, несут соответствующие
support.arbitrage.*permissions. - Для расследования доступны аудит-таймлайны чужих сервисов (
crm/chessboard/rewards/messenger) — читаются их API в platform-scope. ComplainantUserIdне входит в списокRespondents(заявитель не может быть ответчиком по своей жалобе);Respondents≤ 50 без дублей.
Гарантируется после сценария: создано дело ARB-NNN (Registered → UnderInvestigation → Resolved/
Escalated → Closed); добавлены append-only заметки с типизированными AuditRefs; при завершении —
иммутабельный Decision; опубликован arbitrage-resolved.v1; стороны информированы в окне < 1 мин.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button / API call |
| Триггерящий элемент | Кабинет арбитра: реестр дел и карточка дела; опц. кнопка «В арбитраж» из карточки тикета |
Основной Поток
- Арбитр регистрирует дело →
POST /api/support/arbitrage-cases(register-arbitrage-case):Type,Title,Description,ComplainantUserId,Respondents, опц.SourceTicketId/RelatedDealId/RelatedFixationId. - Регистрируется дело (INV-ARB-1):
Status = Registered,Number = ARB-NNN, подпись ФИО заявителя из identity-lookup; поднимается доменное событиеArbitrageRegistered; дело фиксируется →201. - Арбитр берёт дело в работу (назначается
AssigneeUserId) → переходRegistered → UnderInvestigation(INV-ARB-1). - Расследование по аудиту: арбитр читает аудит/чаты/историю чужих сервисов (2.x) и добавляет заметки со ссылками →
POST /api/support/arbitrage-cases/{id}/notes(add-arbitrage-note): append-only заметка сAuditRefs(INV-ARB-5: только вRegistered/UnderInvestigation). AuditRefs— типизированный jsonbArbitrageAuditRefs(Service/EntityType/EntityId/ChangeAt?/Note?) ссылается на записи чужих сервисов; support данные не копирует, только фиксирует ссылку.- Супервизор выносит решение →
POST /api/support/arbitrage-cases/{id}/resolve(resolve-arbitrage-case):ArbitrageDecision(Outcome+Rationale+DecidedByUserId). - Вынесение решения (INV-ARB-2): требуется
AssigneeUserId != nullи заполненныйDecision;Status = Resolved,ResolvedAt = now,Decisionиммутабелен; событиеArbitrageResolvedв outbox →200. notifications(консюмерarbitrage-resolved) информирует стороны (6.4); дело закрывается (Resolved → Closed).
Альтернативные Потоки
A1. Эскалация администратору вместо решения
- Супервизор не может решить дело силами поддержки →
POST /api/support/arbitrage-cases/{id}/escalate(escalate-arbitrage-case). - Эскалация (INV-ARB-3):
Status = Escalated,Decision.Outcome = EscalatedToAdmin,EscalatedToAdminAt/By; событиеArbitrageResolved(Escalated = true). Приём — администратор платформы (ADR-0053). См. UC-SUP-005.
Завершается статусом Escalated.
A2. Дело порождено из обращения
- Из карточки тикета (
Category = Complaint) арбитр регистрирует дело сSourceTicketId;Ticket.ArbitrageCaseIdсвязывается с делом. - Далее — основной поток с шага 3.
Возвращается к основному потоку на шаг 3.
A3. Ручные последствия решения — в owning-сервисах
- Решение требует смены статуса сделки/переназначения/корректировки комиссии — выполняется в
crm/rewardsчерез platform-scope permissions (UC-SUP-004). - Факт фиксируется заметкой
AuditRefs(ссылка на запись owning-сервиса); support чужие таблицы не пишет.
Возвращается к основному потоку на шаг 8.
A4. Заметка/правка после решения
- Арбитр добавляет заметку в деле
Status ∈ {Resolved, Escalated, Closed}→SUPPORT_ARBITRAGE_NOTE_FORBIDDEN(409, INV-ARB-5): после решения заметки только для чтения. - Попытка изменить
Respondents/Related*после решения →SUPPORT_ARBITRAGE_LOCKED_AFTER_DECISION(INV-ARB-4): привязки иммутабельны.
Завершается ошибкой; карточка дела в режиме чтения.
A5. Решение без арбитра/решения или недопустимый переход
ResolveприAssigneeUserId == nullили пустомDecision(Outcome/Rationale/DecidedByUserId) →SUPPORT_ARBITRAGE_DECISION_REQUIRED(409, INV-ARB-2).- Запрос перехода вне цепочки
Registered → UnderInvestigation → (Resolved/Escalated) → Closed(напр.Registered → Resolvedминуя расследование, повторный терминальный переход) →SUPPORT_ARBITRAGE_STATUS_TRANSITION(409, INV-ARB-1).
Завершается ошибкой; показать допустимые действия.
A6. Невалидные стороны/поля дела
Title/Description/Bodyпустые или сверх длины;Respondents> 50 либо с дублями либо содержитComplainantUserId→VALIDATION_FAILED(400); дело/заметка не создаются.
Завершается ошибкой валидации.
A7. Недоступен аудит-источник во время расследования
- Арбитр читает аудит/чат/историю
crm/chessboard/rewards/messenger, а сосед недоступен → показывается частичная картина (id-ссылка без обогащения); расследование, добавление заметок и вынесение решения не блокируются (support ссылается, а не владеет данными, INV-ARB). - Арбитр фиксирует известную
AuditRef(Service/EntityType/EntityId), обогащение дорисуется при доступности соседа.
Возвращается к основному потоку на шаг 6.
A8. Гонка арбитр ↔ супервизор
- Арбитр добавляет заметку одновременно с вынесением решения супервизором: оптимистичная блокировка пропускает первого; проигравший →
CONCURRENCY_CONFLICT(409) и перечитывает дело. После фиксацииResolved/Escalatedзаметки закрыты (INV-ARB-5).
Возвращается к основному потоку после перечитывания.
Постусловие
- Создан
ArbitrageCaseEntity(Registered → UnderInvestigation → Resolved/Escalated → Closed);Number = ARB-NNN. - Добавлены
ArbitrageNoteEntity(append-only) с типизированнымиAuditRefsна аудит чужих сервисов. - При
Resolve/Escalate— иммутабельныйDecision; в outbox —ArbitrageResolvedEvent; стороны уведомлены.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
FORBIDDEN | 403 | нет support.arbitrage.manage/.decide | Скрыть действия |
VALIDATION_FAILED | 400 | пустой Title/Description/Body; Respondents > 50 или с дублями | Подсветить поля |
SUPPORT_ARBITRAGE_NOT_FOUND | 404 | дело не найдено/скрыто | Обновить реестр |
SUPPORT_ARBITRAGE_STATUS_TRANSITION | 409 | недопустимый переход (INV-ARB-1) | Показать допустимые действия |
SUPPORT_ARBITRAGE_NOTE_FORBIDDEN | 409 | заметка после решения (INV-ARB-5) | Заметки только для чтения |
SUPPORT_ARBITRAGE_DECISION_REQUIRED | 409 | Resolve без AssigneeUserId/Decision (INV-ARB-2) | Назначить арбитра/заполнить решение |
SUPPORT_ARBITRAGE_ESCALATE_FORBIDDEN | 403 | эскалация без support.arbitrage.decide (INV-ARB-3) | Скрыть эскалацию |
CONCURRENCY_CONFLICT | 409 | гонка арбитр ↔ супервизор (оптимистичная блокировка) | Перечитать и повторить |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
ArbitrageRegistered | ArbitrageCase.Register | счётчики реестра, таймлайн |
ArbitrageNoteAdded | ArbitrageCase.AddNote | таймлайн расследования (интеграционного события нет) |
ArbitrageResolved | ArbitrageCase.Resolve/Escalate | таймлайн, сборщик ArbitrageResolvedEvent |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
ArbitrageResolvedEvent | pin.support.arbitrage-resolved.v1 | Resolve и Escalate (шаг 7 / A1) — контракт | notifications (информирование сторон, 6.4) |
Кросс-сервисные ожидания и согласованность
Ожидает от соседей (вход)
identity(синхронно/асинхронно). JWT +support.arbitrage.*permissions; кросс-сервисный просмотр аудита —*.view(2.x). Display-подпись заявителя — из репликиidentity(ComplainantName, version-guard поIdentityComplainantVersion); реплика не пришла → карточка показываетComplainantUserIdбез ФИО, расследование не блокируется.- Аудит-таймлайны соседей — только чтение по API в platform-scope.
crm(сделки/фиксации),chessboard(объекты),rewards(начисления/комиссии),messenger(чаты). support данные не копирует, только фиксирует типизированную ссылкуArbitrageAuditRef(Service/EntityType/EntityId/ChangeAt?/Note?). Недоступность соседа → частичная картина, дело работоспособно (A7).
Предоставляет соседям (выход)
pin.support.arbitrage-resolved.v1(ключ =CaseId,ContractVersion = 1, 3 партиции): терминальный исход дела — несётType/Outcome/Escalated/ComplainantUserId/RespondentUserIds/RelatedDealId?/DecidedByUserId/DecidedAt(безRationale/заметок — Internal). Публикуется наResolveиEscalateатомарно с переходом, ровно один раз (Resolve и Escalate взаимоисключающи, INV-ARB-1). Потребительnotificationsинформирует стороны; закрытие (→ Closed) события не порождает.
Окно согласованности и промежуточное состояние
- Вынесение решения/эскалация → информирование сторон: асинхронно через outbox, < 1 мин. В промежутке стороны
видят прежнее состояние дела; арбитр/супервизор — уже
Resolved/Escalated. - Ручные последствия решения (A3, UC-SUP-004) выполняются в owning-сервисах и наблюдаются через их события
(
crm/rewards), не черезarbitrage-resolved.
Идемпотентность и конкурентность
- Повторный терминальный переход отклоняется
SUPPORT_ARBITRAGE_STATUS_TRANSITION(INV-ARB-1) — событие информирования уходит ровно один раз. - Гонка арбитр ↔ супервизор разводится оптимистичной блокировкой (A8): проигравший →
CONCURRENCY_CONFLICT. Decisionфиксируется иммутабельно; дублиarbitrage-resolved.v1уnotificationsснимаются дедупликацией поMessageId(at-least-once).
Acceptance Criteria
| # | Given (дано) | When (когда) | Then (тогда) |
|---|---|---|---|
| AC1 | Известны заявитель и ответчики; арбитр с support.arbitrage.manage | арбитр регистрирует дело | создан ArbitrageCaseEntity Registered с ARB-NNN; ФИО заявителя — реплика identity (или id при отставании) |
| AC2 | Дело Resolved/Escalated/Closed | арбитр добавляет заметку | SUPPORT_ARBITRAGE_NOTE_FORBIDDEN (INV-ARB-5); в Registered/UnderInvestigation — заметка добавляется |
| AC3 | Заметка со ссылками на чужой аудит | заметка сохранена | AuditRefs — типизированный jsonb (Service/EntityType/EntityId), без копий чужих данных |
| AC4 | Дело UnderInvestigation | супервизор выносит Resolve без Decision/assignee | SUPPORT_ARBITRAGE_DECISION_REQUIRED (INV-ARB-2); при заполненном Decision — Resolved, Decision иммутабелен (INV-ARB-4) |
| AC5 | Дело завершается (Resolve/Escalate) | переход зафиксирован | arbitrage-resolved.v1 (ключ CaseId, v1) опубликован; notifications информирует стороны < 1 мин |
| AC6 | Тикет Category = Complaint | арбитр регистрирует дело из тикета | Ticket.ArbitrageCaseId ↔ SourceTicketId связаны |
| AC7 | Аудит-источник (crm/chessboard/rewards/messenger) недоступен | арбитр ведёт расследование | картина частичная (id без обогащения), расследование/решение не блокируются |
Наблюдаемость и НФТ
- Латентность: регистрация/заметка/решение p95 < 500 мс; поиск дел по
Number/Title— PostgreSQLILIKE(низкий объём, без Elasticsearch). Объём арбитража мал (конфликты/жалобы). - Логи (без PII):
arbitrage-registered(caseId, number, type),arbitrage-note-added(caseId, authorUserId, refsCount),arbitrage-resolved(caseId, outcome, escalated);Rationale/тексты заметок и ФИО сторон — не логируются. - Метрики: число дел по типам/исходам, время
Registered → Resolved, доля эскалаций, конфликты арбитр ↔ супервизор, недоступность аудит-соседей при расследовании. - Доступ: регистрация/расследование/заметки —
support.arbitrage.manage; решение/эскалация —support.arbitrage.decide(супервизор); чтение —support.arbitrage.view; всё platform-scope, record-level поAssigneeUserId.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: ArbitrageCaseEntity (+ ArbitrageNoteEntity)
- Use Cases (2): UC-SUP-004. Ручное вмешательство через permissions, UC-SUP-005. Эскалация администратору
- Дашборды (1): Панель Наблюдаемости: support-overview (эксплуатация)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API (будущие пути по конвенции): register-arbitrage-case, add-arbitrage-note, resolve-arbitrage-case, escalate-arbitrage-case, get-arbitrage-case.
- Domain: arbitrage-case.md (INV-ARB-1..5,
ArbitrageDecision,ArbitrageAuditRefs), ticket.md (ArbitrageCaseId). - Событие: arbitrage-resolved.md.
- Смежные сценарии: UC-SUP-004, UC-SUP-005.
- Контекст: README; tenancy — ADR-0052; admin — ADR-0053.