# MarkNotificationsAsSeen

## Permission Scope

inbox

## Overview

markNotificationsAsSeen is the bulk passive-surfacing signal invoked when the caller's inbox feed is opened. The command adds `SEEN` to every caller-owned **IN_APP** Notification whose `deliveryStatus IN {SENT, DELIVERED}` and `SEEN ∉ engagementStatuses`, stamps `seenAt`, and writes one `NotificationDeliveryAudit` row per transition. The selection carries the inbox's implicit `channel is IN_APP` scope (the same scope as `listInboxNotifications` and `markAllNotificationsAsRead`) — rows dispatched on other channels (e.g. EMAIL, terminal at `SENT`) are never stamped SEEN. SEEN is **not** READ — adding SEEN does not decrement the inbox's `unreadCount`; only READ does. The command is idempotent: re-opening a feed where every visible row is already SEEN performs no state changes. Archived rows are not touched regardless of SEEN state, since they are out of the default feed surface.

## Business Rules

- The caller's userId is the implicit target
- Selection filter: `recipientUserId = caller.userId` AND `channelId = IN_APP` AND `deliveryStatus IN {SENT, DELIVERED}` AND `SEEN ∉ engagementStatuses` AND `ARCHIVED ∉ engagementStatuses`
- For each matching row, add `SEEN` and stamp `seenAt`
- Writes one `NotificationDeliveryAudit` row per transition with `eventType=SEEN`, `occurredBy=caller.userId`
- Does **not** add `READ` and does **not** affect `unreadCount`
- Idempotent: a selection of zero rows returns success with `transitionedCount = 0`

## Process Flow

```mermaid
flowchart TD
    A[Receive mark-seen request] --> B[Select caller-owned Notifications where channel is IN_APP AND deliveryStatus IN SENT, DELIVERED AND SEEN 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 SEEN to engagementStatuses, stamp seenAt]
    F --> G[Write audit row eventType=SEEN, occurredBy=caller.userId]
    G --> H[Commit, return transitionedCount]
```

## External Dependencies

- [notification-delivery-audit](../feature/notification-delivery-audit.md) - Writes one `SEEN` 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

- adds SEEN to every caller-owned IN_APP Notification where deliveryStatus IN SENT, DELIVERED AND SEEN not in engagementStatuses AND ARCHIVED not in engagementStatuses
- does not modify rows dispatched on non-IN_APP channels (e.g. EMAIL rows terminal at SENT are never stamped SEEN)
- stamps seenAt on each transitioned row
- does not add READ and does not affect unreadCount
- does not modify rows already containing SEEN (idempotent)
- does not modify rows containing ARCHIVED
- does not modify rows where deliveryStatus is QUEUED, FAILED, or BOUNCED
- does not modify rows owned by other users (recipient-scoped)
- returns transitionedCount=0 and persists no changes on second invocation when no new rows have arrived
- writes one NotificationDeliveryAudit row per transitioned Notification
