SMS Event Handler Wiring QA Guide
Scope: Manual verification that 5 pre-existing
Channels::Sms::EVENT_HANDLERSentries renamed in #1587 now actually send (previously silently dropped), the 2 removed orphan entries don't break any caller flow, and the same-class bug inClinical::Appointments::NotificationService(which bypassed the dispatcher and used the broken keys directly) now sends correctly too.Prerequisites: Backend running on dev or staging, Rails console access, a
+91test phone number registered for SMS in dev or a staging number. SMS dispatch is gated bySmsPolicy— verify the test facility has the relevant per-clinic toggles ON (appt_confirm_sms,appt_reminder_sms,send_doctor_appt_sms) before testing.Related: Wiring contract spec · SMS Dispatcher Wiring Guide · Issue #1587
What changed in #1587
The dispatcher map at backend/domains/shared/services/notifications/channels/sms.rb
had 7 broken entries — 5 with mismatched message_type strings (handler
silently dropped the send because no subtype's match? accepted the configured
string) and 2 orphan entries pointing at non-existent handler subtypes.
Separately, Clinical::Appointments::NotificationService invoked the same
SMS handlers DIRECTLY (bypassing the dispatcher) with the OLD pre-#1587
strings — so doctor-side appointment SMSes from CMS also silently dropped.
Those 3 direct calls now use the same renamed strings; 3 other branches in
that service that pointed at non-existent handler subtypes
('patient_checked_in', 'new_appointment', 'patient_arrived') were
changed to log a skip instead of pretending to send.
Renamed (now send correctly):
| Event key | Before (silent drop) | After (correct subtype) |
|---|---|---|
appointment.reminder | appointment_reminder | future_appointment_reminder |
public_appointment.confirmed | confirm | appointment_confirmed |
doctor_appointment.booked | appointment_booked | consultant_appointment_booked |
doctor_appointment.rescheduled | appointment_rescheduled | doctor_appointment_rescheduled |
doctor_appointment.cancelled | appointment_cancelled | doctor_appointment_cancelled |
(doctor_appointment.teleconsult_booked was already correct.)
Removed (orphans with no handler):
appointment_queue.checked_in— useappointment_queue.position_updateinstead.public_appointment.created— usepublic_appointment.save_detailsor.confirmedinstead.
SmsPolicy::EVENT_TOGGLES and WhatsappPolicy::EVENT_TOGGLES have the
corresponding public_appointment.created entries removed.
Verification — automated (run first)
cd backend
bundle exec rspec domains/shared/spec/services/notifications/channels/sms_event_handlers_spec.rbExpect: 164 examples, 0 failures. This iterates the full EVENT_HANDLERS
map and asserts every entry's message_type resolves to a handler subtype.
If this fails, the wiring guide is wrong — stop and fix before manual QA.
Also run the policy specs (they reference the same event keys):
bundle exec rspec domains/shared/spec/services/notifications/sms_policy_spec.rb \
domains/shared/spec/services/notifications/whatsapp_policy_spec.rbManual flow checks (5 scenarios)
For each scenario:
- Trigger the flow from the UI (or Rails console as documented).
- Confirm a row appears in
shared.notification_logswith the expectedchannel: 'sms',event:, andrecipient. - (Staging only) Confirm the SMS arrives on the test number.
- Tail logs for
[Channels::Sms]suppression messages and[Sms<Family>]delivery confirmations.
Scenario 1 — Future appointment reminder (appointment.reminder)
Trigger: Schedule an appointment 4+ hours in the future, then run the appointment-reminder cron / job that fires future-dated SMSes. In Rails console:
# `Identity::Patient#phone_number` stores the number WITHOUT the
# country code; the country code lives on `phone_country_code`. Use
# the right column for the lookup, then call `phone_full` for the
# concatenated string the SMS sender expects.
patient = Identity::Patient.find_by(phone_country_code: '91', phone_number: 'XXXXXXXXXX')
Shared::Notifications::Dispatcher.call(
event: 'appointment.reminder',
context: { patient_name: 'Test', appointment_time: 1.day.from_now.iso8601 },
recipient: patient,
channels: [:sms],
facility: Identity::Facility.find(<facility_id>),
tenant_id: Identity::Tenant.find(<tenant_id>).id
)Expect:
shared.notification_logsrow withevent: 'appointment.reminder',channel: 'sms',status: 'sent'(or'queued'if Sidekiq deferred).- SMS body templated from
Sms::Appointment::FutureAppointmentReminder. - Before the fix: this would log nothing and return silently.
Scenario 2 — Public appointment confirmation (public_appointment.confirmed)
Trigger: From the patient app, book a public appointment and click the "Confirm" CTA in the booking flow.
Expect:
shared.notification_logsrow withevent: 'public_appointment.confirmed'.- SMS body templated from
Sms::PublicAppointment::AppointmentConfirmed.
Scenario 3 — Doctor-side appointment notifications (3 events)
Trigger: From CMS, perform each action against an appointment whose attending consultant is a different user than the booking clinic owner:
- Book: create a new appointment → fires
doctor_appointment.booked. - Reschedule: edit time → fires
doctor_appointment.rescheduled. - Cancel: cancel the appointment → fires
doctor_appointment.cancelled.
Expect:
- 3 separate
notification_logsrows (one per action), each withrecipientset to the doctor's phone (not the patient). - SMS bodies templated from
NotifyDoctorAppt::AppointmentBooked,::AppointmentRescheduled,::AppointmentCancelled.
Scenario 4 — Removed event: appointment_queue.checked_in
This event no longer routes to SMS. The replacement is
appointment_queue.position_update, which is what the queue notification UI
should already be calling.
Trigger: From CMS queue management, mark a patient as checked in.
Expect:
shared.notification_logsrow withevent: 'appointment_queue.position_update'(NOTchecked_in).- SMS body templated from
Sms::AppointmentQueue::NewAfterCheckin. - If you see a row with
event: 'appointment_queue.checked_in': a caller in the code is still using the removed key. File a bug and tag #1587.
Scenario 5 — Removed event: public_appointment.created
This event no longer routes to SMS. The patient-app booking flow's "saved your
details" notification is public_appointment.save_details, and the booking
confirmation is public_appointment.confirmed.
Trigger: Book a public appointment from the patient app (do not confirm).
Expect:
shared.notification_logsrow withevent: 'public_appointment.save_details'.- No row with
event: 'public_appointment.created'.
Edge cases to check
-
Toggle off: Set
appt_reminder_sms = falseon the test facility'sFacilitySettings. Trigger scenario 1. Expect the dispatcher to log[Channels::Sms] suppressed by SmsPolicy event=appointment.reminder ...and NOT create a notification row. -
DLT not registered: If the test environment doesn't have a
MsgDetailrow for the relevant template + language, the send will fail at theSmsService.calllevel (not at the dispatcher). The dispatcher will still write the notification log row withstatus: 'failed'. -
Pre-auth flow: Some SMS events fire before a user is logged in (e.g. OTP). These pass
facility: nilto the dispatcher and bypass the policy check. Out of scope for this QA pass (covered by the auth flow QA).
What "done" looks like
- Wiring spec passes locally (
164 examples, 0 failures). - Each of the 5 scenarios delivers an SMS or shows the corresponding
notification_logsrow in dev/staging. - No
[LegacyDeprecation]warn lines for the renamed events (those callers should be going through the new dispatcher path). - No errors in the staging error tracker on the dispatcher code path for 7 days post-deploy.
Rollback
If a renamed event causes regressions in production (e.g. some legacy caller
was relying on the broken-but-recoverable old behavior, or a handler subtype
has a bug surfaced only now that the send actually fires), revert the
specific entry in channels/sms.rb back to the broken message_type string.
The dispatcher will go back to silently dropping that one event, restoring
previous "send doesn't happen" behavior while the underlying handler issue
is fixed.
For the removed orphan entries (appointment_queue.checked_in,
public_appointment.created), rollback is straightforward: re-add the entry
to EVENT_HANDLERS and EVENT_TOGGLES. Since they were silently broken
before #1587, the rollback restores the pre-#1587 silent-drop behavior, not
working sends.