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

Entity / Aggregate: TicketTemplateEntity

Дом полей агрегата «Шаблонный ответ» контекста support (greenfield, матрица полей 4.3 — отправка шаблонных ответов; 5.4 — шаблонные юр.документы как метаданные шаблона, генерация отложена). Справочник support: проекции (список шаблонов, apply в переписке) ссылаются сюда и описывают только дельту.

Назначение

TicketTemplateEntity — переиспользуемый шаблон ответа оператора Службы заботы ПИН: заготовка темы и тела с плейсхолдерами, которая при ответе на тикет (reply-ticket) рендерится по данным тикета/заявителя и кладётся в переписку (ticket-message.md). Независимый справочник (не входит в агрегат тикета): ведётся операторами/супервизорами, применяется при формировании публичного ответа. Метаданные шаблонных юр.документов (5.4) живут здесь же как обычный шаблон; сама генерация документа отложена (вне контекста). PII в шаблоне нет — персональные данные подставляются только в момент рендера в TicketMessage.Body.

Классификация

ПолеЗначение
Контекстsupport
KindAggregateRoot (самостоятельный справочник; переписки/тикеты — внешние reference)
Source of truthsupport
Таблицаsupport.ticket_templates
Tenant isolationTenantId = PIN CompanyId — платформенный справочник, доступ по platform-scope (ADR-0052)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор шаблона.
TenantIdGuidДаindex ix_ticket_templates_tenant_idappPIN CompanyId (единый tenant платформы).
CodestringДаvarchar(64), не пустое; unique ux_ticket_templates_code (tenant_id,code) where deleted_at is nullappСтабильный код шаблона (INV-TPL-1).
NamestringДаvarchar(128), не пустоеappОтображаемое имя шаблона.
CategoryTicketCategory?Нет.HasConversion<string>(), varchar(32); часть partial-индексаappПрименимая категория тикета (см. enum в ticket.md); null — шаблон применим к любой категории.
SubjectTemplatestring?Нетvarchar(256)appОпциональный шаблон темы ответа (те же плейсхолдеры).
BodystringДаvarchar(8000), не пустоеappТекст ответа с плейсхолдерами из белого списка (INV-TPL-2).
LanguagestringДаvarchar(8), default 'ru'appЯзык шаблона (BCP-47 tag, напр. ru/en).
StatusTemplateStatusДа.HasConversion<string>(), varchar(16), index ix_ticket_templates_statusappСтатус справочника (см. enum и stateDiagram).
UsageCountlongДаdefault 0, >= 0app (apply)Счётчик применений (аналитика; INV-TPL-3).
CreatedAtDateTimeOffsetДаappСоздание записи.
CreatedByUserIdGuid?НетappКто создал (null — SEED).
UpdatedAtDateTimeOffsetДаappМомент последнего изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?НетappSoft-delete.
DeletedByUserIdGuid?Нет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.NumberSUP-NNN
{{DealId}}TicketEntity.RelatedDealId?Guid или пусто, если сделки нет
{{OperatorName}}actor-оператор (FullName из identity)display-строка ФИО оператора

Value objects

Value objects не используются.

Модели JSONB (IDocument, versioned)

JSONB-модели не используются (тело — плоский varchar, плейсхолдеры — валидируемый белый список).

Связи

СвязьТипКлючПравило загрузкиDelete behavior
TicketMessageEntityreference (обратная)ticket_messages.template_idпроставляется при apply в reply-ticket; обратно в агрегат не грузится— (шаблон не удаляется по сообщению)
Данные тикета/заявителя/операторавнешняя логическаячитаются при обработке reply-ticket для рендера; в агрегате не хранятся

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-TPL-1Code уникален в рамках TenantId (среди неудалённых)уникальный индекс ux_ticket_templates_code + проверка при создании/обновленииSUPPORT_TEMPLATE_CODE_DUPLICATE
INV-TPL-2Body/SubjectTemplate содержат только плейсхолдеры из белого спискапри создании/обновлении шаблонаVALIDATION_FAILED
INV-TPL-3apply (в reply-ticket): рендер плейсхолдеров из данных тикета/заявителя/оператора → результат в TicketMessage.Body, TemplateId = шаблон, UsageCount++при применении шаблона в reply-ticket (см. ../api/support/reply-ticket.md)
INV-TPL-4Архивированный шаблон (Status = Archived) не применяетсяпри применении шаблона в reply-ticketSUPPORT_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
Партиционирование / retentionnone (малый объём справочника)
Concurrencyоптимистическая блокировка (правка не должна затирать чужую)
Permissions / PIIsupport.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, редактируемы операторами).

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

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

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