# ArchiveNotification

## Permission Scope

inbox

## Overview

archiveNotification adds `ARCHIVED` to a Notification's `engagementStatuses` set and stamps `archivedAt`, removing the row from the default inbox feed and from the unread count. The verb `archive` here is the recipient-side **engagement signal** — "I'm done with this, hide it from my active feed" — matching the inbox conventions in Knock, SAP, and Odoo. It is **not** an entity-lifecycle deactivation; the Notification row remains intact, queryable via an explicit archived filter, and continues to serve the audit trail. The command is recipient-only and idempotent: a row already containing `ARCHIVED` is returned unchanged with `archivedAt` preserved. Operator-owned `deliveryStatus` values (`QUEUED`, `FAILED`, `BOUNCED`) are not user-actionable — those rows never appear in the inbox surface and cannot be archived (`NOT_DELIVERED`), mirroring the gate on `markNotificationAsRead`. Archiving does not auto-add `READ`, but the row is excluded from `unreadCount` because the unread predicate also excludes ARCHIVED.

> **Naming-convention note.** The platform-wide command-naming guide flags `archive` / `unarchive` as banned verbs to prevent their use as a soft-delete substitute for `deactivate`. This command is a deliberate, narrowly-scoped exception: `ARCHIVED` is a domain-modeled value in the Notification's `engagementStatuses` set (not an entity-lifecycle status), and the verb is the established industry term for the recipient-side inbox signal. The exception applies only to engagement-axis additions on Notification — soft-disable use of `archive` elsewhere in the module remains banned.

## Business Rules

- `notificationId` is required
- The Notification must exist and be owned by the caller; a missing row and a row owned by another user are both reported as `NOTIFICATION_NOT_FOUND` so callers cannot enumerate foreign notification ids (no existence oracle)
- `deliveryStatus` must be `SENT` or `DELIVERED`; rows in `QUEUED`, `FAILED`, or `BOUNCED` are not user-actionable and are rejected with `NOT_DELIVERED`
- Idempotent: if `ARCHIVED` is already in `engagementStatuses`, return the row unchanged with `archivedAt` preserved
- Adds `ARCHIVED` to `engagementStatuses` (does not auto-add `READ`)
- Stamps `archivedAt` only on the first transition to ARCHIVED
- Writes a `NotificationDeliveryAudit` row with `eventType=ARCHIVED`, `occurredBy=recipientUserId`
- Engagement-axis additions are monotonic: `archivedAt` is never overwritten

## Process Flow

```mermaid
flowchart TD
    A[Receive archive request] --> B{Notification exists AND recipientUserId == caller.userId?}
    B -->|No, missing or foreign-owned| C[Return NOTIFICATION_NOT_FOUND]
    B -->|Yes| D{deliveryStatus IN SENT, DELIVERED?}
    D -->|No| E[Return NOT_DELIVERED]
    D -->|Yes| F{ARCHIVED already in engagementStatuses?}
    F -->|Yes| G[Return existing row, idempotent no-op]
    F -->|No| H[Add ARCHIVED to engagementStatuses, stamp archivedAt]
    H --> I[Write audit row eventType=ARCHIVED, occurredBy=recipientUserId]
    I --> J[Return updated Notification, removed from default feed]
```

## External Dependencies

- [notification-delivery-audit](../feature/notification-delivery-audit.md) - Writes an `ARCHIVED` audit row inline as part of the engagement-axis transition

## Error Scenarios

- **NOTIFICATION_NOT_FOUND**: No Notification matches the supplied `notificationId`
- **NOT_DELIVERED**: Notification's `deliveryStatus` is one of `QUEUED`, `FAILED`, `BOUNCED` and is not user-actionable

## Test Cases

- adds ARCHIVED to engagementStatuses on a caller-owned Notification and stamps archivedAt
- does not auto-add READ when archiving an unread Notification
- adds ARCHIVED to a Notification already containing READ, resulting in engagementStatuses of {SEEN, READ, ARCHIVED}
- returns the existing row unchanged when ARCHIVED is already present (idempotent), without re-stamping archivedAt
- returns NOTIFICATION_NOT_FOUND when the Notification's recipientUserId is a different user (no existence oracle)
- returns NOTIFICATION_NOT_FOUND when notificationId does not match any row
- returns NOT_DELIVERED when deliveryStatus is QUEUED
- returns NOT_DELIVERED when deliveryStatus is FAILED
- returns NOT_DELIVERED when deliveryStatus is BOUNCED
- excludes the row from the default inbox feed after archiving
- excludes the row from unreadCount after archiving regardless of READ presence
- writes a NotificationDeliveryAudit row with eventType=ARCHIVED and occurredBy=recipientUserId on the first transition
