# NotificationPreference

## Description

NotificationPreference records a single user's opt-in/opt-out choice for a `(category, channel)` pair. Each row is keyed by the triple `(userId, categoryId, channelId)` and carries `allowed: boolean`. The model is opt-in-by-default: when no row exists for a `(user, category, channel)` triple, the dispatcher treats it as `allowed = true`. A row is only persisted when the user explicitly diverges from the default — typically by toggling an optional category off for a specific channel.

The transactional invariant is enforced at the command boundary: any toggle or bulk update targeting a category whose `optOutAllowed = false` is rejected with `CATEGORY_NOT_OPT_OUT_ABLE`, so an `allowed = false` row can never be persisted for a transactional category.

The dispatcher consults this row in a fixed resolution order: transactional categories deliver immediately; otherwise this row is evaluated, falling back to default opt-in when no preference is found.

## Domain Model Definitions

### Model type

Standard

### Command Definitions

- [updateNotificationPreference](../command/UpdateNotificationPreference.md) - Upsert a single `(userId, categoryId, channelId)` row, setting `allowed`
- [bulkUpdateNotificationPreferences](../command/BulkUpdateNotificationPreferences.md) - Atomically upsert multiple `(category, channel)` rows for the calling user in a single transaction
- [resetNotificationPreferences](../command/ResetNotificationPreferences.md) - Delete every NotificationPreference row for the calling user so the dispatcher reverts to default opt-in for every cell

### Query Definitions

- [getNotificationPreference](../query/GetNotificationPreference.md) - Retrieve a single preference row by `(userId, categoryId, channelId)`
- [listNotificationPreferences](../query/ListNotificationPreferences.md) - List the calling user's preference rows
- [getEffectiveNotificationPreference](../query/GetEffectiveNotificationPreference.md) - Resolve the effective state for a `(category, channel)` pair, reporting whether transactional override, user opt-out, or default applies

### Models

- NotificationPreference

### Invariants

- The triple `(userId, categoryId, channelId)` is unique; a user has at most one preference row per `(category, channel)` pair
- A user can only read or modify their own preference rows; cross-user access is rejected with `FORBIDDEN`
- `allowed = false` may only be persisted when the referenced category has `optOutAllowed = true`; otherwise the command is rejected with `CATEGORY_NOT_OPT_OUT_ABLE` and no row is written
- The absence of a row is functionally equivalent to `allowed = true`; the dispatcher does not distinguish "no row" from "row with allowed = true" at delivery time
- Toggling a preference back to its default value either deletes the row or stores `allowed = true`; both outcomes produce identical dispatch behavior
- Resetting preferences deletes every NotificationPreference row for the calling user

### Relationships

- **References User (user-management) as userId**: the user whose preference this row records
- **References NotificationCategory as categoryId**: the category bucket whose opt-in state is being expressed
- **References NotificationChannel as channelId**: the channel on which the opt-in state applies
