Which Templates Qualify
Business view
Not every message the clinic has on file can be sent this way. Two separate gates decide what shows up in the Send Template picker:
- It has to be an approved WhatsApp template. Meta reviews and approves WhatsApp templates before a business can
use them outside the 24-hour window — that's a WhatsApp platform rule, not a Dentolize one. A message that hasn't been through that process (or was rejected, or is still pending) simply won't appear in the picker.
- It can only need the contact's name. Reminder messages are usually built from placeholders like the
appointment date, the doctor's name, or an invoice amount — placeholders that only make sense in the automated context (a cron job that has the actual appointment or invoice on hand). Outside that context, sending a message with @DATE in it would either send the literal text @DATE to the patient, or require staff to somehow supply the right value by hand. Rather than either of those, the picker simply won't let the message be sent — a message is only eligible if the only variables it contains are @PATIENT_NAME and/or @FIRST_NAME.
If a staff member picks a message that fails rule 2, they see exactly which placeholders can't be filled, right in the preview, and the Send button stays disabled. Rule 1 is enforced earlier, by the picker only ever showing approved messages in the first place.
Technical view
Rule 1 — approved template, enforced in the search query
The mobile and web pickers both call searchMessages with a new approved: true argument (packages/server/src/schema.graphql:24, added alongside the pre-existing searchTerm/onAction args). On the server, AppointmentQueries.searchMessages adds a where.AND clause when approved is set:
// packages/server/src/resolvers/queries/appointmentQueries.js:674-676
if (args.approved) {
where.AND = [...where.AND, { template: { status: 'APPROVED' } }]
}
Message.template is a ConversationTemplate? relation; TemplateStatus is APPROVED | PENDING | REJECTED. A message with no template at all, or a template still pending or rejected, is filtered out here — before it ever reaches the client.
Rule 2 — name-only variables, enforced twice
Both apps compute this client-side for instant feedback, and the server re-derives and re-checks it — the client check is a UX nicety, not the source of truth.
- Client.
unfillableVariables(details)in each app'stemplateHelpers.jsfilters a fixed list of 25 known
@VARIABLE placeholders (the same list the reminder crons understand — company name, branch info, appointment date/time, doctor, patient/invoice IDs, points, etc.), keeping only the ones present in the message and not in NAME_VARIABLES = ['@PATIENT_NAME', '@FIRST_NAME'] (packages/clinic-mobile/src/components/dashboard/WhatsApp/conversationMessages/templateHelpers.js:1-45, and the byte-identical web copy). An empty result means the message is eligible.
- Server.
sendWhatsappTemplatere-extracts the variables actually present in the message using
extractValues, which was made exportable specifically for this reuse:
``js // packages/server/src/cronJobs/messages/embeddedValues.js:575 export const extractValues = text => { … } ``
and rejects the whole request if anything other than a name variable shows up:
```js // packages/server/src/resolvers/mutations/actions/officialWhatsApp/sendWhatsappTemplate.js:50-56 const variables = [...new Set(extractValues(messageData.details))]
if (variables.some(variable => !NAME_VARIABLES.includes(variable))) { throw new Error('Template Needs Variables[Client Error]') } ```
This is intentionally a duplicated, hand-maintained NAME_VARIABLES constant in three places (both templateHelpers.js files and the server resolver) rather than one shared list — see the note in the glossary if that surfaces as a maintenance question.
What "filling the name" actually means
Once a message passes both gates, the only substitution that happens is turning @PATIENT_NAME into the conversation's full name and @FIRST_NAME into the first word of it — computed identically in three places (mobile fillNameVariables, web fillNameVariables, and inline in the server resolver's valueFor helper at packages/server/src/resolvers/mutations/actions/officialWhatsApp/sendWhatsappTemplate.js:76). The name itself comes from the conversation's linked patient.fullName if there is one, falling back to the conversation's own name field (e.g. for a WhatsApp contact that never became a patient record) — see conversationTemplateName in templateHelpers.js and the equivalent inline lookup in the resolver (sendWhatsappTemplate.js:75).
What WhatsApp actually receives
The resolver doesn't send a free-form message with the name spliced in — it sends a type: "template" payload, naming the approved template (messageData.template.name) and its language, with the filled name as the template's one body parameter if a name variable was present (packages/server/src/resolvers/mutations/actions/officialWhatsApp/sendWhatsappTemplate.js:91-98). This is what lets WhatsApp accept it despite the conversation being expired — it's being sent as a template, which is exempt from the 24-hour rule, not as a disguised free-form message.