Entity / Aggregate: TicketQueueEntity
Дом полей агрегата «Очередь обращений» контекста support (greenfield). Справочник-конфиг маршрутизации:
входящий тикет попадает в очередь по своей категории, а очередь определяет команду поддержки. Реализует
матрицу 4.4 «централизованная очередь». Все проекции (модели ответа, списки-фильтры тикетов, payload
доменных событий) ссылаются сюда и описывают только дельту.
Назначение
TicketQueueEntity — именованная очередь обращений: контейнер маршрутизации, в который сервис support
раскладывает тикеты для последующего разбора командой. Каждая очередь может «владеть» одной категорией
тикетов (RoutingCategory) — тогда тикеты этой категории автоматически попадают в неё; ровно одна очередь
помечена fallback-ом (IsDefault) для категорий без выделенной очереди (INV-Q-5). Справочник управляется
супервизором поддержки (CRUD) и не является пользовательской сущностью — маршрутизация детерминирована и
конфигурируется, а не вычисляется. Внешним контекстам очередь не публикуется (внутренний справочник).
Классификация
| Поле | Значение |
|---|
| Контекст | support |
| Kind | AggregateRoot (справочник-конфиг, CRUD без сложной бизнес-логики) |
| Source of truth | support |
| Таблица | support.ticket_queues |
| Tenant isolation | TenantId = PIN CompanyId (platform-справочник — очереди централизованные, одни на платформу) |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|
Id | Guid | Да | PK | app / SEED | Идентификатор очереди. |
TenantId | Guid | Да | index ix_ticket_queues_tenant_id; часть unique-индексов | identity (platform) / SEED | Компания ПИН (централизованный справочник). |
Code | string | Да | varchar(64), не пустое; unique ux_queues_code on (TenantId, Code) | app / SEED | Стабильный машинный код (General, Deals, …). Иммутабелен после создания. |
Name | string | Да | varchar(128), не пустое | app / SEED | Отображаемое имя очереди (русское). |
Description | string? | Нет | varchar(1000) | app | Пояснение назначения очереди для супервизора. |
DefaultPriority | TicketPriority | Да | .HasConversion<string>(), varchar(32), default Normal | app / SEED | Приоритет по умолчанию для тикетов, попавших в очередь (enum — ticket.md). |
RoutingCategory | TicketCategory? | Нет | .HasConversion<string>(), varchar(32); unique partial ux_queues_routing_category on (TenantId, RoutingCategory) where routing_category is not null and status = 'Active' | app / SEED | Категория, тикеты которой авто-маршрутизируются в эту очередь. null — очередь не привязана к категории (напр. default). Enum — ticket.md. |
IsDefault | bool | Да | default false; unique partial ux_queues_default on (TenantId) where is_default and status = 'Active' | app / SEED | Fallback-очередь для нераспознанных категорий. Ровно одна активная (INV-Q-2). |
SortOrder | int | Да | >= 0, default 0; index ix_ticket_queues_sort_order | app | Порядок отображения очередей в UI/фильтрах. |
Status | QueueStatus | Да | .HasConversion<string>(), varchar(16), index ix_ticket_queues_status | app | Жизненный цикл очереди (см. enum и переходы). |
CreatedAt | DateTimeOffset | Да | — | app | Создание. |
CreatedByUserId | Guid? | Нет | — | app | Супервизор (null — SEED-миграция). |
UpdatedAt | DateTimeOffset | Да | — | app | Момент последнего изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | — | app | Soft-delete (административно). |
Enum QueueStatus
| Значение | Описание |
|---|
Active | Рабочая очередь: участвует в маршрутизации, доступна для назначения тикетов. |
Archived | Выведена из эксплуатации: не маршрутизирует новые тикеты, освобождает partial-unique слоты (ux_queues_routing_category, ux_queues_default). |
Value objects
Value objects не используются (плоский справочник-конфиг).
Модели JSONB (IDocument, versioned)
JSONB-модели не используются.
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|
TicketEntity | reference (children) | tickets.queue_id | выборка/подсчёт по (TenantId, QueueId, Status) | restrict (очередь с активными тикетами архивируется только после переноса — INV-Q-4) |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|
| INV-Q-1 | В рамках TenantId Code уникален | уникальный индекс ux_queues_code + проверка при создании/обновлении очереди | SUPPORT_QUEUE_CODE_DUPLICATE |
| INV-Q-2 | Ровно одна активная очередь с IsDefault = true; SEED гарантирует наличие, смена default — в одном согласованном изменении (снять со старой + поставить новой) | уникальный partial-индекс ux_queues_default + назначение default-очереди | SUPPORT_QUEUE_DEFAULT_REQUIRED / SUPPORT_QUEUE_DEFAULT_DUPLICATE |
| INV-Q-3 | RoutingCategory уникальна среди активных очередей (детерминированная маршрутизация: категория → не более одной очереди) | уникальный partial-индекс ux_queues_routing_category + проверка при создании/обновлении | SUPPORT_QUEUE_ROUTING_DUPLICATE |
| INV-Q-4 | Архивация запрещена, если в очереди есть активные тикеты (Status ∈ {New, InWork}) или это единственная активная default-очередь; перенос тикетов в другую очередь предшествует архивации | при архивации очереди | SUPPORT_QUEUE_HAS_ACTIVE_TICKETS / SUPPORT_QUEUE_LAST_DEFAULT |
| INV-Q-5 | Маршрутизация при создании тикета: по RoutingCategory категории тикета → соответствующая активная очередь; при отсутствии совпадения → активная default-очередь | при маршрутизации создаваемого тикета | — (детерминировано, default всегда существует по INV-Q-2) |
Предусловия и постусловия переходов
| Переход | Инициатор / условие | Предусловие | Постусловие (гарантируется) |
|---|
Create ([*] → Active) | супервизор (support.queues.manage) | Code уникален в TenantId (INV-Q-1); при заданном RoutingCategory — категория не занята активной очередью (INV-Q-3); при IsDefault = true — прежняя default снимается в том же изменении (INV-Q-2) | очередь Active, участвует в маршрутизации; поднято доменное QueueCreated |
Update (Active → Active) | супервизор | Code иммутабелен; смена RoutingCategory/IsDefault соблюдает INV-Q-2/INV-Q-3 | новые routing/default-слоты заняты атомарно (старые освобождены), маршрутизация будущих тикетов идёт по новой конфигурации |
Archive (Active → Archived) | супервизор | нет активных тикетов Status ∈ {New, InWork} (INV-Q-4, иначе SUPPORT_QUEUE_HAS_ACTIVE_TICKETS); не единственная активная default (иначе SUPPORT_QUEUE_LAST_DEFAULT) | partial-unique слоты (ux_queues_routing_category, ux_queues_default) освобождены; очередь не маршрутизирует новые тикеты; поднято QueueArchived |
Restore (Archived → Active) | супервизор | Code по-прежнему уникален; routing-категория и default-слот свободны (иначе SUPPORT_QUEUE_ROUTING_DUPLICATE / SUPPORT_QUEUE_DEFAULT_DUPLICATE) | очередь снова Active и участвует в маршрутизации |
Конкурентность и маршрутизация под нагрузкой
- Смена default (INV-Q-2). «Снять флаг со старой + поставить новой» выполняется в одном согласованном
изменении; две параллельные попытки назначить разные default-очереди сериализуются partial-unique индексом
ux_queues_default — проигравшая упирается в SUPPORT_QUEUE_DEFAULT_DUPLICATE и повторяет на свежем
состоянии. Промежуточного состояния «нет ни одной default» наблюдатель не видит (атомарность).
- Занятие routing-категории (INV-Q-3). Гонка двух очередей за одну
RoutingCategory разрешается тем же
partial-unique индексом ux_queues_routing_category → SUPPORT_QUEUE_ROUTING_DUPLICATE проигравшему.
- Архивация ↔ маршрутизация нового тикета. Маршрутизация (INV-Q-5) выбирает очередь среди
Active;
архивация переводит очередь в Archived. Если create-ticket уже вычислил QueueId этой очереди, а
архивация фиксируется следом — тикет остаётся в архивируемой очереди, и её архивация отклоняется
SUPPORT_QUEUE_HAS_ACTIVE_TICKETS (INV-Q-4: перед архивацией активных тикетов быть не должно). Супервизор
переносит тикеты в другую активную очередь и повторяет архивацию; проигравшая по версии правка не затирает
чужую (оптимистичная блокировка). Категория, оставшаяся без активной routing-очереди, детерминированно
уходит в default General (INV-Q-5).
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|
QueueCreated | доменное | Создание очереди | Id, TenantId, Code, RoutingCategory, IsDefault |
QueueArchived | доменное | Переход Active → Archived | Id, TenantId, Code, TargetQueueId? |
Интеграционных Kafka-событий нет: очередь — внутренний справочник support, внешние контексты о ней не
знают. Доменные события обрабатываются внутренними подписчиками в одном согласованном изменении.
SEED (Migrator)
Начальный набор очередей заводится SEED-миграцией Migrator-проекта support (read-only для API, меняется
только миграцией). Гарантирует INV-Q-2 (наличие ровно одной активной default) и INV-Q-3 (уникальность
routing-категорий).
Code | Name | RoutingCategory | IsDefault |
|---|
General | Общие | null | true |
Deals | Сделки | Deal | false |
Billing | Финансы | Payment | false |
Complaints | Жалобы | Complaint | false |
Technical | Технические | Technical | false |
Очередь «Сделки» обслуживает категорию Deal как routing-ключ; т.к. RoutingCategory единична (INV-Q-3),
категория Booking при отсутствии выделенной активной очереди маршрутизируется в default General
(INV-Q-5) — при необходимости заводится отдельная очередь или расширяется правило маршрутизации миграцией.
Enum-значения категорий и приоритетов — ticket.md.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|
| Индексы | PK; ix_ticket_queues_tenant_id; ux_queues_code unique (tenant_id,code); ux_queues_routing_category unique partial where routing_category is not null and status='Active'; ux_queues_default unique partial where is_default and status='Active'; ix_ticket_queues_sort_order; ix_ticket_queues_status |
| Партиционирование / retention | none (единицы строк на платформу); retention не применяется |
| Concurrency | оптимистическая блокировка — конфликты правки default/маршрутизации (правка не должна затирать чужую) |
| Permissions / PII | support.queues.manage — CRUD (супервизор поддержки); support.tickets.view/support.tickets.manage — read-доступ к очередям для фильтров/списков тикетов. PII отсутствует. |
| Audit | отметки создания/изменения/удаления (кто и когда), soft-delete |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: TicketEntity
- API (10): GET /api/support/dashboard — Дашборд обращений Службы заботы, GET /api/support/queues — Список очередей, GET /api/support/tickets — Список обращений (фильтры), GET /api/support/tickets/{id} — Карточка обращения (GetTicket), PATCH /api/support/tickets/{id} — Приоритет/категория/очередь, POST /api/public/support/tickets — Подать обращение, POST /api/support/queues — Создать очередь, POST /api/support/queues/{id}/archive — Архивировать очередь, POST /api/support/tickets — Создать обращение от имени пользователя, PUT /api/support/queues/{id} — Обновить очередь
- Use Cases (1): UC-SUP-001. Приём обращения и назначение оператора
- События (2): События сервиса support: карта топиков и решения об интеграциях, Event Contract: pin.support.ticket-created (TicketCreatedKafkaEvent)
- Дашборды (1): Панель Наблюдаемости: support-overview (эксплуатация)
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
- index.md (1): Support Service
Связанные документы
- ticket.md —
QueueId, enum TicketCategory/TicketPriority/TicketStatus, правила
маршрутизации create-ticket (INV-Q-5).
- README — обзор контекста
support, SEED-механика (Migrator), матрица 4.4.