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

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
KindAggregateRoot (owned-child NotificationPreferenceEntity)
Базовые контракты SDKAggregateRoot<Guid>, IEntity (Guid)
Source of truthnotifications
Таблицаnotifications.user_notification_settings (+ notifications.notification_preferences)
Tenant isolationINullableTenantedEntity.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)ИсточникОписание
IdGuidДаPKappИдентификатор записи.
UserIdGuidДаunique ux_user_settings_user_idappВладелец (бизнес-ключ; логическая ссылка identity.users(id)).
TenantIdGuid?Нетindex where not nullappКомпания пользователя (null — инвестор).
LocalestringДаvarchar(8) (BCP-47), default ruappЯвно выбранная пользователем локаль (приоритет при резолве шаблона; при отсутствии явного выбора enqueue берёт локаль из контактной реплики recipient-contact.md, затем ru — INV-RC-4) — BCP-47 из закрытого списка (дом — profile.md); НЕ LanguageId справочника handbook (ADR-0063).
QuietHoursQuietHoursWindow (owned)Нетсм. VO нижеappТихие часы (null — не заданы).
PreferencesIReadOnlyList<NotificationPreferenceEntity>Даowned-child (может быть пуст → дефолты)appПереопределения per-(Kind, Channel).
GlobalMuteboolДаdefault falseappПолное «тихо»: только Urgency = Critical доставляется (безопасность/деньги/дедлайны).
CreatedAt/UpdatedAtDateTimeOffsetДаappТайминги (IModifiedEntity).
VersionlongДа>= 1appIVersionedEntity.
RowVersionuintДаconcurrency (xmin)dbIOptimisticBlockedEntity.

Value objects / EF owned types

QuietHoursWindow (owned)

Окно тишины в локальном времени пользователя (строгая типизация — TimeOnly + IANA-timezone, не bare-string диапазон). Окно может пересекать полночь (Start > End → интервал через полночь).

ПолеТипОбязательностьОграниченияКолонка
StartTimeOnlyДалокальное время началаquiet_start
EndTimeOnlyДалокальное время концаquiet_end
TimeZoneIdstringДаIANA (Europe/Moscow), varchar(64)quiet_tz

NotificationPreferenceEntity (owned-child)

Переопределение канала для конкретного Kind; таблица notifications.notification_preferences (cascade).

ПолеТипОбязательностьОграниченияКолонка
IdGuidДаPKid
SettingsIdGuidДаFK → user_notification_settings(id), cascadesettings_id
KindNotificationKindДа.HasConversion<string>(); unique (SettingsId, Kind, Channel)kind
ChannelChannelДа.HasConversion<string>()channel
IsEnabledboolДаopt-in (true) / opt-out (false)is_enabled

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

JSONB-модели не используются: переопределения — child-строки, тихие часы — owned VO.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
NotificationPreferenceEntityowned-childnotification_preferences.settings_idinclude при GetByUserId/резолвеcascade
Получательвнешняя логическаяUserId

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

Статус-машины нет (справочник настроек). Инварианты применения:

ИнвариантУсловиеГде проверяетсяОшибка
INV-UNS-1Одна запись на UserId (ux_user_settings_user_id); правит только владелец (UserId = actor)unique + handlerNTF_SETTINGS_FORBIDDEN
INV-UNS-2(Kind, Channel) в Preferences уникальны; отсутствие переопределения = дефолт политики для Kindunique + SetPreferenceNTF_PREFERENCE_DUPLICATE
INV-UNS-3Резолв каналов: enabled(Kind, Channel) = явное IsEnabled, иначе дефолт Kind; GlobalMute оставляет только Urgency = CriticalResolveChannels(kind, urgency)
INV-UNS-4Urgency = 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-appinterruptive-каналы по умолчанию выключены (сдержанно)

Резолв каналов получателя: ResolveChannels(kind, urgency) = для каждого Channel — явное IsEnabled из Preferences, иначе дефолт класса; затем GlobalMute оставляет только Urgency = Critical; затем к interruptive-каналам применяются тихие часы (ScheduledAt), кроме Critical. Итог пересекается с кандидатностью канала на стороне notification.md (config IsEnabled + тело шаблона).

Конкурентность правок. Параллельные обновления настроек владельцем разводит оптимистичная блокировка версии записи: побеждает первая зафиксированная правка, проигравшая повторяется на свежей версии; частичного применения набора переопределений нет (изменения фиксируются в одном согласованном изменении).

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
SettingsChangedIDomainEventОбновление настроеквнутренний invalidate кэша резолва (in-process)

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

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

АспектЗначение
ИндексыPK; ux_user_settings_user_id unique; child ux_notification_preferences (settings_id, kind, channel)
Партиционирование / retentionnone (одна строка на пользователя)
ConcurrencyRowVersion (xmin); Version — аудит
Permissions / PIIown action (notifications.settings.manage, UserId = actor); Locale/QuietHours — low-sensitivity
AuditIAuditableEntity + Version

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

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

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