> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bbrands.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> The notification dispatcher, the unified queue consumer, per-channel handlers and the queue payload contract.

## Components

| Component                   | Path                                            |
| --------------------------- | ----------------------------------------------- |
| OneSignal client (REST v11) | `lib/integration/onesignal/onesignal.client.ts` |
| Dispatcher (enqueue)        | `lib/notification/notification-dispatcher.ts`   |
| Registry `(channel,type)`   | `lib/notification/notification-registry.ts`     |
| Targeting (`external_id`)   | `lib/notification/notification-targeting.ts`    |
| Unified consumer route      | `app/api/queue/notification/route.ts`           |
| Per-channel handlers        | `lib/queue/notification/handlers/*.handler.ts`  |

## Queue payload contract

Every message on the `notification` topic satisfies `NotificationQueuePayload`:

```jsonc theme={null}
{
  "category": "payment",       // marketing | payment | system | transactional
  "channel": "push",           // in_app | live_activity | push | sms
  "correlation": "corr-uuid",  // producer → queue → handler traceability
  "data": { "debtId": "…" },   // channel/type-specific content
  "queue_event_id": "uuid",    // idempotency key
  "type": "PAYMENT_FAILED",    // registry type
  "user": "auth-user-uuid"      // target; used as OneSignal external_id
}
```

The consumer uses `channel` to segment `queue_event.event_type`
(`vercel_queue.notification.<channel>`) and `type` to pick the handler.

## Dispatcher usage (producers)

```ts theme={null}
import { notificationDispatcher } from "@/lib/notification/notification-dispatcher";

// Best-effort enqueue: gating + send.
await notificationDispatcher.enqueue({
  category: "payment",
  channel: "push",
  correlation,
  data: { amount, debtId },
  type: "PAYMENT_FAILED",
  user: authUserId,
});

// Durable enqueue (outbox): persists a replayable queue_event before the send.
await notificationDispatcher.enqueueDurable({ /* same params */ });
```

<Note>
  Use `enqueueDurable` for critical notifications
  (`payment` / `system` / `transactional`) and `enqueue` for the rest. Bulk
  sends use `enqueueBulk` with per-batch throttling.
</Note>

## First real producer

The failed-payment push (`PAYMENT_FAILED`) is wired end-to-end:
`notifyPaymentFailed` resolves the account members and enqueues one `payment`
push per user when a Toku `transaction.failed` webhook records a failed attempt.
Because `payment` is non-suppressible, preference gating never discards it, and
any enqueue failure is logged without breaking the webhook.

## Adding a new type

<Steps>
  <Step title="Register the type">
    Add it to `NOTIFICATION_TYPES` in `notification-registry.ts`.
  </Step>

  <Step title="Define its data">
    Extend `notification-queue-types.ts` if the type needs its own fields.
  </Step>

  <Step title="Optional override">
    Register a `(channel, type)` handler for type-specific logic; otherwise the
    default channel handler is used.
  </Step>

  <Step title="Produce">
    Call `notificationDispatcher.enqueue(...)` from the business service.
  </Step>
</Steps>

No change to the consumer route or `vercel.json` is needed for a new type
within an already-supported channel.
