Skip to main content

WhatsApp Patient Phone Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Add a per-patient whatsapp_phone field used exclusively for outbound WhatsApp messaging, gated behind the whatsapp_api addon module. Architecture: Single nullable whatsapp_phone column on the patients table, encrypted via the existing AES-256-GCM PHI pipeline (encryptPatientPHI/decryptPatientPHI). Frontend shows a “Also on WhatsApp” toggle next to the phone field — only visible when the clinic has the whatsapp_api addon. All WA sends use whatsappPhone exclusively, no fallback to phone. Tech Stack: Drizzle ORM (schema push), react-international-phone (flag picker for both phone fields, all countries), Zod (validation schema), Hono routes, React state (toggle logic).

Task 1: Install react-international-phone

Files:
  • Modify: ui/package.json
  • Step 1: Install
cd /Users/ssh/Documents/Beta-App/odontoX/ui
npm install react-international-phone
Expected: added 1 package (or similar).
  • Step 2: Verify
ls node_modules/react-international-phone/package.json
Expected: file exists.
  • Step 3: Commit
git add ui/package.json ui/package-lock.json
git commit -m "build(ui): add react-international-phone"

Task 2: Create PhoneInput wrapper component

Files:
  • Create: ui/src/components/ui/PhoneInput.tsx
  • Step 1: Create the file
import { PhoneInput as BasePhoneInput } from 'react-international-phone';
import 'react-international-phone/style.css';

interface PhoneInputProps {
  value: string;
  onChange: (value: string) => void;
  placeholder?: string;
  required?: boolean;
  disabled?: boolean;
  defaultCountry?: string;
}

export function PhoneInput({
  value,
  onChange,
  placeholder,
  required,
  disabled,
  defaultCountry = 'ae',
}: PhoneInputProps) {
  return (
    <div
      style={{
        '--react-international-phone-background-color': '#1a1a1a',
        '--react-international-phone-text-color': '#e0e0e0',
        '--react-international-phone-border-color': '#333333',
        '--react-international-phone-selected-dropdown-item-background-color': '#2a2a2a',
        '--react-international-phone-dropdown-item-background-color': '#111111',
        '--react-international-phone-dropdown-item-text-color': '#e0e0e0',
        '--react-international-phone-placeholder-color': '#555555',
        '--react-international-phone-height': '38px',
        '--react-international-phone-font-size': '13px',
        '--react-international-phone-border-radius': '6px',
      } as React.CSSProperties}
    >
      <BasePhoneInput
        defaultCountry={defaultCountry}
        value={value}
        onChange={onChange}
        placeholder={placeholder}
        disabled={disabled}
        inputProps={{ required }}
      />
    </div>
  );
}
  • Step 2: Commit
git add ui/src/components/ui/PhoneInput.tsx
git commit -m "feat(ui): add PhoneInput wrapper with dark-theme CSS variables"

Task 3: Add whatsappPhone column to patients schema

Files:
  • Modify: server/src/schema/patients.ts
  • Step 1: Add the column
In server/src/schema/patients.ts, after line 15 (phone: text('phone').notNull()), insert:
  whatsappPhone: text('whatsapp_phone'),
The table block should now have phone on line 15 and whatsappPhone on the next line.
  • Step 2: Verify TypeScript
cd /Users/ssh/Documents/Beta-App/odontoX/server
npx tsc --noEmit 2>&1 | head -20
Expected: no new errors.
  • Step 3: Push schema to DB
npm run db:push
When prompted, confirm adding the whatsapp_phone column (y). This runs drizzle-kit push:pg and applies the ALTER TABLE to the Neon PostgreSQL database.
  • Step 4: Commit
cd /Users/ssh/Documents/Beta-App/odontoX
git add server/src/schema/patients.ts
git commit -m "feat(db): add nullable whatsapp_phone column to patients table"

Task 4: Add whatsappPhone to PHI encryption

Files:
  • Modify: server/src/lib/encryption.ts (line 177)
  • Step 1: Add field to PATIENT_PHI_FIELDS
In server/src/lib/encryption.ts, change line 177 from:
const PATIENT_PHI_FIELDS = ['phone', 'dateOfBirth', 'address', 'insurancePolicyNumber'] as const;
To:
const PATIENT_PHI_FIELDS = ['phone', 'whatsappPhone', 'dateOfBirth', 'address', 'insurancePolicyNumber'] as const;
Both encryptPatientPHI (line 183) and decryptPatientPHI (line 199) iterate PATIENT_PHI_FIELDS. The null-guard at line 186 (encrypted[field] != null && typeof encrypted[field] === 'string') means null whatsappPhone values are skipped automatically — no extra handling needed.
  • Step 2: Verify TypeScript
cd /Users/ssh/Documents/Beta-App/odontoX/server
npx tsc --noEmit 2>&1 | head -20
  • Step 3: Commit
git add server/src/lib/encryption.ts
git commit -m "feat(server): include whatsappPhone in patient PHI encrypt/decrypt pipeline"

Task 5: Add whatsappPhone to Zod validation schema and patient routes

Files:
  • Modify: server/src/lib/validation.ts (around line 44)
  • Modify: server/src/routes/patients.ts (lines 290, 303–316, 535–538)
  • Step 1: Update Zod schema
In server/src/lib/validation.ts, in patientBaseSchema (around line 44), add whatsappPhone after insurancePolicyNumber:
  insurancePolicyNumber: z.string().optional(),
  whatsappPhone: z.string().nullable().optional(),   // ← add this line
  status: z.enum(['active', 'inactive']).optional(),
Because patientUpdateSchema = patientBaseSchema.partial() (line 49), both create and edit inherit this field automatically.
  • Step 2: Update CREATE handler
In server/src/routes/patients.ts, around line 290, after:
const phone = body.phone ? normalizePhoneNumber(body.phone) : body.phone;
Add:
const whatsappPhone = body.whatsappPhone
  ? normalizePhoneNumber(body.whatsappPhone)
  : (body.whatsappPhone ?? null);
Then in the encryptPatientPHI({...}) call (lines 303–316), add whatsappPhone after phone:
const insertValues: any = encryptPatientPHI({
  id: crypto.randomUUID(),
  clinicId: currentClinicId,
  patientNumber,
  firstName: body.firstName,
  lastName: body.lastName,
  email: body.email,
  phone,
  whatsappPhone,            // ← add this line
  dateOfBirth,
  gender: body.gender as any,
  address: body.address,
  insuranceProvider: body.insuranceProvider,
  insurancePolicyNumber: body.insurancePolicyNumber,
  status: body.status || 'active',
});
  • Step 3: Update EDIT handler
In the PUT /:id handler (around line 535), change:
const normalizedBody = {
  ...body,
  ...(body.phone ? { phone: normalizePhoneNumber(body.phone) } : {}),
};
To:
const normalizedBody = {
  ...body,
  ...(body.phone ? { phone: normalizePhoneNumber(body.phone) } : {}),
  ...(body.whatsappPhone !== undefined
    ? { whatsappPhone: body.whatsappPhone ? normalizePhoneNumber(body.whatsappPhone) : null }
    : {}),
};
encryptPatientPHI(normalizedBody) (line 540) already receives and encrypts the normalized body — no further changes needed in the update path.
  • Step 4: Verify TypeScript
cd /Users/ssh/Documents/Beta-App/odontoX/server
npx tsc --noEmit 2>&1 | head -20
  • Step 5: Commit
git add server/src/lib/validation.ts server/src/routes/patients.ts
git commit -m "feat(server): accept and persist whatsappPhone in patient create/edit routes"

Task 6: Update PatientForm.tsx — phone picker + WhatsApp toggle

Files:
  • Modify: ui/src/components/patients/PatientForm.tsx
  • Step 1: Add imports
At the top of PatientForm.tsx, add after the existing import block:
import { PhoneInput } from '../ui/PhoneInput';
import { useModules } from '../providers/ModuleProvider';
  • Step 2: Add module check and WhatsApp state
Inside PatientForm component, after the existing useState calls (around line 48), add:
const { hasModule } = useModules();
const hasWhatsApp = hasModule('whatsapp_api');

const resolveInitialWhatsappSame = (data: any) =>
  !data?.whatsappPhone || data.whatsappPhone === data.phone;

const [whatsappSameAsPhone, setWhatsappSameAsPhone] = useState(
  resolveInitialWhatsappSame(initialData)
);
const [whatsappPhone, setWhatsappPhone] = useState(
  initialData?.whatsappPhone && initialData.whatsappPhone !== initialData.phone
    ? initialData.whatsappPhone
    : ''
);
  • Step 3: Update the useEffect reset
In the useEffect (around line 53), inside the if (initialData) branch, after setAccountCheckDone(true), add:
setWhatsappSameAsPhone(resolveInitialWhatsappSame(initialData));
setWhatsappPhone(
  initialData?.whatsappPhone && initialData.whatsappPhone !== initialData.phone
    ? initialData.whatsappPhone
    : ''
);
In the else branch (new patient reset), after setAccountExists(false), add:
setWhatsappSameAsPhone(true);
setWhatsappPhone('');
  • Step 4: Update handleSubmit to include whatsappPhone
In handleSubmit, find the onSubmit(formData) call (around line 151). Replace it with:
const resolvedWhatsappPhone = hasWhatsApp
  ? (whatsappSameAsPhone ? formData.phone : (whatsappPhone || null))
  : undefined;

onSubmit({
  ...formData,
  ...(hasWhatsApp ? { whatsappPhone: resolvedWhatsappPhone } : {}),
});
  • Step 5: Replace the phone Input with PhoneInput + toggle
Find the phone field block (around lines 221–232):
<Label htmlFor="phone" required>Phone</Label>
<Input
  id="phone"
  type="tel"
  value={formData.phone}
  onChange={(e) => updateField('phone', e.target.value)}
  placeholder="0300-1234567 or +92-300-1234567"
  required
/>
Replace with:
<Label htmlFor="phone" required>Phone</Label>
<div className="flex items-center gap-2">
  <div className="flex-1">
    <PhoneInput
      value={formData.phone}
      onChange={(val) => setFormData(prev => ({ ...prev, phone: val }))}
      required
    />
  </div>
  {hasWhatsApp && (
    <button
      type="button"
      onClick={() => setWhatsappSameAsPhone(prev => !prev)}
      className={`flex items-center gap-1.5 rounded-md border px-3 py-2 text-xs font-medium transition-colors whitespace-nowrap ${
        whatsappSameAsPhone
          ? 'border-[#25D366] bg-[#1a3320] text-[#25D366]'
          : 'border-border bg-muted text-muted-foreground'
      }`}
    >
      <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" className="shrink-0">
        <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z"/>
        <path d="M12 0C5.373 0 0 5.373 0 12c0 2.127.558 4.126 1.534 5.856L0 24l6.318-1.507A11.95 11.95 0 0012 24c6.627 0 12-5.373 12-12S18.627 0 12 0zm0 22c-1.867 0-3.617-.49-5.13-1.346l-.368-.214-3.75.895.944-3.639-.237-.385A9.955 9.955 0 012 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10z"/>
      </svg>
      {whatsappSameAsPhone ? 'Also on WhatsApp ✓' : 'Also on WhatsApp'}
    </button>
  )}
</div>
{hasWhatsApp && !whatsappSameAsPhone && (
  <div className="mt-2">
    <Label>
      <span className="flex items-center gap-1.5">
        <svg width="12" height="12" viewBox="0 0 24 24" fill="#25D366">
          <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z"/>
          <path d="M12 0C5.373 0 0 5.373 0 12c0 2.127.558 4.126 1.534 5.856L0 24l6.318-1.507A11.95 11.95 0 0012 24c6.627 0 12-5.373 12-12S18.627 0 12 0zm0 22c-1.867 0-3.617-.49-5.13-1.346l-.368-.214-3.75.895.944-3.639-.237-.385A9.955 9.955 0 012 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10z"/>
        </svg>
        Different WhatsApp Number
        <span className="text-muted-foreground font-normal">(optional)</span>
      </span>
    </Label>
    <PhoneInput
      value={whatsappPhone}
      onChange={setWhatsappPhone}
      placeholder="Only fill if different from above"
    />
  </div>
)}
  • Step 6: Build to verify
cd /Users/ssh/Documents/Beta-App/odontoX/ui
npm run build 2>&1 | tail -20
Expected: build succeeds, 0 TypeScript errors.
  • Step 7: Commit
git add ui/src/components/patients/PatientForm.tsx
git commit -m "feat(ui): replace phone Input with PhoneInput picker; add WhatsApp toggle (addon-gated)"

Task 7: Update PatientDetails.tsx — WhatsApp row

Files:
  • Modify: ui/src/components/patients/PatientDetails.tsx (around line 393)
  • Step 1: Confirm useModules is already imported
Line 58 already has const { hasModule } = useModules(); — no import changes needed.
  • Step 2: Add WhatsApp row after the phone block
Find the phone display block (lines 387–393):
<div className="flex items-start gap-3 p-3 rounded-md bg-muted/40 transition-colors hover:bg-muted/60">
  <Phone className="h-4 w-4 mt-1 text-muted-foreground" />
  <div>
    <p className="text-sm font-medium">Phone</p>
    <p className="text-sm text-muted-foreground">{patient.phone}</p>
  </div>
</div>
Immediately after the closing </div> of that block, add:
{hasModule('whatsapp_api') && patient.whatsappPhone && (
  <div className="flex items-start gap-3 p-3 rounded-md bg-muted/40 transition-colors hover:bg-muted/60">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="#25D366" className="mt-1 shrink-0">
      <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z"/>
      <path d="M12 0C5.373 0 0 5.373 0 12c0 2.127.558 4.126 1.534 5.856L0 24l6.318-1.507A11.95 11.95 0 0012 24c6.627 0 12-5.373 12-12S18.627 0 12 0zm0 22c-1.867 0-3.617-.49-5.13-1.346l-.368-.214-3.75.895.944-3.639-.237-.385A9.955 9.955 0 012 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10z"/>
    </svg>
    <div>
      <p className="text-sm font-medium">WhatsApp</p>
      <p className="text-sm text-muted-foreground">{patient.whatsappPhone}</p>
    </div>
  </div>
)}
  • Step 3: Build to verify
cd /Users/ssh/Documents/Beta-App/odontoX/ui
npm run build 2>&1 | tail -20
Expected: 0 errors.
  • Step 4: Commit
git add ui/src/components/patients/PatientDetails.tsx
git commit -m "feat(ui): add WhatsApp row to PatientDetails (addon-gated, hidden if null)"

Task 8: Fix appointment-reminders — use decrypted whatsappPhone

Files:
  • Modify: server/src/scheduled/appointment-reminders.ts (lines 1–10, 64, 166–202)
Background: The join query returns raw (encrypted) patient columns. The existing patient.phone usage at line 166 sends the encrypted string to the WhatsApp API, which silently fails. This task fixes that by decrypting and switching to whatsappPhone.
  • Step 1: Add decryptPatientPHI import
At the top of appointment-reminders.ts, check if decryptPatientPHI is already imported. If not, add it to the existing encryption import or add a new import:
import { decryptPatientPHI } from '../lib/encryption';
  • Step 2: Decrypt patient in the loop
In the for (const item of upcomingAppointments) loop, after line 64 (const { appointment, patient, doctor, clinic } = item;), add:
const decPat = decryptPatientPHI(patient as Record<string, any>) as typeof patient & { whatsappPhone?: string | null };
  • Step 3: Update WhatsApp send block
Find the WhatsApp block at line 166:
if (patient.phone && await isWhatsAppConfiguredForClinic(clinic.id)) {
Replace the entire block (lines 166–203) with:
// ── WhatsApp — uses whatsappPhone only, no fallback to phone ───────────
const waPhone = decPat.whatsappPhone || null;
if (waPhone && await isWhatsAppConfiguredForClinic(clinic.id)) {
    const waReminders = await db.select()
        .from(scheduledReminders)
        .where(and(
            eq(scheduledReminders.appointmentId, appointment.id),
            eq(scheduledReminders.reminderType, 'whatsapp'),
        ));
    const waAlreadySent = waReminders.some(r => {
        const sentTime = r.sentAt || r.createdAt;
        if (!sentTime) return false;
        const diffHours = (appDateTime.getTime() - sentTime.getTime()) / (1000 * 60 * 60);
        return Math.abs(diffHours - hoursUntil) < 2;
    });

    if (!waAlreadySent) {
        console.log(`Sending ${reminderTag} (${timeContext}) WhatsApp to ${waPhone}`);
        try {
            await sendAppointmentReminder({
                patientPhone: waPhone,
                patientName,
                clinicName: clinic.name,
                date: apptDateStr,
                time: apptTimeStr,
                clinicId: clinic.id,
                patientId: patient.id,
            });
            await db.insert(scheduledReminders).values({
                appointmentId: appointment.id,
                reminderType: 'whatsapp',
                scheduledFor: appDateTime,
                status: 'sent',
                sentAt: new Date(),
            });
        } catch (err) {
            console.error(`WhatsApp reminder failed for patient ${patient.id}:`, err);
        }
    }
}
  • Step 4: Verify TypeScript
cd /Users/ssh/Documents/Beta-App/odontoX/server
npx tsc --noEmit 2>&1 | head -20
  • Step 5: Commit
git add server/src/scheduled/appointment-reminders.ts
git commit -m "fix(server): decrypt patient PHI and use whatsappPhone for WA appointment reminders"

Task 9: Update whatsapp-webhook.ts — match by whatsappPhone first

Files:
  • Modify: server/src/routes/whatsapp-webhook.ts (lines 294–308)
  • Step 1: Add whatsappPhone to the patient select
In handleIncomingMessage, change the db.select({...}) call (lines 294–303) from:
const clinicPatients = await db
  .select({
    id:        patients.id,
    clinicId:  patients.clinicId,
    firstName: patients.firstName,
    lastName:  patients.lastName,
    phone:     patients.phone,
    email:     patients.email,
  })
  .from(patients)
  .where(eq(patients.clinicId, clinicId));
To:
const clinicPatients = await db
  .select({
    id:            patients.id,
    clinicId:      patients.clinicId,
    firstName:     patients.firstName,
    lastName:      patients.lastName,
    phone:         patients.phone,
    whatsappPhone: patients.whatsappPhone,
    email:         patients.email,
  })
  .from(patients)
  .where(eq(patients.clinicId, clinicId));
  • Step 2: Update matching logic
Change lines 306–308:
const matchingPatients = clinicPatients
  .map(p => decryptPatientPHI(p) as typeof p)
  .filter(p => p.phone && normalizePhoneNumber(p.phone) === senderPhone)
  .slice(0, 10);
To:
const matchingPatients = clinicPatients
  .map(p => decryptPatientPHI(p) as typeof p & { whatsappPhone?: string | null })
  .filter(p => {
    if (p.whatsappPhone && normalizePhoneNumber(p.whatsappPhone) === senderPhone) return true;
    if (p.phone && normalizePhoneNumber(p.phone) === senderPhone) return true;
    return false;
  })
  .slice(0, 10);
  • Step 3: Verify TypeScript
cd /Users/ssh/Documents/Beta-App/odontoX/server
npx tsc --noEmit 2>&1 | head -20
  • Step 4: Commit
git add server/src/routes/whatsapp-webhook.ts
git commit -m "fix(server): match incoming WhatsApp messages by whatsappPhone first, then phone"

Task 10: Deploy and smoke test

Files:
  • Deploy server and UI
  • Step 1: Deploy server
cd /Users/ssh/Documents/Beta-App/odontoX/server
npx wrangler deploy 2>&1 | tail -10
Expected: Published with no errors.
  • Step 2: Build and deploy UI
cd /Users/ssh/Documents/Beta-App/odontoX/ui
npm run build 2>&1 | tail -5
npx wrangler pages deploy dist --project-name odonto-prod-ui --branch main --commit-dirty=true 2>&1 | tail -8
  • Step 3: Force-promote canonical deployment
ACCT=9da8a2bb48668ff798b91bd00e9ae005
TOKEN=$(grep 'oauth_token' ~/Library/Preferences/.wrangler/config/default.toml | sed 's/.*"\(.*\)"/\1/')
PROJECT=odonto-prod-ui
LATEST=$(curl -s "https://api.cloudflare.com/client/v4/accounts/$ACCT/pages/projects/$PROJECT" \
  -H "Authorization: Bearer $TOKEN" | \
  python3 -c "import sys,json;print(json.load(sys.stdin)['result']['latest_deployment']['id'])")
curl -s -X POST \
  "https://api.cloudflare.com/client/v4/accounts/$ACCT/pages/projects/$PROJECT/deployments/$LATEST/rollback" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" | \
  python3 -c "import sys,json;d=json.load(sys.stdin);print('✅ Promoted' if d.get('result') else '❌', d.get('errors',''))"
  • Step 4: Smoke test
Manual checks:
  1. Addon-gated (clinic WITHOUT whatsapp_api): Open patient form → phone field shows flag picker, no “Also on WhatsApp” toggle visible anywhere.
  2. Addon-gated (clinic WITH whatsapp_api): Open patient form → phone field shows flag picker, green “Also on WhatsApp” toggle visible next to it.
  3. Toggle ON (default): Create new patient → toggle is ON → save → open patient detail → WhatsApp row shows same number as phone.
  4. Toggle OFF, different number: Edit patient → toggle OFF → WhatsApp field appears → enter different number → save → open detail → WhatsApp row shows the different number.
  5. Toggle OFF, blank: Edit patient → toggle OFF → leave WhatsApp field empty → save → open detail → WhatsApp row hidden.
  6. Appointment reminder fires: Trigger a reminder manually or wait for the scheduled window. Confirm WA goes to whatsappPhone and not phone.