GET /api/public/support/tickets — Мои обращения
Постраничный список обращений (тикетов) текущего заявителя — экран «Мои обращения» в ЛК пользователя
(матрица 4.4/4.5: пользователь видит свои тикеты, статус и активность переписки). Заявитель видит только
собственные тикеты; счётчик сообщений отражает видимый тред — внутренние заметки операторов
(Visibility = Internal) в него не входят.
Назначение
Пользователь открывает «Мои обращения» и получает страницу своих тикетов с фильтрами по статусу и категории,
отсортированную по активности переписки (свежие сверху). Owning-контекст — support; дом полей —
../../domain/ticket.md, тред — ../../domain/ticket-message.md.
Feature group — tickets.
| Поле | Значение |
|---|
| Сервис/контекст | support |
| API area | public |
| Feature group | tickets |
| Статус | draft |
| Документ | docs/06-services/support/api/public/list-my-tickets.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (любой авторизованный пользователь-заявитель; JWT) |
| Auth policy / Permissions | authenticated user; отдельных support.* permissions нет (self-scope заявителя) |
| Scope (RBAC) | self: только собственные обращения |
| Record-level | RequesterUserId == actor.UserId — обязательный предикат выборки (задаётся из claim sub, не из query) |
| Tenant isolation | заявитель — вне PIN-tenant; owned-тикеты несут фиксированный TenantId = PIN CompanyId (ADR-0052). Actor-tenant-интерсептор не применяется (bypass для public self-scope); изоляция обеспечивается record-level предикатом RequesterUserId |
| Audit | none (чтение собственных данных); PII заявителя в проекцию не входит |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/public/support/tickets |
| Success status | 200 |
| Idempotency header | не применяется (безопасный read) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
RequesterUserId | identity claims (sub) | Guid | Да | Required; не принимается из query; отсутствие → 401 | record-level фильтр |
Status | query | TicketStatus? | Нет | enum-строка New/InWork/Resolved/Closed; unknown → VALIDATION_FAILED | — |
Category | query | TicketCategory? | Нет | enum-строка Account/Deal/Booking/Payment/Content/Technical/Complaint/Other | — |
Sort | query | MyTicketsSort? | Нет (default LastMessageAtDesc) | enum-строка (см. ниже) | — |
Page | query | int | Нет (default 1) | >= 1 | — |
PageSize | query | int | Нет (default 20) | 1..100 | — |
Body отсутствует.
Enum MyTicketsSort
| Значение | Правило сортировки |
|---|
LastMessageAtDesc | LastMessageAt DESC NULLS LAST, tie-break CreatedAt DESC (default — свежая переписка сверху). |
CreatedAtDesc | CreatedAt DESC (сначала недавно созданные обращения). |
Модель ответа: постраничный список TicketListItemModel
Постраничная выдача: Items (список TicketListItemModel), Page, PageSize, TotalCount. Дом полей
тикета — ../../domain/ticket.md.
TicketListItemModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Number | long | Да | Ticket.Number | — | Человекочитаемый номер (SUP-NNN); ключ перехода в карточку get-my-ticket. |
Subject | string | Да | Ticket.Subject | — | Тема обращения. |
Status | TicketStatus (enum-строка) | Да | Ticket.Status | — | Статус жизненного цикла. |
Priority | TicketPriority (enum-строка) | Да | Ticket.Priority | — | Приоритет (Low/Normal/High/Urgent). |
Category | TicketCategory (enum-строка) | Да | Ticket.Category | — | Тематика обращения. |
LastMessageAt | DateTimeOffset? | Нет | Ticket.LastMessageAt | — | Момент последнего сообщения; null — сообщений ещё нет. |
MessagesCount | int | Да | computed (подзапрос, шаг 3) | — | Сообщений в видимом треде: Public и не удалённые; Internal-заметки оператора исключены (≠ Ticket.MessagesCount). |
Id наружу не отдаётся (внутренний ключ агрегата); детальная карточка запрашивается по Number
(get-my-ticket). PII заявителя (RequesterName/RequesterPhone/RequesterEmail) в проекцию не включается —
владелец совпадает с актором.
Алгоритм
- Контекст и доступ. Операцию выполняет авторизованный заявитель (actor
User, RequesterUserId —
только из claim sub, не из query). Специальный permission не требуется; изоляция — record-level
(только свои обращения; заявитель вне PIN-tenant).
- Проверка входа.
Page >= 1, PageSize ∈ 1..100; Status/Category/Sort — валидные enum-строки
(unknown → VALIDATION_FAILED). Ссылочных id во входе нет.
- Что читаем. Страница обращений заявителя: обязательный предикат
RequesterUserId == actor и не
удалённые, опциональные фильтры по Status и Category, сортировка по Sort (LastMessageAtDesc по
умолчанию), страница Page/PageSize, TotalCount — по тому же предикату. MessagesCount считается по
видимому треду (Visibility == Public, не удалённые; Internal-заметки исключены, INV-MSG-1/2) —
отдельным подсчётом, переписка не материализуется. Ведущий индекс — ix_tickets_requester_user_id;
Status/Category — остаточные предикаты; сортировка LastMessageAtDesc опирается на
ix_tickets_last_message_at; счётчик public-треда — на partial-индекс ix_ticket_messages_public.
- Маппинг. Поле-в-поле по
TicketListItemModel: Number/Subject/Status/Priority/Category/
LastMessageAt — из обращения; MessagesCount — public-count из шага 3; enum-поля сериализуются строкой.
- Результат. Постраничный список
TicketListItemModel → HTTP 200. PII заявителя в проекцию не
входит. Свежесть: read-your-writes из основной БД (реплик нет, ADR-0056).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none |
| External calls | none (display-данные заявителя не нужны — владелец = актор) |
| Cache / projections / search | none (btree/partial PostgreSQL, ADR-0056) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | Page/PageSize вне диапазона или неизвестное значение enum Status/Category/Sort | «Некорректные параметры запроса» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT заявителя | — | retry после refresh |
Acceptance criteria
- AC-1 (happy path). Given авторизованный заявитель со своими обращениями; When
GET /api/public/support/tickets; Then 200: страница только его обращений, отсортированная
LastMessageAtDesc (свежая переписка сверху), MessagesCount считается по видимому треду (Public,
не удалённые; Internal-заметки исключены).
- AC-2 (фильтры). Given заданы
Status/Category; When запрос; Then выдача сужена этими
предикатами поверх обязательного RequesterUserId == actor.
- AC-3 (сортировка по созданию). Given
Sort = CreatedAtDesc; When запрос; Then сортировка
по CreatedAt DESC.
- AC-4 (record-level). Given заявитель пытается передать чужой
RequesterUserId в query; When
запрос; Then параметр игнорируется — фильтр берётся только из claim sub; чужие обращения не видны.
- AC-5 (валидация). Given
Page < 1/PageSize ∉ 1..100 или неизвестное значение enum
Status/Category/Sort; When запрос; Then VALIDATION_FAILED (400).
- AC-6 (не аутентифицирован). Given нет/просрочен JWT; When запрос; Then
UNAUTHORIZED
(401).
- AC-7 (пустой список). Given у заявителя нет обращений (под фильтром); When запрос; Then
200 с Items = [], TotalCount = 0.
Совместимость и наблюдаемость
- Новые optional поля ответа — additive; удаление/переименование/смена типа — breaking (migration + rollout).
Unknown-значение enum
Status/Category/Sort в query → VALIDATION_FAILED.
- Logs:
RequesterUserId, применённые Status/Category/Sort, Page/PageSize, TotalCount
(Subject/PII не логируются); metrics: support_api_request_duration_seconds{route}; traces: OTel span
list-my-tickets; dashboard pin-support.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы
- Сущности: ../../domain/ticket.md (
Number/Status/Priority/Category/
LastMessageAt, индексы ix_tickets_requester_user_id/ix_tickets_last_message_at),
../../domain/ticket-message.md (Visibility Public/Internal,
ix_ticket_messages_public).
- Границы public-области и модель доступа: ../../README.md; ADR-0052 (tenancy),
ADR-0056 (simplicity-first).
- Соседние операции:
get-my-ticket, create-ticket, add-ticket-message (public-область, будущие пути
по конвенции).