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

GET /api/support/arbitrage-cases — Очередь арбитражных дел (фильтры)

Назначение

Дать наблюдателю/оператору/супервизору очередь конфликтов для разбора (нераспределённые, «мои», по статусу Registered/UnderInvestigation/Resolved/Escalated/Closed). Точка входа в арбитраж (6.x).

Metadata

ПолеЗначение
Сервис/контекстsupport
API areasupport (наблюдатель/оператор/супервизор)
Feature grouparbitrage
Статусdraft
Документdocs/06-services/support/api/support/list-arbitrage-cases.md

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

ПроверкаЗначение
Actor typeСлужба заботы (Наблюдатель / Оператор / Супервизор)
Auth policy / Permissionssupport.arbitrage.view (наблюдатель+) — каталог ../../../identity/domain/permission.md
Scope (RBAC)platform-scope (ADR-0052) — не агентство/застройщик-tenant
Record-levelне ограничивает: support.arbitrage.view видит все дела платформы; AssigneeUserId (вкл. me) — фильтр, не ограничение
Tenant isolationTenantId = PIN CompanyId (единый платформенный tenant) — не агентство-фильтр
Auditread: логируется actor + набор фильтров + число результатов; ФИО/значение поиска в логи не пишутся

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/support/arbitrage-cases
Success status200
Idempotency headerне применяется (безопасный GET)
CorrelationOpenTelemetry trace (traceparent)

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаRequired; permission support.arbitrage.view @ platform-scopeлог platform-доступа
StatusqueryArbitrageStatus?Нетenum строкой (RegisteredClosed); unknown → ValidationError
TypequeryArbitrageType?Нетenum строкой; unknown → ValidationError
PriorityqueryTicketPriority?Нетenum строкой; unknown → ValidationError
Assigneequery (assigneeUserId)AssigneeFilter?НетmeActorUserId; unassignedIS NULL; иначе Guid != Empty
ComplainantUserIdqueryGuid?Нет!= Guid.Empty; логический фильтр по заявителю
RelatedDealIdqueryGuid?Нет!= Guid.Empty; логический фильтр (partial index)
Qquerystring?Нетtrim, 1..128; пустой → игнор; case-insensitive ILIKE по Number::text/Title (опц. pg_trgm)значение поиска в логи не пишется
Createdquery (createdFrom/createdTo)открытый диапазон DateTimeOffsetНетоткрытый по любой границе диапазон CreatedAt; при обеих границах — From <= To
Sortqueryполе + направлениеНетenum-поле + направление; default CreatedAt desc
PagingqueryпагинацияНетPage >= 1 (default 1), PageSize 1..100 (default 20)

Фильтры комбинируются по AND (логические предикаты: нет совпадений → пустая страница, не 404). Фильтр по назначению Assignee (Mode = Me/Unassigned/User + UserId?) — тот же контракт, что в list-tickets.md. Поля сортировки (ArbitrageSortField): CreatedAt (default), Priority, Status, ResolvedAt.

Модель ответа — постраничный список ArbitrageCaseListItemModel

Постраничный контейнер: Items, Page, PageSize, TotalCount. Элемент — проекция arbitrage-case.md.

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаCase.IdПереход в карточку дела.
NumberlongДаCase.NumberЧеловекочитаемый номер (ARB-NNN).
TitlestringДаCase.TitleЗаголовок дела.
TypeArbitrageType (string)ДаCase.TypeТип конфликта/жалобы.
StatusArbitrageStatus (string)ДаCase.StatusСтатус жизненного цикла.
PriorityTicketPriority (string)ДаCase.PriorityПриоритет разбора.
ComplainantUserIdGuidДаCase.ComplainantUserIdЗаявитель (display-имя резолвит фронт).
ComplainantNamestring?НетCase.ComplainantName (FullName-реплика)ФИО-маска в логахФИО заявителя (PII); null до backfill.
AssigneeUserIdGuid?НетCase.AssigneeUserIdНазначенный арбитр; null — не взято.
RelatedDealIdGuid?НетCase.RelatedDealIdСвязанная сделка (crm, логический id).
RespondentsCountintДаCase.Respondents.Items.CountЧисло ответчиков.
ResolvedAtDateTimeOffset?НетCase.ResolvedAtМомент решения (если вынесено).
CreatedAtDateTimeOffsetДаCase.CreatedAtРегистрация; default-ключ сортировки.

Телефон заявителя, Description, заметки и решение в список не проецируются (детали — get-arbitrage-case.md).

Алгоритм

  1. Контекст и доступ. Операцию выполняет наблюдатель/оператор/супервизор Службы заботы (JWT) в platform-scope; требуется право support.arbitrage.view (иначе → SUPPORT_ARBITRAGE_FORBIDDEN, 403). Tenant — PIN. Разрешить Assignee: Mode = MeAssigneeUserId = ActorUserId.
  2. Проверка. Paging.PageSize ∈ [1..100], Page >= 1; Status/Type/Priority — известные enum (unknown → ValidationError); ComplainantUserId/RelatedDealId/Assignee.UserId!= Guid.Empty, если заданы; Assignee.Mode = User требует UserId; Q — trim и 1..128; Created.From <= Created.To, если обе границы заданы. Существование id не проверяется (логические фильтры → пустая страница, не 404). Record-level не добавляется (support.arbitrage.view видит все дела).
  3. Выборка. Собрать предикат из фильтров (все — AND): Status/Type/Priority/ComplainantUserId/ RelatedDealId — равенства; Assignee: Me/UserAssigneeUserId == X, Unassigned== null; Created → диапазон CreatedAt (обе границы опциональны); Q → case-insensitive ILIKE по Number::text/Title (опц. pg_trgm; объём дел мал — отдельный поисковый индекс не заводится, ADR-0056). Tenant (PIN) и не-удалённые записи (deleted_at IS NULL) добавляются автоматически. Сортировка = Sort (default CreatedAt desc), tiebreak Id desc. ComplainantName — из FullName-реплики (консюмер identity). Индексы: ix_arbitrage_status, ix_arbitrage_assignee, ix_arbitrage_complainant_user_id, ix_arbitrage_related_deal_id, ix_arbitrage_created_at.
  4. Результат. Страница ArbitrageCaseListItemModel (Items, Page, PageSize) + TotalCount (по тому же предикату) → HTTP 200. Пустая выдача → Items = [], TotalCount = 0 (не 404).

Диаграмма

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

ТипДетали
Integration eventsnone (read-only)
External callsnone (данные — своя схема support; ФИО заявителя — реплика из identity; display-имена арбитров резолвит фронт)
Cache / projections / searchпоиск по Number/Title — PostgreSQL ILIKE (опц. pg_trgm); отдельного поискового индекса для арбитража нет — низкий объём (ADR-0056; поисковые поверхности support — тикеты и база знаний, ADR-0059)
Notificationsnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Page/PageSize вне диапазона; unknown Status/Type/Priority; Guid.Empty в фильтре; Q длина; CreatedFrom > CreatedTo; Assignee.Mode = User без UserId«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
SUPPORT_ARBITRAGE_FORBIDDEN403нет support.arbitrage.view в platform-scope«Недостаточно прав для просмотра арбитража»no retry
INTERNAL_ERROR500инфраструктурный сбойgenericretry (backoff)

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

  • Новые optional фильтры/поля ответа — additive; удаление/переименование/смена типа поля или значения enum ArbitrageStatus/ArbitrageType/TicketPriority — breaking (migration + rollout). Unknown enum во входе — ValidationError; клиент обязан иметь fallback на unknown в ответе.
  • Logs: arbitrage-cases listed (ActorUserId, набор фильтров, hasSearch, число результатов; ФИО/значение поиска — не в логи); metrics: support_arbitrage_list_total{result}, latency p95 < 1 c (SLO списков/поиска); traces — OpenTelemetry span support.list-arbitrage; dashboard Grafana pin-support; runbook — docs/06-services/support/runbooks/ (будущий путь).

Acceptance-критерии

  • Happy path. Given наблюдатель с support.arbitrage.view; When запрашивает без фильтров; Then 200 с первой страницей CreatedAt desc (tiebreak Id desc), Items/Page/PageSize/TotalCount; ФИО заявителя — из реплики (может быть null до backfill).
  • Назначение. Given Assignee = me → только «мои» дела; unassigned → нераспределённые (AssigneeUserId is null); конкретный UserId → дела арбитра.
  • Фильтры (AND). Given заданы Status/Type/Priority/ComplainantUserId/RelatedDealId/Created; When запрашивает; Then выборка по конъюнкции; несовпадение → пустая страница (Items = [], TotalCount = 0), не 404.
  • Поиск. Given Q (1..128); When запрашивает; Then case-insensitive ILIKE по Number/Title.
  • Невалидный вход. Given unknown enum Status/Type/Priority, Guid.Empty в фильтре, Q вне длины, CreatedFrom > CreatedTo, Assignee.Mode = User без UserId, Page/PageSize вне диапазона; When запрашивает; Then 400 VALIDATION_FAILED.
  • Нет прав. Given нет support.arbitrage.view403 SUPPORT_ARBITRAGE_FORBIDDEN; нет JWT → 401; инфраструктурный сбой → 500 INTERNAL_ERROR (retry).

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

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

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