Skip to main content

Spec: WhatsApp Phone Number on Patient Profile

Date: 2026-04-28
Status: Approved

Summary

Add a dedicated whatsapp_phone field to each patient record so outbound WhatsApp appointment reminders go to the correct number. The feature is only visible to clinics with the whatsapp_api addon enabled. Both phone fields get a country-flag + dial-code picker using react-international-phone.

Addon Gate

The entire WhatsApp phone UI is hidden unless the clinic has the whatsapp_api module enabled.
  • If the addon is off, the patient form shows only the standard phone field (unchanged).
  • If the addon is on, the “Also on WhatsApp” toggle and the separate WhatsApp number field become visible.
  • Gate is enforced in the frontend via the existing useClinicModules / module-check pattern. No server-side enforcement needed beyond existing permission checks.

Database

New nullable column on patients:
ALTER TABLE patients ADD COLUMN whatsapp_phone text;
  • whatsapp_phone — nullable text, E.164 format, encrypted at rest using the same AES-256-GCM path as phone.
  • Existing rows are unaffected; whatsapp_phone = NULL means no WhatsApp number on file.
  • No migration needed for existing patients — WhatsApp sends are silently skipped when NULL.

Phone Input Component

Library: react-international-phone
  • Flag picker + searchable country dropdown (all countries).
  • Stores value in E.164 (+971501234567).
  • Lightweight, TypeScript, styleable with CSS variables.
  • Applied to both the primary phone field and the WhatsApp number field.
  • Dark-theme styling to match existing inputs (border #333, background #1a1a1a, text #e0e0e0).
A shared PhoneInput wrapper component will be created at ui/src/components/ui/PhoneInput.tsx to apply dark-theme CSS overrides and consistent props to both fields.

Patient Form Behaviour (addon ON)

Primary phone field

  • Replaced with <PhoneInput> (flag + E.164 input).
  • Still required (NOT NULL).
  • Next to the field: a green “Also on WhatsApp” badge toggle.
    • Default state: ON (green, checked) for new patients — most patients share one number.
    • When ON: whatsapp_phone is saved as the same value as phone. No second field shown.
    • When toggled OFF: the badge goes grey and a second WhatsApp field appears below.

WhatsApp number field (shown only when toggle is OFF)

  • Same <PhoneInput> component.
  • Label: WhatsApp icon + “Different WhatsApp Number (optional)”.
  • Optional — may be left blank. If blank and toggle is OFF, whatsapp_phone is saved as NULL.
  • Placeholder: “Only fill if different from above”.

Edit mode (existing patients)

  • If whatsapp_phone === phone → toggle starts ON, no second field.
  • If whatsapp_phone is a different number → toggle starts OFF, second field pre-filled.
  • If whatsapp_phone === NULL → toggle starts OFF, second field empty.

Patient Detail View

  • Phone row: existing, unchanged.
  • WhatsApp row: shown only if whatsapp_phone is non-null AND clinic has addon enabled.
    • Label: WhatsApp SVG icon + “WhatsApp”.
    • Value: formatted number (display as-is from DB — already E.164).
    • No wa.me link — plain text only.
    • If whatsapp_phone is null → row hidden entirely.

Backend: WhatsApp Send Logic

Appointment reminders (appointment-reminders.ts)

if (patient.whatsappPhone) → send to whatsappPhone
else → skip (no WhatsApp send, no fallback to phone)
No fallback. Silence is correct when the number is unknown.

Incoming webhook cancel flow (whatsapp-webhook.ts)

Match incoming message sender in this order:
  1. patients.whatsapp_phone first
  2. patients.phone second (patient may have texted in from their regular number)

Outbound messages route (messages.ts)

Same rule: use patient.whatsapp_phone if set, otherwise skip.

Decryption / API

decryptPatientPHI must include whatsapp_phone in the decrypted fields returned to the frontend (same encryption path as phone). The API response shape for patient records gains:
whatsappPhone: string | null

What Is NOT Changing

  • phone stays required — no migration for existing patients.
  • Patient list view unchanged.
  • No WhatsApp links anywhere in the UI.
  • Clinics without the whatsapp_api addon see no new UI at all — their patient form is identical to today.

Files Affected

FileChange
server/src/schema/patients.tsAdd whatsapp_phone nullable column
server/drizzle/New migration file
server/src/lib/encryption.ts (or PHI utils)Include whatsapp_phone in encrypt/decrypt
server/src/routes/patients.tsRead/write whatsapp_phone; return in API response
server/src/scheduled/appointment-reminders.tsUse whatsappPhone for WA sends
server/src/routes/whatsapp-webhook.tsMatch incoming by whatsapp_phone first
server/src/routes/messages.tsUse whatsappPhone for outbound
ui/src/components/ui/PhoneInput.tsxNew wrapper around react-international-phone
ui/src/components/patients/PatientForm.tsxToggle + conditional WhatsApp field; addon gate
ui/src/components/patients/PatientDetails.tsxWhatsApp row; hide if null; addon gate
ui/package.jsonAdd react-international-phone