# GetEffectiveNotificationPreference

## Overview

GetEffectiveNotificationPreference resolves the dispatch decision for a `(userId, eventType, channelKey)` triple by composing the same gate stack the dispatcher applies at runtime: the transactional override, the user's NotificationPreference row, and the default opt-in. The result is `{ allowed, source }` where `source` discloses which layer determined the decision (`TRANSACTIONAL_OVERRIDE`, `USER_PREFERENCE`, or `DEFAULT`). This query is the canonical answer to "would this user receive this event on this channel right now?" and is consumed by preference-resolution UIs that want to show the user the live effective state, including which layer is overriding their explicit setting.

Keying by `eventType` (not `categoryId`) lets the query resolve the category through the same `EventCategoryBinding` the dispatcher uses, so the transactional override reflects **both** the binding's `transactional` flag and the category's `optOutAllowed = false` — identical to dispatch. One dispatch-time gate is intentionally out of scope: the reason-driven **critical bypass** (`ASSIGNED` / `MENTION` forcing delivery past a mute) depends on the per-event resolved reason, which a reason-free query cannot know — an `allowed: false` result therefore means "muted for ordinary deliveries", not "muted for every possible dispatch".

## Business Rules

- Accepts `{ userId, eventType, channelKey }`
- Self-scope: the caller may only read their own effective preference (`userId == caller.actorId`); otherwise `FORBIDDEN`
- Resolution order (mirrors the dispatcher's gate order):
  1. Resolve the event's `EventCategoryBinding` by `eventType`; if none exists, `CATEGORY_NOT_FOUND`
  2. If the binding is `transactional` or the resolved category's `optOutAllowed = false`, return `{ allowed: true, source: 'TRANSACTIONAL_OVERRIDE' }` — the user preference is ignored
  3. Resolve the channel by `channelKey`; if no NotificationChannel matches, `CHANNEL_NOT_FOUND`
  4. Else if a NotificationPreference row exists for `(userId, categoryId, channelId)`, return `{ allowed: row.allowed, source: 'USER_PREFERENCE' }`
  5. Else return `{ allowed: true, source: 'DEFAULT' }`
- The reason-driven critical bypass is not modeled (the query carries no per-event reason); the result reflects the no-bypass path
- Read-only computation; does not mutate any preference or policy row

## Process Flow

```mermaid
flowchart TD
    A[Receive userId, eventType, channelKey] --> B{Caller is userId?}
    B -->|No| C[Error: FORBIDDEN]
    B -->|Yes| D[Load EventCategoryBinding by eventType]
    D --> E{Binding exists?}
    E -->|No| EN[Error: CATEGORY_NOT_FOUND]
    E -->|Yes| F{binding.transactional OR category.optOutAllowed = false?}
    F -->|Yes| G[Return allowed=true, source=TRANSACTIONAL_OVERRIDE]
    F -->|No| H[Load NotificationChannel by channelKey]
    H --> I{Channel exists?}
    I -->|No| IN[Error: CHANNEL_NOT_FOUND]
    I -->|Yes| J[Load NotificationPreference for user+category+channel]
    J --> K{Preference row exists?}
    K -->|Yes| L[Return allowed=row.allowed, source=USER_PREFERENCE]
    K -->|No| M[Return allowed=true, source=DEFAULT]
```

## External Dependencies

- None — resolution reads only module-owned tables (`EventCategoryBinding`, `NotificationCategory`, `NotificationChannel`, `NotificationPreference`)

## Error Scenarios

- **FORBIDDEN**: Caller is not authorized to perform this operation in the target scope
- **CATEGORY_NOT_FOUND**: No NotificationCategory matches the supplied categoryId
- **CHANNEL_NOT_FOUND**: No NotificationChannel row matches the supplied channelId

## Test Cases

- returns `{ allowed: true, source: 'TRANSACTIONAL_OVERRIDE' }` when the event's binding is transactional
- returns `{ allowed: true, source: 'TRANSACTIONAL_OVERRIDE' }` when the category is not opt-out-able even if the binding is not transactional
- returns `{ allowed: false, source: 'USER_PREFERENCE' }` when the user has an explicit `allowed = false` row
- returns `{ allowed: true, source: 'USER_PREFERENCE' }` when the user has an explicit `allowed = true` row
- returns `{ allowed: true, source: 'DEFAULT' }` when no preference row exists for the triple
- rejects a caller requesting another user's preference with FORBIDDEN
- returns CATEGORY_NOT_FOUND when no EventCategoryBinding matches the supplied eventType
- returns CHANNEL_NOT_FOUND when the channelKey resolves to no NotificationChannel row
