Entity / Aggregate: TicketTemplateEntity
Дом полей агрегата «Шаблонный ответ» контекста support (greenfield, матрица полей 4.3 — отправка
шаблонных ответов; 5.4 — шаблонные юр.документы как метаданные шаблона, генерация отложена). Справочник
support: проекции (список шаблонов, apply в переписке) ссылаются сюда и описывают только дельту.
Назначение
TicketTemplateEntity — переиспользуемый шаблон ответа оператора Службы заботы ПИН: заготовка темы и тела
с плейсхолдерами, которая при ответе на тикет (reply-ticket) рендерится по данным тикета/заявителя и кладётся
в переписку (ticket-message.md). Независимый справочник (не входит в агрегат тикета):
ведётся операторами/супервизорами, применяется при формировании публичного ответа. Метаданные шаблонных
юр.документов (5.4) живут здесь же как обычный шаблон; сама генерация документа отложена (вне контекста).
PII в шаблоне нет — персональные данные подставляются только в момент рендера в TicketMessage.Body.
Классификация
| Поле | Значение |
|---|---|
| Контекст | support |
| Kind | AggregateRoot (самостоятельный справочник; переписки/тикеты — внешние reference) |
| Source of truth | support |
| Таблица | support.ticket_templates |
| Tenant isolation | TenantId = PIN CompanyId — платформенный справочник, доступ по platform-scope (ADR-0052) |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор шаблона. |
TenantId | Guid | Да | index ix_ticket_templates_tenant_id | app | PIN CompanyId (единый tenant платформы). |
Code | string | Да | varchar(64), не пустое; unique ux_ticket_templates_code (tenant_id,code) where deleted_at is null | app | Стабильный код шаблона (INV-TPL-1). |
Name | string | Да | varchar(128), не пустое | app | Отображаемое имя шаблона. |
Category | TicketCategory? | Нет | .HasConversion<string>(), varchar(32); часть partial-индекса | app | Применимая категория тикета (см. enum в ticket.md); null — шаблон применим к любой категории. |
SubjectTemplate | string? | Нет | varchar(256) | app | Опциональный шаблон темы ответа (те же плейсхолдеры). |
Body | string | Да | varchar(8000), не пустое | app | Текст ответа с плейсхолдерами из белого списка (INV-TPL-2). |
Language | string | Да | varchar(8), default 'ru' | app | Язык шаблона (BCP-47 tag, напр. ru/en). |
Status | TemplateStatus | Да | .HasConversion<string>(), varchar(16), index ix_ticket_templates_status | app | Статус справочника (см. enum и stateDiagram). |
UsageCount | long | Да | default 0, >= 0 | app (apply) | Счётчик применений (аналитика; INV-TPL-3). |
CreatedAt | DateTimeOffset | Да | — | app | Создание записи. |
CreatedByUserId | Guid? | Нет | — | app | Кто создал (null — SEED). |
UpdatedAt | DateTimeOffset | Да | — | app | Момент последнего изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | — | app | Soft-delete. |
DeletedByUserId | Guid? | Нет | — | app | Кто удалил. |
Enum TemplateStatus
| Значение | Описание |
|---|---|
Active | Активен; доступен для применения в reply-ticket. |
Archived | Архивирован; не применяется (INV-TPL-4), в списках скрыт по умолчанию. |
Белый список плейсхолдеров Body / SubjectTemplate
Валидатор допускает только перечисленные токены; иное — VALIDATION_FAILED (INV-TPL-2). Рендер — при apply
(INV-TPL-3), значения берутся из тикета/заявителя/оператора и не хранятся в шаблоне.
| Плейсхолдер | Источник при рендере | Формат |
|---|---|---|
{{RequesterName}} | TicketEntity.RequesterName (FullName) | display-строка ФИО заявителя |
{{TicketNumber}} | TicketEntity.Number | SUP-NNN |
{{DealId}} | TicketEntity.RelatedDealId? | Guid или пусто, если сделки нет |
{{OperatorName}} | actor-оператор (FullName из identity) | display-строка ФИО оператора |
Value objects
Value objects не используются.
Модели JSONB (IDocument, versioned)
JSONB-модели не используются (тело — плоский varchar, плейсхолдеры — валидируемый белый список).
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
TicketMessageEntity | reference (обратная) | ticket_messages.template_id | проставляется при apply в reply-ticket; обратно в агрегат не грузится | — (шаблон не удаляется по сообщению) |
| Данные тикета/заявителя/оператора | внешняя логическая | — | читаются при обработке reply-ticket для рендера; в агрегате не хранятся | — |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-TPL-1 | Code уникален в рамках TenantId (среди неудалённых) | уникальный индекс ux_ticket_templates_code + проверка при создании/обновлении | SUPPORT_TEMPLATE_CODE_DUPLICATE |
| INV-TPL-2 | Body/SubjectTemplate содержат только плейсхолдеры из белого списка | при создании/обновлении шаблона | VALIDATION_FAILED |
| INV-TPL-3 | apply (в reply-ticket): рендер плейсхолдеров из данных тикета/заявителя/оператора → результат в TicketMessage.Body, TemplateId = шаблон, UsageCount++ | при применении шаблона в reply-ticket (см. ../api/support/reply-ticket.md) | — |
| INV-TPL-4 | Архивированный шаблон (Status = Archived) не применяется | при применении шаблона в reply-ticket | SUPPORT_TEMPLATE_ARCHIVED |
Рендер и конкурентность (apply в reply-ticket)
- Только активный шаблон. Применение архивированного шаблона (
Status = Archived) отклоняетсяSUPPORT_TEMPLATE_ARCHIVED(INV-TPL-4) — гонка «оператор применяет ↔ супервизор архивирует» разрешается на момент apply: если к моменту рендера шаблон ужеArchived, ответ не формируется и оператор выбирает другой. - Отсутствующий источник плейсхолдера. Токен из белого списка, чей источник пуст, рендерится в пустую
строку, а не в ошибку:
{{DealId}}приRelatedDealId = null→ пусто;{{RequesterName}}, если реплика заявителя изidentityещё не пришла (TicketEntity.RequesterNameпуст) → пусто/деградация подписи. Неизвестный токен вне белого списка — это ошибка валидации самого шаблона (INV-TPL-2,VALIDATION_FAILED) на создании/обновлении, а не на рендере. - PII появляется только на рендере. Результат подстановки уходит в
TicketMessage.Body, где и действуют правила PII (ticket-message.md); сам шаблон PII не хранит. UsageCountприблизителен. Инкремент при apply (INV-TPL-3) — счётчик аналитики; строгой идемпотентности/точности под конкуренцией не требует, потерянные единицы допустимы. Контентная правка тела шаблона разводится оптимистичной блокировкой (правка не должна затирать чужую).
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
TemplateCreated | доменное | Создание шаблона | внутренние подписчики (аудит/счётчики) |
TemplateArchived | доменное | Переход Active → Archived | внутренние подписчики |
Интеграционных Kafka-событий нет: справочник наружу не публикуется (потребителей вне support нет).
Доменные события обрабатываются внутренними подписчиками.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK; ix_ticket_templates_tenant_id; unique ux_ticket_templates_code (tenant_id,code) where deleted_at is null; ix_ticket_templates_status; partial btree (category,status) where status = 'Active' — подбор применимых шаблонов в reply-ticket |
| Партиционирование / retention | none (малый объём справочника) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую) |
| Permissions / PII | support.templates.manage (оператор/супервизор — CRUD, create/update/archive); support.tickets.manage — read + apply в reply-ticket. PII — нет: шаблон хранит только текст с плейсхолдерами; персональные данные подставляются в момент рендера в TicketMessage.Body (там и действуют правила PII, ticket-message.md). |
| Audit | отметки создания/изменения/удаления (кто и когда), soft-delete |
SEED
Базовые шаблоны создаются миграцией (CreatedByUserId = null), Status = Active, Language = 'ru':
приветствие, «уточните детали», «решено», «эскалировано» (стабильные Code, редактируемы операторами).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: KnowledgeBaseArticleEntity, Entity: TicketMessageEntity (child of Ticket)
- API (5): GET /api/support/templates — Список шаблонов ответов, POST /api/support/templates — Создать шаблон ответа, POST /api/support/templates/{id}/archive — Архивировать шаблон ответа, POST /api/support/tickets/{id}/messages — Ответ оператора / внутренняя заметка, PUT /api/support/templates/{id} — Обновить шаблон ответа
- Use Cases (1): UC-SUP-002. Решение обращения
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
- index.md (1): Support Service
Связанные документы
- ticket-message.md (поле
TemplateId, дом PII рендера), ticket.md (источник данных рендера, enumTicketCategory), ../api/support/reply-ticket.md (операция apply). - README контекста; tenancy — ADR-0052.