GET /api/support/arbitrage-cases — Очередь арбитражных дел (фильтры)
Назначение
Дать наблюдателю/оператору/супервизору очередь конфликтов для разбора (нераспределённые, «мои», по статусу
Registered/UnderInvestigation/Resolved/Escalated/Closed). Точка входа в арбитраж (6.x).
| Поле | Значение |
|---|
| Сервис/контекст | support |
| API area | support (наблюдатель/оператор/супервизор) |
| Feature group | arbitrage |
| Статус | draft |
| Документ | docs/06-services/support/api/support/list-arbitrage-cases.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Служба заботы (Наблюдатель / Оператор / Супервизор) |
| Auth policy / Permissions | support.arbitrage.view (наблюдатель+) — каталог ../../../identity/domain/permission.md |
| Scope (RBAC) | platform-scope (ADR-0052) — не агентство/застройщик-tenant |
| Record-level | не ограничивает: support.arbitrage.view видит все дела платформы; AssigneeUserId (вкл. me) — фильтр, не ограничение |
| Tenant isolation | TenantId = PIN CompanyId (единый платформенный tenant) — не агентство-фильтр |
| Audit | read: логируется actor + набор фильтров + число результатов; ФИО/значение поиска в логи не пишутся |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/support/arbitrage-cases |
| Success status | 200 |
| Idempotency header | не применяется (безопасный GET) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ActorUserId | identity claims | Guid | Да | Required; permission support.arbitrage.view @ platform-scope | лог platform-доступа |
Status | query | ArbitrageStatus? | Нет | enum строкой (Registered…Closed); unknown → ValidationError | — |
Type | query | ArbitrageType? | Нет | enum строкой; unknown → ValidationError | — |
Priority | query | TicketPriority? | Нет | enum строкой; unknown → ValidationError | — |
Assignee | query (assigneeUserId) | AssigneeFilter? | Нет | me → ActorUserId; unassigned → IS NULL; иначе Guid != Empty | — |
ComplainantUserId | query | Guid? | Нет | != Guid.Empty; логический фильтр по заявителю | — |
RelatedDealId | query | Guid? | Нет | != Guid.Empty; логический фильтр (partial index) | — |
Q | query | string? | Нет | trim, 1..128; пустой → игнор; case-insensitive ILIKE по Number::text/Title (опц. pg_trgm) | значение поиска в логи не пишется |
Created | query (createdFrom/createdTo) | открытый диапазон DateTimeOffset | Нет | открытый по любой границе диапазон CreatedAt; при обеих границах — From <= To | — |
Sort | query | поле + направление | Нет | enum-поле + направление; default CreatedAt desc | — |
Paging | query | пагинация | Нет | 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 | Описание |
|---|
Id | Guid | Да | Case.Id | — | Переход в карточку дела. |
Number | long | Да | Case.Number | — | Человекочитаемый номер (ARB-NNN). |
Title | string | Да | Case.Title | — | Заголовок дела. |
Type | ArbitrageType (string) | Да | Case.Type | — | Тип конфликта/жалобы. |
Status | ArbitrageStatus (string) | Да | Case.Status | — | Статус жизненного цикла. |
Priority | TicketPriority (string) | Да | Case.Priority | — | Приоритет разбора. |
ComplainantUserId | Guid | Да | Case.ComplainantUserId | — | Заявитель (display-имя резолвит фронт). |
ComplainantName | string? | Нет | Case.ComplainantName (FullName-реплика) | ФИО-маска в логах | ФИО заявителя (PII); null до backfill. |
AssigneeUserId | Guid? | Нет | Case.AssigneeUserId | — | Назначенный арбитр; null — не взято. |
RelatedDealId | Guid? | Нет | Case.RelatedDealId | — | Связанная сделка (crm, логический id). |
RespondentsCount | int | Да | Case.Respondents.Items.Count | — | Число ответчиков. |
ResolvedAt | DateTimeOffset? | Нет | Case.ResolvedAt | — | Момент решения (если вынесено). |
CreatedAt | DateTimeOffset | Да | Case.CreatedAt | — | Регистрация; default-ключ сортировки. |
Телефон заявителя, Description, заметки и решение в список не проецируются (детали — get-arbitrage-case.md).
Алгоритм
- Контекст и доступ. Операцию выполняет наблюдатель/оператор/супервизор Службы заботы (JWT) в
platform-scope; требуется право
support.arbitrage.view (иначе → SUPPORT_ARBITRAGE_FORBIDDEN, 403).
Tenant — PIN. Разрешить Assignee: Mode = Me → AssigneeUserId = ActorUserId.
- Проверка.
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 видит все дела).
- Выборка. Собрать предикат из фильтров (все — AND):
Status/Type/Priority/ComplainantUserId/
RelatedDealId — равенства; Assignee: Me/User → AssigneeUserId == 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.
- Результат. Страница
ArbitrageCaseListItemModel (Items, Page, PageSize) + TotalCount (по тому
же предикату) → HTTP 200. Пустая выдача → Items = [], TotalCount = 0 (не 404).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none (read-only) |
| External calls | none (данные — своя схема support; ФИО заявителя — реплика из identity; display-имена арбитров резолвит фронт) |
| Cache / projections / search | поиск по Number/Title — PostgreSQL ILIKE (опц. pg_trgm); отдельного поискового индекса для арбитража нет — низкий объём (ADR-0056; поисковые поверхности support — тикеты и база знаний, ADR-0059) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | Page/PageSize вне диапазона; unknown Status/Type/Priority; Guid.Empty в фильтре; Q длина; CreatedFrom > CreatedTo; Assignee.Mode = User без UserId | «Проверьте параметры запроса» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
SUPPORT_ARBITRAGE_FORBIDDEN | 403 | нет support.arbitrage.view в platform-scope | «Недостаточно прав для просмотра арбитража» | no retry |
INTERNAL_ERROR | 500 | инфраструктурный сбой | generic | retry (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.view → 403 SUPPORT_ARBITRAGE_FORBIDDEN; нет JWT → 401;
инфраструктурный сбой → 500 INTERNAL_ERROR (retry).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы