Entity / Aggregate: NotificationTemplateEntity
Дом полей агрегата «Шаблон уведомления» контекста notifications (greenfield). Проекции (рендер тела в
notification.md, admin-CRUD) ссылаются сюда.
Назначение
NotificationTemplateEntity — шаблон текста уведомления, ключуемый парой (Kind, Locale) и содержащий
отдельное тело на каждый канал (channel.md) как owned-child ChannelTemplateEntity
(sms — короткое без заголовка; email — subject + html/text; push/in-app — title + body + deeplink и т.д.).
Тело содержит типизированные плейсхолдеры ({...}) из бо́льшого множества, допустимого для данного
Kind (набор задаётся PayloadModel источника — notification.md); рендер подставляет
значения из Payload. Управляется администратором (CRUD, ../api/admin/); дефолтные шаблоны —
SEED мигратора. Aggregate root; изменение шаблона версионируется, in-flight уведомления хранят собственный
RenderedBody (снапшот), поэтому правка шаблона их не меняет.
Классификация
| Поле | Значение |
|---|
| Контекст | notifications |
| Kind | AggregateRoot (owned-child ChannelTemplateEntity — тело по каналам) |
| Базовые контракты SDK | AggregateRoot<Guid>, IEntity (Guid) |
| Source of truth | notifications |
| Таблица | notifications.notification_templates (+ notifications.channel_templates) |
| Tenant isolation | не tenanted — платформенный каталог (ADR-0052 п.3); доступ по admin-permission |
Реализуемые capability-интерфейсы (Talent.Common.Primitives.Entity)
| Интерфейс | Применяется | Комментарий |
|---|
IEntity (Guid) | Да | Id = Guid.CreateVersion7(). |
ICodeEntity | Да | Code = "{Kind}:{Locale}" — уникальный ключ шаблона. |
IEntityWithStatus<TemplateStatus> | Да | Draft/Active/Archived. |
IAuditableEntity / IModifiedEntity | Да | Actor-stamping + UpdatedAt. |
IVersionedEntity | Да | long Version — ревизия шаблона. |
IOptimisticBlockedEntity | Да | uint RowVersion (xmin). |
ISoftDeleteEntity | Да | DeletedAt? (архивирование = Archived + soft-delete). |
ITenantedEntity/INullableTenantedEntity | Нет | Платформенный каталог. |
IDocument | Нет | jsonb не используется — тело в child-строках, плейсхолдеры — enum-массив. |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|
Id | Guid | Да | PK | app | Идентификатор шаблона. |
Kind | NotificationKind | Да | .HasConversion<string>(), varchar(48) | app | Вид уведомления (notification.md). |
Locale | string | Да | varchar(8) (BCP-47), default ru | app | Локаль — BCP-47 из закрытого списка (дом — profile.md); НЕ LanguageId справочника handbook (ADR-0063). |
Code | string | Да | unique ux_templates_code = "{Kind}:{Locale}" | app | Ключ резолва (ICodeEntity). |
Name | string | Да | varchar(128) | app | Человекочитаемое имя (admin-список). |
Description | string? | Нет | varchar(512) | app | Назначение шаблона. |
DefaultUrgency | NotificationUrgency | Да | .HasConversion<string>(), default Normal | app | Срочность по умолчанию для Kind (notification.md). |
Placeholders | IReadOnlyList<NotificationPlaceholder> | Да | text[] (enum-строки), не пустой | app | Допустимый типизированный набор плейсхолдеров для Kind. |
Status | TemplateStatus | Да | .HasConversion<string>(), varchar(16), index | app | Статус (см. enum). |
IsSystem | bool | Да | default false | app | SEED-шаблон (защита от удаления). |
CreatedAt | DateTimeOffset | Да | — | app | Создание. |
CreatedByUserId | Guid? | Нет | — | app | Автор (null — SEED). |
UpdatedAt | DateTimeOffset | Да | — | app | IModifiedEntity. |
UpdatedByUserId | Guid? | Нет | — | app | Последний редактор. |
DeletedAt | DateTimeOffset? | Нет | — | app | Soft-delete (архив). |
Version | long | Да | >= 1 | app | Ревизия (IVersionedEntity). |
RowVersion | uint | Да | concurrency (xmin) | db | IOptimisticBlockedEntity. |
Enum TemplateStatus
| Значение | Описание |
|---|
Draft | Создан/редактируется; для рендера не выбирается. |
Active | Действующий; резолвится при enqueue. |
Archived | Выведен из обращения (soft-delete); историю рендеров не затрагивает. |
Enum NotificationPlaceholder (типизированный набор)
Бо́льшое конечное множество плейсхолдеров всех Kind; для конкретного Kind допустимо своё подмножество
(Placeholders). Значения биндятся из полей PayloadModel при рендере (строгая типизация — не свободный
словарь). Примеры: RecipientName, DealNumber, PropertyObjectName, StageName, Amount, Currency,
ExpiresAt, PostTitle, AuthorName, CompanyName, SignInAt, DecisionReason, DeepLink.
Value objects / EF owned types
ChannelTemplateEntity (owned-child)
Тело шаблона на конкретный канал; таблица notifications.channel_templates (cascade). Строгая типизация —
явные поля subject/title/body, не bare-json.
| Поле | Тип | Обязательность | Ограничения | Колонка |
|---|
Id | Guid | Да | PK | id |
TemplateId | Guid | Да | FK → notification_templates(id), cascade | template_id |
Channel | Channel | Да | .HasConversion<string>(); unique (TemplateId, Channel) | channel |
Subject | string? | Нет | varchar(256); email/тема | subject |
Title | string? | Нет | varchar(256); push/in-app заголовок | title |
Body | string | Да | text; плейсхолдеры {...} | body |
Format | TemplateBodyFormat | Да | Text/Html/Markdown (.HasConversion<string>()) | format |
IsEnabled | bool | Да | default true; выключен — канал не рендерится | is_enabled |
Модели JSONB (IDocument, versioned)
JSONB-модели не используются: тело — в child-строках ChannelTemplateEntity, набор плейсхолдеров — text[].
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|
ChannelTemplateEntity | owned-child | channel_templates.template_id | include при рендере/GetById | cascade |
NotificationEntity | reference (обратная) | Notification.TemplateId | — | restrict (шаблон не удаляется физически) |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|
| INV-TPL-1 | Code = "{Kind}:{Locale}" уникален; один Active-шаблон на (Kind, Locale) | unique + Template.Publish | NTF_TEMPLATE_DUPLICATE |
| INV-TPL-2 | Плейсхолдеры тела ⊆ Placeholders (допустимых для Kind); неизвестный плейсхолдер запрещён | Template.SetChannelBody (валидатор) | NTF_TEMPLATE_UNKNOWN_PLACEHOLDER |
| INV-TPL-3 | Переход в Active требует ≥ 1 ChannelTemplateEntity с IsEnabled | Template.Publish | NTF_TEMPLATE_NO_CHANNEL_BODY |
| INV-TPL-4 | IsSystem-шаблон нельзя Archived/удалить, только редактировать | командные методы | NTF_TEMPLATE_SYSTEM_LOCKED |
| INV-TPL-5 | Резолв при enqueue: Active-шаблон (Kind, Locale); нет локали → fallback ru; нет тела канала (IsEnabled) → канал пропускается | ResolveTemplate(kind, locale, channel) | NTF_TEMPLATE_NOT_RESOLVED |
| INV-TPL-6 | Биндинг при рендере: значение плейсхолдера берётся из соответствующего поля PayloadModel; опциональный плейсхолдер с отсутствующим/null значением рендерится пустым/опускается; тело не должно опираться на обязательный плейсхолдер, чей источник может быть пуст (проверяется при Publish против набора Placeholders) | рендер тела | — |
Снапшот тела. RenderedBody/RenderedTitle рассчитываются один раз при enqueue и хранятся на
NotificationEntity; последующая правка/архивирование шаблона in-flight уведомления не меняет (историю рендеров
не переписываем).
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|
TemplateChanged | IDomainEvent | Publish/Edit/Archive | внутренний invalidate кэша резолва шаблонов (in-process) |
Интеграционных IKafkaEvent шаблон не публикует (внутренний каталог, ADR-0056: без событий без потребителей).
Индексы, хранение, безопасность
| Аспект | Значение |
|---|
| Индексы | PK; ux_templates_code unique; ix_templates_status; child ux_channel_templates (template_id, channel) |
| Партиционирование / retention | none (десятки-сотни строк) |
| Concurrency | RowVersion (xmin); Version — ревизия |
| Permissions / PII | notifications.templates.view/notifications.templates.manage (admin); PII нет (тело — шаблон с плейсхолдерами) |
| Audit | IAuditableEntity + Version + soft-delete |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы