# MarkAllNotificationsAsRead

## Permission Scope

inbox

## Overview

markAllNotificationsAsRead is the bulk inbox action that adds `READ` (and `SEEN` if absent) to every caller-owned **IN_APP** Notification whose `deliveryStatus IN {SENT, DELIVERED}`, has not yet been read, and has not yet been archived. The command is the bulk counterpart of the inbox feed, so it scopes the candidate set to the `IN_APP` channel the same way `listInboxNotifications` / `unreadCount` do — rows dispatched on other channels (e.g. EMAIL) are never stamped `READ`. The command runs in a single transaction, stamps `readAt` on each transitioned row, writes one `NotificationDeliveryAudit` row per transition, and returns the count of rows updated. It is idempotent — invoking it on a user with zero unread rows returns success with `transitionedCount = 0`. Already-archived rows are not touched even when `READ` is absent, since archive is the user's explicit "done with this" signal.

## Business Rules

- The caller's userId is the implicit target; no other user may be supplied
- The selection filter is: `recipientUserId = caller.userId` AND channel is `IN_APP` AND `deliveryStatus IN {SENT, DELIVERED}` AND `READ ∉ engagementStatuses` AND `ARCHIVED ∉ engagementStatuses`
- For each matching row, add `READ` (and `SEEN` if absent) and stamp `readAt`
- Writes one `NotificationDeliveryAudit` row per transition with `eventType=READ`, `occurredBy=caller.userId`
- Returns `transitionedCount`
- Idempotent: zero matching rows returns success with `transitionedCount = 0`

## Process Flow

```mermaid
flowchart TD
    A[Receive mark-all-read request] --> B[Select caller-owned IN_APP Notifications where deliveryStatus IN SENT, DELIVERED AND READ not in engagementStatuses AND ARCHIVED not in engagementStatuses]
    B --> C{Selection non-empty?}
    C -->|No| D[Return transitionedCount=0]
    C -->|Yes| E[For each matching row]
    E --> F[Add READ to engagementStatuses, stamp readAt]
    F --> G{SEEN already present?}
    G -->|No| H[Add SEEN, stamp seenAt]
    G -->|Yes| I[Skip SEEN add]
    H --> J[Write audit row eventType=READ, occurredBy=caller.userId]
    I --> J
    J --> K[Commit, return transitionedCount]
```

## External Dependencies

- [notification-delivery-audit](../feature/notification-delivery-audit.md) - Writes one `READ` audit row per transitioned Notification

## Error Scenarios

- None at the row scope — the command is bounded to caller-owned rows and per-row state checks are part of the selection filter

## Test Cases

- transitions every caller-owned Notification where deliveryStatus IN SENT, DELIVERED AND READ not in engagementStatuses AND ARCHIVED not in engagementStatuses
- adds SEEN alongside READ when SEEN was not previously present
- stamps readAt on each transitioned row
- does not modify rows already containing READ
- does not modify rows already containing ARCHIVED
- does not modify rows where deliveryStatus is QUEUED, FAILED, or BOUNCED
- scopes the candidate set to the IN_APP channel; EMAIL rows are never stamped READ
- does not modify rows owned by other users (recipient-scoped)
- returns transitionedCount=0 and persists no changes when no rows match (idempotent)
- writes one NotificationDeliveryAudit row per transitioned Notification
- causes unreadCount returned by the inbox feed to become zero on the next query
