SMS Dispatcher Wiring (Channels::Sms::EVENTHANDLERS)
Last Updated: 2026-10-15
Why this guide exists
The unified notification dispatcher (Shared::Notifications::Dispatcher) routes
domain events to channel adapters. The SMS adapter (Shared::Notifications::Channels::Sms)
maps each dispatcher event key to:
- A handler class (e.g.
Shared::Notifications::Sms::SmsAppointment). - A
message_typestring that the handler'smessage_typesregistry resolves to a specific subtype renderer (e.g.Sms::Appointment::FutureAppointmentReminder).
When either side of the map drifts — typo in the event key, wrong
message_type string, or a handler subtype that doesn't accept the string —
sends silently no-op. The dispatcher logs nothing because the handler's
message_types.find { ... } returns nil and the send returns without raising.
This guide documents the contract that prevents that silent drop and the process for adding or fixing entries.
The wiring contract
Every entry in Channels::Sms::EVENT_HANDLERS must satisfy:
klass = mapping[:handler].constantize
klass.message_types.find { |m| m.match?(mapping[:message_type]) }.present?This is enforced by backend/domains/shared/test/services/notifications/channels/sms_event_handlers_test.rb,
which iterates the full table and fails if any entry breaks the invariant.
A handler subtype that no entry reaches must be listed in UNWIRED_SUBTYPES
in the same test. Each entry cites its callers as path#method (or is
dead:), and the test checks that the file defines the method and names a
message key that the subtype matches, so a citation cannot drift (#2709).
Historical incidents
- #1566 —
'reminder_patient'(legacy class name) mistagged as the message_type after the port. NoSms::Teleconsultation::PatientRemindersubtype accepted that string. Caught in production after silent SMS drops. - #1569 — added the wiring contract spec scoped to the 15 newly-mapped events.
- #1587 — widened the spec to iterate the full
EVENT_HANDLERSmap after renaming 5 pre-existing entries with mismatchedmessage_typestrings (oneappointment.reminder, onepublic_appointment.confirmed, threedoctor_appointment.*) and removing 2 orphan entries with no handler at all (appointment_queue.checked_in,public_appointment.created).
How to add a new dispatcher event
-
Decide the event key — namespace by domain:
<aggregate>.<verb>(e.g.appointment.rescheduled,clinic_doctor.id_verification). Lowercase, dot- separated, snake_case after the dot. -
Find or create the handler subtype under
backend/domains/shared/services/notifications/sms/<family>/<subtype>.rb. The subtype must implement:def match?(input) input == 'your_message_type_string' # or a set of strings end def get_message_text(message_params, basic_language = nil) I18n.with_locale(basic_language || :en) do I18n.t('sms.your_family.your_subtype', name: message_params[:name].to_s) end endThe text lives in
config/locales/en.ymlundersms.*; the class holds no words of its own (text_in_i18n_test.rb, #2709). -
Register the subtype in the handler class's
message_typesarray (Shared::Notifications::Sms::Sms<Family>). -
Add the entry to
Channels::Sms::EVENT_HANDLERS:'your_aggregate.your_verb' => { handler: 'Shared::Notifications::Sms::SmsYourFamily', message_type: 'your_message_type_string' } -
(Optional, recommended) Seed the corresponding
Shared::MsgDetailrow inbackend/db/seeds/reference/so the DLT registration is tracked. -
(Optional) If the event should be gated by a per-facility toggle (e.g.
appt_confirm_sms), add the entry toShared::Notifications::SmsPolicy::EVENT_TOGGLESandWhatsappPolicy::EVENT_TOGGLES(if WhatsApp also routes this event). Declare the same event on the message class (gated_by event: 'your_aggregate.your_verb'), so a direct call to the handler withfacility:applies the same toggle (#2711, seenotifications.md). -
Run the contract spec:
RAILS_ENV=test bin/rails test domains/shared/test/services/notifications/channels/sms_event_handlers_test.rbIt will iterate every entry and verify the new one resolves.
How to fix a mismatched entry
Run the spec above. The failure message tells you which event broke and the
configured message_type it couldn't resolve:
handler Shared::Notifications::Sms::SmsAppointment has no message_type
accepting "appointment_reminder" for event "appointment.reminder".Pick one of:
Option A — Change the dispatcher's message_type to match the handler
Look up what match? actually accepts on each subtype:
grep -A2 "def match?" backend/domains/shared/services/notifications/sms/<family>/*.rbUpdate the EVENT_HANDLERS entry. Preferred when: only the dispatcher
references this string. Faster, no ripple effect.
Option B — Widen the handler subtype's match? to accept the dispatcher's string
def match?(input)
%w[old_string dispatcher_string].include?(input)
endThis is the pattern Sms::Teleconsultation::PatientReminder uses (accepts both
'patient_10min' and 'patient_reminder'). Use when: the existing
acceptance string is referenced by non-dispatcher callers (e.g. legacy parts/
code still calling the handler directly) and renaming would break them.
How to remove a dead entry
An entry is dead if:
- No handler subtype accepts its
message_type(the spec fails), AND - No caller invokes
Dispatcher.call(event: 'this_event', ...)(grep -rnthe codebase), AND - No
MsgDetailseed references the event, AND - No policy map references the event.
In that case, remove the entry from EVENT_HANDLERS. Also remove any
corresponding entries from SmsPolicy::EVENT_TOGGLES and
WhatsappPolicy::EVENT_TOGGLES — the policy maps are independent and won't
fail loudly if they reference a non-existent dispatcher event.
Document the removal in the file with a comment explaining what the canonical
event is now (if any) so callers searching for the removed key land on the
replacement. Example from channels/sms.rb after #1587:
# `appointment_queue.checked_in` was an orphan entry pointing at
# `'checkin_confirmation'` — no `AppointmentQueue` subtype matched
# that string, and grep finds zero callers and zero policy or seed
# entries. The "checkin happened, notify queue position" notification
# is what `appointment_queue.position_update` (above) covers.
# Removed in #1587.Reference: handler families
| Handler class | Subtypes live in | Common message_type pattern |
|---|---|---|
SmsAppointment | sms/appointment/ | booking_confirmation, reschedule_appointment, future_appointment_reminder |
SmsAppointmentQueue | sms/appointment_queue/ | new_after_checkin, update_after_checkin, prescription_attached |
SmsPublicAppointment | sms/public_appointment/ | save_details, appointment_confirmed, reminder |
SmsNotifyDoctorAppt | sms/notify_doctor_appt/ | consultant_appointment_booked, doctor_appointment_rescheduled |
SmsTeleconsultation | sms/teleconsultation/ | patient_reminder, reminder_doctor, patient_joined |
SmsCode | sms/code/ | verification, two_factor, reset_password |
Run the contract spec after any change to either side.
Related guides
- Notification System — high-level overview of all channels (SMS, push, WhatsApp, Slack).
- SMS + WhatsApp parts → domains migration — the ongoing program that builds out the dispatcher.