# ListNotificationPreferences

## Overview

ListNotificationPreferences returns every persisted NotificationPreference row for a given user. Because the preference model is opt-in-by-default, the result represents only the cells the user has explicitly diverged from default — the absence of a row for a given `(category, channel)` pair means "default opt-in". The caller-facing preference UI uses this query to render the preference matrix, joined against the seeded category catalog and channel registry on the client side.

## Business Rules

- Accepts optional `{ userId? }`; when omitted, defaults to the calling user. Standard pagination inputs (`limit`, `offset`, `orderBy`, `orderDirection`) are also accepted
- Caller may only list their own preferences — a non-self `userId` is rejected with `FORBIDDEN`
- Returns an empty list when the caller has no persisted preference rows (i.e., is fully on defaults)
- Each row carries `userId`, `categoryId`, `channelId`, and `allowed`
- No implicit ordering is guaranteed; the UI is expected to project rows into the (category, channel) matrix on the client

## Process Flow

```mermaid
flowchart TD
    A[Receive optional userId] --> B[Default userId to caller when omitted]
    B --> C{Resolved userId == caller userId?}
    C -->|No| D[Error: FORBIDDEN]
    C -->|Yes| E[Load NotificationPreference rows for userId]
    E --> F[Return list]
```

## 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 every persisted NotificationPreference row for the caller
- returns an empty list when the caller has no persisted rows (fully on defaults)
- defaults to the caller when no userId is supplied
- accepts `userId == caller.userId` and returns the same result as omitting userId
- rejects `userId != caller.userId` with FORBIDDEN
