# ListInboxNotifications

## Overview

ListInboxNotifications returns the caller's in-app notification feed plus the unread count. The feed is implicitly scoped to `recipientUserId == caller.userId` — there is no `userId` input — restricted to rows dispatched on the `IN_APP` channel, and filtered to rows where `deliveryStatus IN {SENT, DELIVERED}`, ordered by `createdAt` descending with paging. The inbox is the IN_APP surface only: rows dispatched on other channels (EMAIL, SMS, PUSH, …) are delivered by their own adapter and never appear in the feed, so a single event that fans out to IN_APP + EMAIL yields exactly one inbox row. Operator-owned `deliveryStatus` values (QUEUED, FAILED, BOUNCED) are excluded so recipients are never exposed to dispatch internals; delivery triage stays in the audit surface.

`unreadCount` is computed against the full caller-owned IN_APP set, not against the paged window: it is the number of caller-owned IN_APP rows where `deliveryStatus IN {SENT, DELIVERED}` AND `READ ∉ engagementStatuses` AND `ARCHIVED ∉ engagementStatuses`. By default ARCHIVED rows are excluded from the feed; passing `includeArchived = true` returns archived rows alongside active rows for the "Archive" tab.

This query is **read-only**. The feature doc specifies that opening the inbox feed adds `SEEN` to engagementStatuses for all visible rows lacking it — that side-effect is performed by a separate `markNotificationsAsSeen` command, which the client is expected to call after fetching the feed. This query does not mutate `engagementStatuses`.

## Business Rules

- Takes optional `{ limit?, offset?, includeArchived? }`; recipient is implicit (`recipientUserId == caller.userId`)
- Returns only rows dispatched on the `IN_APP` channel (rows on EMAIL / SMS / PUSH / SLACK / TEAMS are never returned)
- Returns rows where `deliveryStatus IN {SENT, DELIVERED}` (rows in QUEUED, FAILED, or BOUNCED are never returned)
- Excludes rows where `ARCHIVED ∈ engagementStatuses` by default
- When `includeArchived = true`, returns archived rows alongside active rows
- Orders rows by `createdAt` descending; `limit` and `offset` paginate the result
- `unreadCount` is computed across the full caller-owned IN_APP set (not the paged window) as rows where `deliveryStatus IN {SENT, DELIVERED}` AND `READ ∉ engagementStatuses` AND `ARCHIVED ∉ engagementStatuses`
- Returns an empty list with `unreadCount = 0` when the caller has no qualifying rows
- Read-only — does not add `SEEN` to engagementStatuses; the client calls `markNotificationsAsSeen` separately after fetch

## Process Flow

```mermaid
flowchart TD
    A[Receive optional limit, offset, includeArchived] --> B[Filter Notification rows by recipientUserId == caller.userId]
    B --> B2[Join NotificationChannel and keep only channelId == IN_APP]
    B2 --> C[Apply deliveryStatus IN SENT, DELIVERED]
    C --> D{includeArchived = true?}
    D -->|No| E[Add archivedAt IS NULL filter]
    D -->|Yes| F[Keep archived rows]
    E --> G[Order by createdAt desc and apply limit + 1 / offset]
    F --> G
    G --> H[Run count query: readAt IS NULL AND archivedAt IS NULL for unreadCount]
    H --> I[Return paged feed and unreadCount]
```

## External Dependencies

- None

## Error Scenarios

- **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 caller's feed rows
- scopes the feed to the caller, IN_APP, deliverable statuses, ordered by createdAt desc
- excludes archived rows from the feed via archivedAt IS NULL by default
- does not filter archivedAt on the feed when includeArchived = true
- applies limit + 1 and offset to the feed query
- returns unreadCount from the count query
- counts unread over IN_APP, deliverable, unread, non-archived rows
- keeps the unread filters even when includeArchived = true
- issues exactly one feed query and one count query, both IN_APP-scoped
- returns an empty list with unreadCount = 0 when the caller has no qualifying rows
- does not mutate any row
