# GetNotificationPreference

## Overview

GetNotificationPreference returns the persisted NotificationPreference row for the triple `{ userId, categoryId, channelId }`, or null when no row has been written. A null result is meaningful: the preference model is opt-in-by-default, so a missing row denotes "user has not diverged from the default" — the dispatcher treats it as `allowed = true`. Callers that need the resolved dispatch decision (rather than the raw row) should use `GetEffectiveNotificationPreference`.

## Business Rules

- Accepts `{ userId, categoryId, channelId }`
- Returns null when no NotificationPreference row exists for the triple — null denotes default opt-in
- Caller must equal `userId`; cross-user reads are rejected with `FORBIDDEN`
- Returned row carries `userId`, `categoryId`, `channelId`, and `allowed`

## Process Flow

```mermaid
flowchart TD
    A[Receive userId, categoryId, channelId] --> B{Caller userId == input userId?}
    B -->|No| C[Error: FORBIDDEN]
    B -->|Yes| D[Load NotificationPreference by triple]
    D --> E{Row found?}
    E -->|No| F[Return null - default opt-in implied]
    E -->|Yes| G[Return NotificationPreference row]
```

## External Dependencies

- None

## Error Scenarios

- **FORBIDDEN**: Caller is not authorized to perform this operation in the target scope
- **NO_MATCH**: No matching row exists for the supplied input; null or empty result is returned

> Returning `NO_MATCH` is a normal empty/null result rather than a thrown error — the query returns the null/empty value and does not raise `NoMatchError`.

## Test Cases

- returns the persisted NotificationPreference row when found by `(userId, categoryId, channelId)`
- returns null when no row has been persisted for the triple
- returns `allowed = false` rows when the user has explicitly opted out
- rejects cross-user reads with FORBIDDEN
