# Communication history

Apply `php artisan migrate` when deploying the API. The admin sidebar has a Communications page; borrower profiles have a Communications section. Access follows `borrowers.read` and also requires an admin account.

History starts with this deployment. Existing notification deduplication rows and activity flags are not evidence of delivery and are not backfilled.

## Tracking a new message

Prefer `NotificationDispatcher::dispatch($user, $event, $context)`. It records the event, safe loan/mandate references and each selected channel independently. In-app rows retain their notification ID so the history can report the current read time.

For standalone email, use `CommunicationMail::to($email, $user)->send($mailable)` instead of `Mail::to()`. Pass the recipient model whenever known, because a borrower and an affiliate can share an email address. Without a model, ambiguous addresses are left unlinked. OTP bodies are suppressed, links and code values are redacted, and attachments are not retained. Snapshot rendering uses a clone of the mailable to avoid affecting delivery.

The existing SMS and FCM services also track direct sends. A bulk SMS produces one log per destination, and push to a user produces one log with an acceptance count when only some devices succeed. Device tokens are not retained. Campaigns add campaign references and track missing destinations as skipped. Nested tracking reuses the outer channel record to avoid duplicates.

For a new transport, call the scoped `CommunicationTracker::track($user, $channel, $destination, $subject, $message, $context, $send)` method. It returns the callback's result and rethrows its exceptions, preserving caller behavior. False means failed; an explicit result `['communication_status' => 'skipped', 'reason' => 'No destination']` records a skip. Optional `notification_id` associates an in-app record. The context allowlist stores references only. Audit database failures are logged and do not prevent sending.

## Status meanings

- Pending: an attempt started and no final outcome has been recorded yet; a crashed process can leave this status unresolved.
- Sent: the send call was accepted, or an in-app row was saved. This is not confirmation of device delivery, email delivery or opening.
- Failed: a send threw an exception or the provider rejected it. The sanitized reason is shown.
- Skipped: no usable destination, push disabled/unavailable, or mail sending cancelled.

External delivery, bounce and open events require provider receipts and are not inferred. In-app read times come directly from the inbox table.

Endpoints: `GET /api/v1/admin/communications` and `GET /api/v1/admin/users/{id}/communications`. Optional filters: `channel`, `status`, `event`, `search`, `from`, `to`, `page`, `per_page` (maximum 100).
