# RecordDeliveryEvent

## Permission Scope

auditLogWrite

## Overview

recordDeliveryEvent is the async ingestion path that channel adapters call back into when a provider webhook reports a downstream lifecycle event — `DELIVERED` (provider acknowledgement), `BOUNCED` (hard bounce / undeliverable), or `OPENED` (email tracking pixel). The command writes one `NotificationDeliveryAudit` row referencing the parent Notification and updates `Notification.deliveryStatus` when the event implies a state transition (`DELIVERED` and `BOUNCED` advance the status; `OPENED` is observability-only and does not promote either lifecycle axis). The caller must be an authorized adapter context — end users and emitters cannot invoke this path.

**Reserved.** The dispatcher does not consume email-provider webhooks: EMAIL `deliveryStatus` is terminal at `SENT` (provider success) or `FAILED` (provider error), and the EMAIL adapter never invokes `recordDeliveryEvent`. The contract surface is documented here so provider-webhook ingestion can be wired without re-shaping the audit feature. `DELIVERED`, `BOUNCED`, and `OPENED` audit rows for the EMAIL channel are not produced; investigators correlate provider-side delivery state through the provider's dashboards using `Notification.adapterMessageId`.

## Business Rules

- `notificationId` is required and must reference an existing Notification row
- `eventType` is required and must be one of `DELIVERED`, `BOUNCED`, or `OPENED`
- `occurredAt` is required (provider event timestamp)
- `errorClass` and `errorDetail` are required for `BOUNCED`; sanitized at the writer boundary (no PII / credentials / API keys)
- Authorization is enforced by the command's permission gate on `notification:auditLogWrite` (or the command-level `notification:auditLogWrite:recordDeliveryEvent`; the channel-adapter context); the command body assumes an authorized caller
- `DELIVERED` advances `Notification.deliveryStatus` from `SENT` to `DELIVERED` (no other prior status is advanced)
- `BOUNCED` advances `Notification.deliveryStatus` to `BOUNCED` **only when the prior status is `SENT` or `DELIVERED`**; out-of-lifecycle statuses (`QUEUED`, `FAILED`, `BOUNCED`) keep their status — the audit row is written regardless
- `OPENED` writes the audit row only and does not advance `deliveryStatus` or `engagementStatuses`
- This command is reserved on the contract; the dispatcher does not invoke it for any production flow
- Writes are append-only; the row is never overwritten

## Process Flow

```mermaid
flowchart TD
    A[Channel adapter receives provider webhook] --> B[Sanitize errorDetail, build event payload]
    B --> C[Call recordDeliveryEvent]
    C --> F{eventType IN DELIVERED, BOUNCED, OPENED?}
    F -->|No| G[Return INVALID_EVENT_TYPE]
    F -->|Yes| H{Notification exists?}
    H -->|No| I[Return NOTIFICATION_NOT_FOUND]
    H -->|Yes| J[Insert NotificationDeliveryAudit row with occurredBy=system]
    J --> K{eventType is DELIVERED or BOUNCED?}
    K -->|Yes, DELIVERED| L[Update Notification.deliveryStatus = DELIVERED if currently SENT]
    K -->|Yes, BOUNCED| M[Update Notification.deliveryStatus = BOUNCED if currently SENT or DELIVERED]
    K -->|No, OPENED| N[Skip Notification update]
    L --> O[Return success]
    M --> O
    N --> O
```

## External Dependencies

- Channel adapter authorization context - The caller must be an authenticated channel adapter; end-user and emitter contexts are rejected

## Error Scenarios

- **NOTIFICATION_NOT_FOUND**: No Notification matches the supplied `notificationId`
- **INVALID_EVENT_TYPE**: `eventType` is not one of `DELIVERED`, `BOUNCED`, or `OPENED`
- **MISSING_REQUIRED_FIELD**: One or more required input fields are missing or blank

## Test Cases

- inserts a NotificationDeliveryAudit row with eventType=DELIVERED and advances Notification.deliveryStatus to DELIVERED
- inserts a NotificationDeliveryAudit row with eventType=BOUNCED, populates errorClass / errorDetail, and advances Notification.deliveryStatus to BOUNCED
- advances deliveryStatus to BOUNCED when the prior status is DELIVERED
- inserts a NotificationDeliveryAudit row with eventType=OPENED and leaves Notification.deliveryStatus unchanged
- returns NOTIFICATION_NOT_FOUND when notificationId does not match any row
- returns INVALID_EVENT_TYPE when eventType is not DELIVERED / BOUNCED / OPENED
- returns MISSING_REQUIRED_FIELD when occurredAt is omitted
- returns MISSING_REQUIRED_FIELD when BOUNCED omits errorClass or errorDetail
- the audit row carries occurredBy=system for adapter-driven async events
- the EMAIL adapter does not invoke this path in any production flow (documented in test)
- accepts BOUNCED arriving for a Notification whose earlier audit rows have been anonymized by the retention sweep
- writes are append-only; the audit row is never overwritten by subsequent calls
