# ListAuditByNotification

## Overview

ListAuditByNotification returns the full audit timeline for a single Notification — every NotificationDeliveryAudit row referencing the supplied `notificationId`, ordered by `occurredAt` ascending. The result is the lifecycle reconstruction of one message: `QUEUED` at persist time, `SENT` at adapter dispatch (and `DELIVERED` for IN_APP), any `FAILED` or `BOUNCED` event, plus user-driven `SEEN`, `READ`, `ARCHIVED` engagement events. Investigators use this when answering "what happened to notification N?".

## Business Rules

- Accepts a single lookup parameter: `{ notificationId }`
- Self-scope: a caller may only read the audit trail of notifications addressed to them (`recipientUserId == caller.actorId`)
- A missing notificationId and another user's notificationId both return an **empty page** (not an error), so existence cannot be probed
- Returns rows ordered by `occurredAt` ascending (chronological replay)
- Returns an empty list when the notificationId does not exist or has no audit rows
- Each row carries `id`, `notificationId`, `eventType` (one of QUEUED, SENT, DELIVERED, FAILED, BOUNCED, SEEN, READ, ARCHIVED, OPENED), `occurredAt`, `occurredBy` (userId or `system`), `errorClass`, and `errorDetail` (populated only for FAILED / BOUNCED)
- Audit rows that have been anonymized by the retention sweep are still returned, with `errorDetail` cleared and user-driven `occurredBy` replaced by the `TOMBSTONED` sentinel; structural fields (`id`, `notificationId`, `eventType`, `occurredAt`) are preserved

## Process Flow

```mermaid
flowchart TD
    A[Receive notificationId] --> Q{Parent Notification exists and recipientUserId == caller?}
    Q -->|No, missing or foreign-owned| R[Return empty page]
    Q -->|Yes| B[Load NotificationDeliveryAudit rows where notificationId matches]
    B --> C[Order by occurredAt asc]
    C --> D[Return list]
```

## 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 every audit row for the supplied notificationId ordered by `occurredAt` ascending
- returns an empty list when the notificationId has no audit rows
- returns the audit rows when the caller is the notification's recipient
- returns an empty list when the caller is not the notification's recipient
- returns an empty list (not an error) when the caller supplies a missing notificationId
- returns rows for system-driven events (QUEUED, SENT, DELIVERED, FAILED) with `occurredBy == system`
- returns rows for user-driven events (SEEN, READ, ARCHIVED) with `occurredBy == recipientUserId`
- returns FAILED rows with populated `errorClass` and `errorDetail`
- returns anonymized rows with `errorDetail` cleared and `occurredBy = TOMBSTONED` while preserving structural fields
- honors limit/offset pagination and reports hasNextPage when more rows remain
