Entity / Aggregate: UserNotificationSettingsEntity
Дом полей агрегата «Настройки уведомлений пользователя» контекста notifications (greenfield). Проекции
(consumer-фильтр каналов notification.md, API ../api/public/)
ссылаются сюда.
Назначение
UserNotificationSettingsEntity — персональные настройки уведомлений одного пользователя: per-Kind
per-Channel opt-in/out (owned-child NotificationPreferenceEntity) и тихие часы (QuietHoursWindow).
Бизнес-ключ — UserId (одна запись на пользователя). При enqueue consumer грузит настройки получателя и
оставляет только каналы, разрешённые для данного Kind; interruptive-каналы в тихие часы деферятся
(ScheduledAt), кроме Urgency = Critical. Отсутствие записи = дефолтная политика (transactional-Kind
включены по каналам по умолчанию; маркетинговые/лента — сдержаннее). Aggregate root — правится владельцем
(../api/public/update-my-notification-settings.md).
Классификация
| Поле | Значение |
|---|---|
| Контекст | notifications |
| Kind | AggregateRoot (owned-child NotificationPreferenceEntity) |
| Базовые контракты SDK | AggregateRoot<Guid>, IEntity (Guid) |
| Source of truth | notifications |
| Таблица | notifications.user_notification_settings (+ notifications.notification_preferences) |
| Tenant isolation | INullableTenantedEntity.TenantId = компания пользователя или null (инвестор), ADR-0052 |
Реализуемые capability-интерфейсы (Talent.Common.Primitives.Entity)
| Интерфейс | Применяется | Комментарий |
|---|---|---|
IEntity (Guid) | Да | Id = Guid.CreateVersion7(). |
INullableTenantedEntity | Да | TenantId = компания пользователя или null. |
IAuditableEntity / IModifiedEntity | Да | Actor = сам пользователь. |
IVersionedEntity / IOptimisticBlockedEntity | Да | Version + RowVersion (xmin) — параллельные правки. |
IEntityWithStatus / ISoftDeleteEntity / IDocument / ICodeEntity | Нет | статуса/soft-delete/jsonb нет; ключ — UserId (не Code). |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор записи. |
UserId | Guid | Да | unique ux_user_settings_user_id | app | Владелец (бизнес-ключ; логическая ссылка identity.users(id)). |
TenantId | Guid? | Нет | index where not null | app | Компания пользователя (null — инвестор). |
Locale | string | Да | varchar(8) (BCP-47), default ru | app | Явно выбранная пользователем локаль (приоритет при резолве шаблона; при отсутствии явного выбора enqueue берёт локаль из контактной реплики recipient-contact.md, затем ru — INV-RC-4) — BCP-47 из закрытого списка (дом — profile.md); НЕ LanguageId справочника handbook (ADR-0063). |
QuietHours | QuietHoursWindow (owned) | Нет | см. VO ниже | app | Тихие часы (null — не заданы). |
Preferences | IReadOnlyList<NotificationPreferenceEntity> | Да | owned-child (может быть пуст → дефолты) | app | Переопределения per-(Kind, Channel). |
GlobalMute | bool | Да | default false | app | Полное «тихо»: только Urgency = Critical доставляется (безопасность/деньги/дедлайны). |
CreatedAt/UpdatedAt | DateTimeOffset | Да | — | app | Тайминги (IModifiedEntity). |
Version | long | Да | >= 1 | app | IVersionedEntity. |
RowVersion | uint | Да | concurrency (xmin) | db | IOptimisticBlockedEntity. |
Value objects / EF owned types
QuietHoursWindow (owned)
Окно тишины в локальном времени пользователя (строгая типизация — TimeOnly + IANA-timezone, не bare-string
диапазон). Окно может пересекать полночь (Start > End → интервал через полночь).
| Поле | Тип | Обязательность | Ограничения | Колонка |
|---|---|---|---|---|
Start | TimeOnly | Да | локальное время начала | quiet_start |
End | TimeOnly | Да | локальное время конца | quiet_end |
TimeZoneId | string | Да | IANA (Europe/Moscow), varchar(64) | quiet_tz |
NotificationPreferenceEntity (owned-child)
Переопределение канала для конкретного Kind; таблица notifications.notification_preferences (cascade).
| Поле | Тип | Обязательность | Ограничения | Колонка |
|---|---|---|---|---|
Id | Guid | Да | PK | id |
SettingsId | Guid | Да | FK → user_notification_settings(id), cascade | settings_id |
Kind | NotificationKind | Да | .HasConversion<string>(); unique (SettingsId, Kind, Channel) | kind |
Channel | Channel | Да | .HasConversion<string>() | channel |
IsEnabled | bool | Да | opt-in (true) / opt-out (false) | is_enabled |
Модели JSONB (IDocument, versioned)
JSONB-модели не используются: переопределения — child-строки, тихие часы — owned VO.
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
NotificationPreferenceEntity | owned-child | notification_preferences.settings_id | include при GetByUserId/резолве | cascade |
| Получатель | внешняя логическая | UserId | — | — |
Инварианты и переходы состояний
Статус-машины нет (справочник настроек). Инварианты применения:
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-UNS-1 | Одна запись на UserId (ux_user_settings_user_id); правит только владелец (UserId = actor) | unique + handler | NTF_SETTINGS_FORBIDDEN |
| INV-UNS-2 | (Kind, Channel) в Preferences уникальны; отсутствие переопределения = дефолт политики для Kind | unique + SetPreference | NTF_PREFERENCE_DUPLICATE |
| INV-UNS-3 | Резолв каналов: enabled(Kind, Channel) = явное IsEnabled, иначе дефолт Kind; GlobalMute оставляет только Urgency = Critical | ResolveChannels(kind, urgency) | — |
| INV-UNS-4 | Urgency = Critical (безопасность/деньги/дедлайны) нельзя отключить (opt-out игнорируется), но канал можно сменить | ResolveChannels | — |
| INV-UNS-5 | Тихие часы применяются только к interruptive-каналам (channel.md); QuietHours пусты → без дефера | IsInQuietHours(now) | — |
Дефолтная политика каналов и резолв
Отсутствие записи UserNotificationSettingsEntity или переопределения (Kind, Channel) в Preferences = дефолт
по срочности Kind (INV-UNS-3). Базовая политика (применяется среди IsEnabled-каналов и при наличии тела
шаблона):
Класс Kind | Каналы по умолчанию | Комментарий |
|---|---|---|
Critical (безопасность/деньги/дедлайны) | все способные и включённые каналы | opt-out игнорируется (INV-UNS-4); тихие часы игнорируются |
Normal (транзакционные: crm/rewards/referral/promotion/profile/investor-owner) | in-app всегда + email (если включён); push — если включён | interruptive-каналы уважают тихие часы |
Low (лента/новости) | in-app | interruptive-каналы по умолчанию выключены (сдержанно) |
Резолв каналов получателя: ResolveChannels(kind, urgency) = для каждого Channel — явное IsEnabled из
Preferences, иначе дефолт класса; затем GlobalMute оставляет только Urgency = Critical; затем к
interruptive-каналам применяются тихие часы (ScheduledAt), кроме Critical. Итог пересекается с
кандидатностью канала на стороне notification.md (config IsEnabled + тело шаблона).
Конкурентность правок. Параллельные обновления настроек владельцем разводит оптимистичная блокировка версии записи: побеждает первая зафиксированная правка, проигравшая повторяется на свежей версии; частичного применения набора переопределений нет (изменения фиксируются в одном согласованном изменении).
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
SettingsChanged | IDomainEvent | Обновление настроек | внутренний invalidate кэша резолва (in-process) |
Интеграционных IKafkaEvent не публикует (внутренние настройки, ADR-0056).
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK; ux_user_settings_user_id unique; child ux_notification_preferences (settings_id, kind, channel) |
| Партиционирование / retention | none (одна строка на пользователя) |
| Concurrency | RowVersion (xmin); Version — аудит |
| Permissions / PII | own action (notifications.settings.manage, UserId = actor); Locale/QuietHours — low-sensitivity |
| Audit | IAuditableEntity + Version |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: NotificationEntity, Entity / Aggregate: RecipientContactEntity
- API (3): GET /api/public/notifications/settings — Мои настройки уведомлений, POST /api/internal/notifications/enqueue — Прямой enqueue уведомления (m2m), PUT /api/public/notifications/settings — Обновить мои настройки уведомлений
- Use Cases (2): UC-NTF-001. Пользователь настраивает уведомления, UC-NTF-002. Событие сделки → многоканальное уведомление с учётом настроек
- Консюмеры (3): Consumer: Calendar Events → Notifications (notifications-calendar-events), Consumer: CRM Events → Notifications (notifications-crm-events), Consumer: Identity Events → Notifications (notifications-identity-events)
- index.md (1): Notifications Service
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- notification.md (резолв каналов при enqueue,
Urgency, тихие часы), channel.md (IsInterruptive); API — ../api/public/get-my-notification-settings.md, ../api/public/update-my-notification-settings.md. - README контекста; tenancy — ADR-0052.
- Правила: entity-documentation-rules, strict-typed-documentation-rules.