# Business Domain — Notifications

> The multi-channel, multi-source notification engine.

_Verified against `app/Models/{Automation,AdminNotification,NotificationLog}.php` and `app/Services/Notifications/*` on 2026-07-23._

---

## Three notification sources
All implement `ProvidesNotificationChannels` and feed a shared channel dispatch layer:

1. **`Automation`** (`automations`) — trigger-keyed rules (e.g. `registered`, `reminder`, `committee_created`, `demo_reminder`, `demo_confirmation`) scoped by audience (investor/startup) and type. hasMany `AutomationDispatch`.
2. **`LabelOption` actions** — an `action` option can carry per-channel notification content (email/sms/whatsapp/in-app) fired when the action is applied.
3. **`AdminNotification`** — Notification Center admin broadcasts; belongsToMany target Groups; resolves the live audience of active investors. Lifecycle: draft → scheduled → sending → sent.

## Channels
Email, SMS, WhatsApp, push, in-app (`app/Services/Notifications/` — `Sms`, `WhatsApp`, `Push`, `InApp`, `Pending`, SendPulse). WhatsApp + OTP go through **SendPulse** (see [`../04-integrations/external-services.md`](../04-integrations/external-services.md)). SMS provider: **Unknown - requires confirmation** (`config/sms.php`).

## Delivery tracking & reliability
- **`NotificationLog`** — one row per delivery attempt; morphTo `recipient`; retry ladder; provider-webhook status transitions `sent → delivered → read` / `undelivered` / `failed`.
- **Idempotency:** `event_id` groups a multi-channel send; `idempotency_key` = `entity:recipient:channel:message_type`.
- **OTP:** one engine (`MeetingBookingOtpService`) issues a single code (SHA-256 in `meeting_otp_codes`, one active row per phone) and delivers it over **SMS and a new independent email channel off the SAME code** — an SMS "sent"/"processing" status is not treated as receipt, so email is a genuine redundant path, not a fallback. Email failure never blocks SMS and SMS failure never blocks email; a request is `sent` when either channel delivers, and only a total failure returns `sent=false`. Email (`App\Mail\OtpVerificationMail`, bilingual AR/EN, `lang/{ar,en}/otp.php`) is sent **synchronously (NOT `ShouldQueue`)** so the code lands immediately without depending on a queue worker and the plaintext is never serialized into the jobs table; recipient email is resolved from the SAME investor/startup that owns the phone, and email is **best-effort with no audit row of its own** (send failures are caught; on failure only masked, safe metadata is logged). The **SMS audit (`OtpLog`) and WhatsApp mirror are unchanged** from before the email channel — WhatsApp is still attempted only after a successful SMS, and `otp_logs` stays SMS-scoped (no email destination/status/channel metadata). The plaintext code is never logged or returned. Audit via `OtpLog`, `app/Services/Otp/`.
- **Test-recipient override:** when `NOTIFICATIONS_TEST_EMAIL` / `NOTIFICATIONS_TEST_PHONE` are set, all dispatch (incl. OTP) reroutes to the override, preserving the original target in `context.original_address`. Intended for local/staging only.

## Admin surfaces
Notification Center (compose/target/send-now/schedule; a scanner claims due broadcasts), Automations manager, Notification Templates, Notification Logs (retry/test), Notification Preview.
