# `opt-out` _(no submodules)_ ## Purpose `opt-out` is the in-memory opt-out registry that `SmsService` consults before every outbound message. Recipients on the registry receive no further messages — `service.send` throws `SmsRecipientOptedOutError` (code `SMS.RECIPIENT_OPTED_OUT`). Entries can be scoped to a channel so transactional messages remain deliverable when only marketing has been opted out by the host application. The registry is intentionally pluggable but ships as an in-process `Map` so unit tests and small deployments need no external state; production hosts that share opt-out across processes can wrap `OptOutRegistry` in their own persistence layer. ## Features - `add(phoneNumber, channel?)` — records the opt-out with a timestamp from an injectable `Clock`. - `remove(phoneNumber)` — opt-in helper. - `has(phoneNumber)` and `get(phoneNumber)` — predicate + detail accessors. - `list()` snapshot, `size()`, and `clear()` for bulk operations. - `importEntries(entries)` — bulk import with overwrite-by-id semantics for syncing from an external source. ## Usage: Basic ```ts import { OptOutRegistry } from "@suaveplan/sms/opt-out"; const reg = new OptOutRegistry(); reg.add("+15551234567", "marketing"); console.log(reg.has("+15551234567")); // true reg.remove("+15551234567"); ``` ## Usage: Advanced ```ts import { OptOutRegistry } from "@suaveplan/sms/opt-out"; // Inject a deterministic clock for tests; importEntries // reseeds the registry from a downstream cache. const reg = new OptOutRegistry({ clock: () => 1_700_000_000_000 }); reg.importEntries([ { phoneNumber: "+15550000001", optedOutAt: new Date(1_700_000_000_000), channel: "marketing", }, { phoneNumber: "+15550000002", optedOutAt: new Date(1_700_000_000_000), }, ]); console.log(reg.size()); // 2 ``` ## API Reference - `class OptOutRegistry` - `constructor(options?: { clock?: Clock })` - `add(phoneNumber: string, channel?: string): OptOutEntry` - `remove(phoneNumber: string): boolean` - `has(phoneNumber: string): boolean` - `get(phoneNumber: string): OptOutEntry | undefined` - `list(): ReadonlyArray` - `importEntries(entries: ReadonlyArray): void` - `clear(): void` - `size(): number` ## Implementation Notes The registry stores `OptOutEntry` records keyed by the verbatim phone number — no normalisation. Callers SHOULD pass E.164 strings; mismatched formats will fail to match. `add` overwrites existing entries by `phoneNumber`, so a caller switching the `channel` on an existing opt-out simply re-adds. The `clock` defaults to `Date.now`, deterministic only when explicitly injected.