# 12 — Notifications: bookings, offer subscriptions, appointment reminders

## Build status (2026-09-26)

All tracks A–H are built with the recommended decisions D1–D8. Deviations from the text below:
- D4: bookings store no payment method, so "pay-at-clinic pending" is implemented as
  *pending bookings created by staff* (`created_by` set); pending bookings made online are unpaid and not reminded.
- Phone numbers are normalized when sending (`App\Support\PhoneNumber::toE164()`), no `phone_e164` column.
- In-app notifications are written synchronously (`viaConnections`), so the bells work even without a queue worker;
  push and WhatsApp/SMS are queued.
- Clinic Admin and Receptionist were missing `system.notifications.view/update` (no bell at all); granted in the seeder
  and by migration `2026_09_26_110000_grant_notification_bell_to_clinic_roles`.
- Website web push (service worker) is not built; mobile push is.

Goal (from the request):

1. After **any booking** (website, mobile API, or admin dashboard) the **doctor** and the **admins** get a notification.
2. When a visitor **subscribes to an offer**, the **admins** get a notification.
3. Before a **visit**, a **follow-up (إعادة الكشف)** or an **online consultation**, the **patient** gets a reminder,
   in-app/push, and by **WhatsApp or SMS** when that integration is active in the admin.

---

## 1. What exists today

| Piece | Where | State |
|---|---|---|
| In-app notifications (Laravel `notifications` table) | `App\Services\System\NotificationService` (`sendToNotifiable`, `sendToAdmins`) | Works. Data shape: `{title, message, url, …}` |
| Admin bell dropdown | `dashboard/admin/system/notifications/dropdown.blade.php`, route `admin.dashboard.system.notifications.*` | Works, **only refreshes on page load** |
| Doctor bell + list | `dashboard/doctor/notifications/*`, `routes/doctor.php` | Works |
| Patient notifications API | `Api\V1\NotificationController` (`/api/v1/notifications`) | Works (list, unread count, mark read) |
| Patient notifications on the website | none | **Missing**, no bell in the site header |
| Booking notifications | `BookingService::notifyDoctorOfConfirmedBooking()` | **Doctor only, only on confirm** (staff confirm or online payment success). No admin notification, nothing on create/cancel/reschedule |
| Booking entry points | `Site\Clinic\BookingController`, `Api\V1\Clinic\BookingController`, `Admin\Clinic\BookingController` → all call `BookingService::create()`; follow-ups are booked through the admin form with `type=followup` | Single choke point, good |
| Offer subscriptions | `MainController::subscribeOffer()` → `site_offer_subscriptions` | No notification, only the sidebar counter |
| SMS / WhatsApp / push settings | `IntegrationProvider` (Settings ▸ Integrations): `twilio`, `msegat` (SMS); `meta_cloud_api`, `twilio_whatsapp` (WhatsApp); `firebase_fcm` (push). One active provider per category. Config is encrypted | **Config only, no sending code anywhere** |
| Device tokens for push | none | **Missing** |
| Scheduler | `routes/console.php` has no scheduled tasks | **Missing** |
| Queue | `QUEUE_CONNECTION=database` | Available, but a worker must run on the server |
| Patient phone | `users.phone` (free text) | Needs normalizing to international format (E.164, e.g. `+2010…`) for SMS/WhatsApp |
| Patient language | not stored | **Missing**, needed to send AR or EN text |

---

## 2. Events × recipients × channels

| # | Event | Doctor | Admins | Patient |
|---|---|---|---|---|
| E1 | Booking created (any source) | in-app | in-app (+ live toast) | in-app + push ("booking received") |
| E2 | Booking confirmed (staff confirm / payment success) | in-app (exists, reuse) | in-app | in-app + push (+ WhatsApp/SMS if enabled) |
| E3 | Booking cancelled (patient or staff) | in-app | in-app | in-app + push (if cancelled by staff) |
| E4 | Booking rescheduled | in-app | in-app | in-app + push |
| E5 | Offer subscription received | — | in-app (+ live toast) | — |
| E6 | Appointment reminder (visit / follow-up / online) | optional daily agenda (phase 2) | — | in-app + push + **WhatsApp or SMS** |

Each channel can be switched on/off per event from the admin (section 6). The table above is the default.

**Who counts as "admins"** (recommended, see decision D2):
- Admins with `branch_id = null` (Super Admin level) who have `clinic.bookings.view`, **plus**
- Admins of the booking's branch (`admins.branch_id = booking.branch_id`) who have `clinic.bookings.view`
  (Clinic Admin, Receptionist).
- For E5: admins with `clinic.offers.view`; if the offer belongs to a branch, only that branch's admins + global admins.

---

## 3. Architecture

Use **Laravel Notification classes** (one per event) instead of more ad-hoc `sendToNotifiable()` calls:
each class decides its channels in `via()`, is **queued** (`ShouldQueue`, `afterCommit()`), and keeps the
existing database payload shape (`title`, `message`, `url`) so the three existing bells keep working unchanged.

```
app/Notifications/Clinic/
    BookingCreated.php          (E1)   → doctor, admins, patient
    BookingConfirmed.php        (E2)   → replaces notifyDoctorOfConfirmedBooking()
    BookingCancelled.php        (E3)
    BookingRescheduled.php      (E4)
    AppointmentReminder.php     (E6)
app/Notifications/Site/
    OfferSubscriptionReceived.php (E5)

app/Notifications/Channels/
    FcmChannel.php        → Firebase Cloud Messaging HTTP v1
    SmsChannel.php        → active SMS provider (Twilio or Msegat)
    WhatsAppChannel.php   → active WhatsApp provider (Meta Cloud API or Twilio WhatsApp)

app/Services/Messaging/
    SmsSender (interface) + TwilioSms, MsegatSms
    WhatsAppSender (interface) + MetaCloudWhatsApp, TwilioWhatsApp
    FcmSender  (OAuth token from the service-account JSON, cached ~55 min)
    MessagingManager  → resolves the active IntegrationProvider per category, or null (channel skipped)

app/Services/Clinic/ClinicNotifier.php
    bookingCreated(Booking), bookingConfirmed(Booking), bookingCancelled(Booking, by), bookingRescheduled(old, new)
    offerSubscribed(SiteOfferSubscription)
    → resolves recipients (section 2), reads the event/channel switches (section 6), dispatches the notifications
```

Hook points (one line each, no controller changes):
- `BookingService::create()` → `ClinicNotifier::bookingCreated()` **after the transaction commits**.
- `BookingService::confirmByStaff()` and `BookingPaymentService` payment success → `bookingConfirmed()`
  (removes the old `notifyDoctorOfConfirmedBooking()`).
- `BookingService::cancel()` / `cancelByStaff()` / `reschedule()` → `bookingCancelled()` / `bookingRescheduled()`.
- `MainController::subscribeOffer()` → `offerSubscribed()`.

Rules:
- A failing SMS/WhatsApp/push call must **never** fail the booking (same rule as chat broadcasting today):
  channels catch provider errors, log them, and mark the delivery failed.
- No active provider for a channel → that channel is silently skipped (and logged as `skipped`).
- Missing/invalid patient phone → SMS/WhatsApp skipped, logged.

---

## 4. Appointment reminders (E6)

Scheduled command `clinic:send-appointment-reminders`, every 5 minutes (`routes/console.php`,
`->everyFiveMinutes()->withoutOverlapping()`).

- Reminder offsets come from settings, default **24 hours** and **2 hours** before `scheduled_at`
  (decision D3). Each offset is a separate reminder.
- Picks bookings with `status in (confirmed, pending*)`, `type in (visit, followup, online)`,
  `scheduled_at` inside `[now + offset − 5 min, now + offset]`.
  (*pending only for pay-at-clinic bookings; see decision D4.)
- **Idempotent**: new table `clinic_booking_reminders (booking_id, offset_minutes, sent_at)`, unique on
  `(booking_id, offset_minutes)`. A reminder is recorded before dispatch, so a rerun or overlap never double-sends.
- Cancelled, completed or rescheduled-away bookings are skipped automatically (status filter).
  A rescheduled booking is a new row, so it gets its own reminders.
- If the server was down and a window was missed, the command also sends a reminder that is late by up to
  1 hour, never older (no "your appointment was 3 hours ago" messages).
- Text differs per type:
  - visit: "Reminder: your visit with Dr. X at {clinic}, {branch} on {date} at {time}."
  - followup: "Reminder: your follow-up visit (إعادة الكشف) with Dr. X …"
  - online: "Reminder: your online consultation with Dr. X at {time}. Open the app/website to join."
    (the video call itself is still out of scope; the link opens the booking page)
- Optional (phase 2): doctor daily agenda at 8:00 with today's bookings.

---

## 5. Data model changes

| Migration | Purpose |
|---|---|
| `users.locale` (`string(5)`, default `ar`) | Language for SMS/WhatsApp/push text. Set on register/login from the site locale, and from `Accept-Language` in the API |
| `users.phone_e164` (nullable), or normalize `users.phone` on save | SMS/WhatsApp need `+20…`. A `PhoneNumber::toE164($raw, 'EG')` helper handles `01…` → `+201…` |
| `user_devices` (`user_id`, `platform` android/ios/web, `token`, `last_used_at`), unique `token` | FCM push targets. Invalid tokens (FCM `UNREGISTERED`) are deleted automatically |
| `clinic_booking_reminders` (`booking_id`, `offset_minutes`, `sent_at`), unique pair | Reminder idempotency |
| `notification_deliveries` (`notifiable`, `notification_type`, `channel`, `provider`, `to`, `status` sent/failed/skipped, `error`, `provider_message_id`, `created_at`) | Delivery log for SMS/WhatsApp/push: debugging + an admin log screen + cost tracking |

---

## 6. Admin: Settings ▸ Notifications (new screen)

Stored with the existing `SettingService` keys (`notifications.*`), one screen, permission `core.settings.update`:

1. **Events**: a matrix of event × channel switches (in-app / push / SMS / WhatsApp) with the defaults from section 2.
2. **Reminders**: on/off, offsets (repeater: 24h, 2h …), which booking types, SMS vs WhatsApp preference
   ("WhatsApp, fall back to SMS if it fails" / "SMS only" / "WhatsApp only").
3. **Message texts**: AR + EN text per event for SMS (and push), with placeholders
   `:patient`, `:doctor`, `:clinic`, `:branch`, `:date`, `:time`, `:type`. Placeholder check like the translation editor.
4. **WhatsApp templates** (Meta Cloud API): template name + language per event (see D5).
5. **Test send**: enter a phone number → sends a test SMS/WhatsApp through the active provider and shows the result.
6. **Delivery log**: a datatable over `notification_deliveries` (filter by channel/status/date).

Provider credentials stay in Settings ▸ Integrations (already built). The Notifications screen shows which
provider is active per channel and links to it.

---

## 7. Mobile API additions

- `POST /api/v1/devices` `{token, platform}` (auth) → upsert into `user_devices`.
- `DELETE /api/v1/devices/{token}` (auth, and called automatically on logout).
- Push payload: `notification {title, body}` + `data {type: booking_created|reminder|…, booking_id, url}` for deep links.
- `PATCH /api/v1/profile/locale` (or reuse profile update) to store the app language.
- Postman collection + response examples updated.

## 8. Website additions

- Patient bell in the site header (logged-in patients): unread count + dropdown + a "My notifications" page,
  reusing `NotificationService::getAllFor()/markAsReadFor()`.
- Optional: web push through FCM for the website (needs the web fields already in the Firebase config +
  a service worker). Recommended for a later phase.

## 9. Admin live updates

The admin bell only refreshes on page reload today. Two options (decision D6):
- **Polling (recommended)**: a small JSON endpoint (`unread count + latest 5`) polled every 60 s, with a toast
  for new items. Works on shared hosting with no extra services.
- **Broadcast**: reuse the existing Reverb/Pusher setup (`private-admin.{id}` channel). Instant, but needs Reverb
  running or a paid Pusher app in production.

---

## 10. Server requirements (production)

- Cron: `* * * * * php /path/artisan schedule:run` (reminders).
- Queue worker for the queued notifications. On cPanel: a cron line
  `* * * * * php /path/artisan queue:work --stop-when-empty --max-time=55` (or Supervisor if available).
- SMS: an approved sender name (Msegat/Twilio). WhatsApp: a Meta Business account, a verified number and
  **approved message templates** (Meta only allows template messages for business-initiated messages such as
  reminders).
- Firebase: service account JSON + the mobile app registering tokens.

---

## 11. Build order (one commit per track)

- **A. Foundation**: migrations (§5), `PhoneNumber` helper, `users.locale`, `MessagingManager` + provider senders
  (Twilio SMS, Msegat SMS, Meta WhatsApp, Twilio WhatsApp, FCM) with HTTP fakes in tests, `notification_deliveries` logging.
- **B. Booking events (E1–E4)**: notification classes, `ClinicNotifier`, hooks in `BookingService`/`BookingPaymentService`,
  admin recipient resolution with branch scoping. Replaces `notifyDoctorOfConfirmedBooking()`.
- **C. Offer subscription (E5)**: notification + recipients.
- **D. Reminders (E6)**: `clinic_booking_reminders`, scheduled command, texts per type, SMS/WhatsApp fallback.
- **E. Settings ▸ Notifications screen**: event matrix, reminder offsets, texts, WhatsApp templates, test send, delivery log.
- **F. Mobile API**: device tokens, locale, push payloads, Postman update.
- **G. Website + admin UI**: patient bell/page on the site; admin bell polling + toasts (or broadcast).
- **H. Docs + deploy notes**: cron/queue setup, provider onboarding checklist.

## 12. Tests

- Booking created from **each** entry point (site, API, admin) notifies the doctor, the branch admins and global
  admins, and **not** admins of another branch.
- Confirm / cancel / reschedule notify the right people once.
- Offer subscription notifies offer-branch admins + global admins.
- Reminder command: sends once per offset (running it twice sends nothing new), skips cancelled/completed,
  respects each booking type, uses the patient's language.
- SMS/WhatsApp: `Http::fake()` for each provider; no active provider → skipped + logged; provider error → booking
  still created, delivery logged as failed; WhatsApp failure falls back to SMS when configured.
- FCM: token registration, `UNREGISTERED` token cleanup.
- Phone normalization (`01012345678`, `+20 101 234 5678`, `0020…`).

---

## 13. Decisions needed before building

| # | Question | Recommendation |
|---|---|---|
| D1 | Notify when a booking is **created** (still pending payment) or only when **confirmed**? | Both: E1 on create (doctor + admins see new demand), E2 on confirm |
| D2 | Which admins receive booking notifications? | Global admins + the booking branch's Clinic Admin **and** Receptionist (anyone with `clinic.bookings.view` in that branch) |
| D3 | Reminder times? | 24 h and 2 h before, editable in the settings screen |
| D4 | Remind unpaid (`pending`) bookings too? | Only pay-at-clinic pending bookings; unpaid online bookings get no reminder |
| D5 | WhatsApp provider in production: Meta Cloud API (needs approved templates) or Twilio WhatsApp? | Meta Cloud API (cheaper, official); the client must create/approve the templates in Meta Business Manager |
| D6 | Admin live updates: polling or broadcast? | Polling every 60 s (no extra server process) |
| D7 | FCM auth: add `google/auth` package or sign the OAuth JWT ourselves with `openssl`? | `google/auth` (small, maintained, handles token caching correctly) |
| D8 | SMS/WhatsApp also for E2/E3 (confirmation/cancellation), or reminders only? | Reminders + confirmation by default; the rest switchable in the settings screen |
