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

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
KindAggregateRoot (owned-child ChannelTemplateEntity — тело по каналам)
Базовые контракты SDKAggregateRoot<Guid>, IEntity (Guid)
Source of truthnotifications
Таблица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)ИсточникОписание
IdGuidДаPKappИдентификатор шаблона.
KindNotificationKindДа.HasConversion<string>(), varchar(48)appВид уведомления (notification.md).
LocalestringДаvarchar(8) (BCP-47), default ruappЛокаль — BCP-47 из закрытого списка (дом — profile.md); НЕ LanguageId справочника handbook (ADR-0063).
CodestringДаunique ux_templates_code = "{Kind}:{Locale}"appКлюч резолва (ICodeEntity).
NamestringДаvarchar(128)appЧеловекочитаемое имя (admin-список).
Descriptionstring?Нетvarchar(512)appНазначение шаблона.
DefaultUrgencyNotificationUrgencyДа.HasConversion<string>(), default NormalappСрочность по умолчанию для Kind (notification.md).
PlaceholdersIReadOnlyList<NotificationPlaceholder>Даtext[] (enum-строки), не пустойappДопустимый типизированный набор плейсхолдеров для Kind.
StatusTemplateStatusДа.HasConversion<string>(), varchar(16), indexappСтатус (см. enum).
IsSystemboolДаdefault falseappSEED-шаблон (защита от удаления).
CreatedAtDateTimeOffsetДаappСоздание.
CreatedByUserIdGuid?НетappАвтор (null — SEED).
UpdatedAtDateTimeOffsetДаappIModifiedEntity.
UpdatedByUserIdGuid?НетappПоследний редактор.
DeletedAtDateTimeOffset?НетappSoft-delete (архив).
VersionlongДа>= 1appРевизия (IVersionedEntity).
RowVersionuintДаconcurrency (xmin)dbIOptimisticBlockedEntity.

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.

ПолеТипОбязательностьОграниченияКолонка
IdGuidДаPKid
TemplateIdGuidДаFK → notification_templates(id), cascadetemplate_id
ChannelChannelДа.HasConversion<string>(); unique (TemplateId, Channel)channel
Subjectstring?Нетvarchar(256); email/темаsubject
Titlestring?Нетvarchar(256); push/in-app заголовокtitle
BodystringДаtext; плейсхолдеры {...}body
FormatTemplateBodyFormatДаText/Html/Markdown (.HasConversion<string>())format
IsEnabledboolДаdefault true; выключен — канал не рендеритсяis_enabled

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

JSONB-модели не используются: тело — в child-строках ChannelTemplateEntity, набор плейсхолдеров — text[].

Связи

СвязьТипКлючПравило загрузкиDelete behavior
ChannelTemplateEntityowned-childchannel_templates.template_idinclude при рендере/GetByIdcascade
NotificationEntityreference (обратная)Notification.TemplateIdrestrict (шаблон не удаляется физически)

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

ИнвариантУсловиеГде проверяетсяОшибка
INV-TPL-1Code = "{Kind}:{Locale}" уникален; один Active-шаблон на (Kind, Locale)unique + Template.PublishNTF_TEMPLATE_DUPLICATE
INV-TPL-2Плейсхолдеры тела ⊆ Placeholders (допустимых для Kind); неизвестный плейсхолдер запрещёнTemplate.SetChannelBody (валидатор)NTF_TEMPLATE_UNKNOWN_PLACEHOLDER
INV-TPL-3Переход в Active требует ≥ 1 ChannelTemplateEntity с IsEnabledTemplate.PublishNTF_TEMPLATE_NO_CHANNEL_BODY
INV-TPL-4IsSystem-шаблон нельзя 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/поля
TemplateChangedIDomainEventPublish/Edit/Archiveвнутренний invalidate кэша резолва шаблонов (in-process)

Интеграционных IKafkaEvent шаблон не публикует (внутренний каталог, ADR-0056: без событий без потребителей).

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK; ux_templates_code unique; ix_templates_status; child ux_channel_templates (template_id, channel)
Партиционирование / retentionnone (десятки-сотни строк)
ConcurrencyRowVersion (xmin); Version — ревизия
Permissions / PIInotifications.templates.view/notifications.templates.manage (admin); PII нет (тело — шаблон с плейсхолдерами)
AuditIAuditableEntity + Version + soft-delete

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

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

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