Dentolize · Hidden-Number WhatsApp Contacts Walkthrough
On this pageWhat changed, and why it's hard to test end-to-endWhere to focus manual/exploratory testingEdges called out directly in the code (verify these are actually true, not just intended)

For Quality

What changed, and why it's hard to test end-to-end

This fix depends entirely on Meta's webhook payload shape — there is no way to trigger a genuine hidden-number contact from inside Dentolize's UI or a normal test account, because the privacy behavior is enforced by WhatsApp itself based on real contact history. Realistic testing means either a live WhatsApp Business test contact configured with username privacy against a number the test WABA hasn't recently exchanged with, or constructing webhook payloads by hand and posting them to the whatsapp-official endpoint that handles onMessage. This sandbox has no seeded WhatsApp conversations at all (whatsapp=false), so the screenshots in this walkthrough show real, current, empty screens rather than a demonstrated hidden-number conversation — see Walkthrough.

Where to focus manual/exploratory testing

  • First message from a BSUID-only contact. Confirm a new OnlineConversation row is created with bsuid set and phone null, no server error, and the conversation appears in /whatsapp/chats.
  • First message from a BSUID-only contact where the same phone number already has a conversation from before this migration. This is the conflict/backfill: false path (online-conversation.service.ts:19-38) — confirm the existing phone-keyed row is used and unmodified, and that the message doesn't crash or create a duplicate second row that then also fails.
  • A contact who later reveals their number (or messages the same clinic again after being seen by phone before). Confirm the same conversation is updated (not duplicated) — check messagesCount/lastMessage continuity and that a previously blank phone gets backfilled.
  • Replying to a BSUID-only conversation from clinic-web/clinic-mobile as a staff member, and via the auto-responder (officialWhatsAppConfig.waConfig.autoAnswer). Confirm the outbound Graph API call uses recipient, not to (this can only really be verified by confirming actual delivery, or by inspecting whatsapp-official logs/outbound request if there's a way to capture it in the test environment).
  • Patient/lead auto-matching for a BSUID-only first contact. Expected: no patient match attempted, no lead created — confirm this is what actually happens rather than an error or a mismatched lead.
  • A hidden-number contact requesting an online payment link (ACTION.PAYMENT_OPTION). Expected: handleOnlinePayment.ts:67-70 throws Cannot create a payment link without a phone number — confirm this is caught gracefully upstream (the outer try/catch in MessagesService.onMessage) rather than surfacing to the contact or crashing the webhook handler.
  • Editing a patient linked to a phone-less OnlineConversation. Change the patient's phone number and confirm the conversation link is not disconnected (editPatient.js:211-218 — there's nothing to compare against, so it should be a no-op on the link).
  • Regression on the ordinary (phone-visible) path. Every touched file has a phone-first branch — worth a quick pass confirming normal contacts still show phone numbers, still link to patients/leads, and still receive replies via to, unchanged from before this PR.

Edges called out directly in the code (verify these are actually true, not just intended)

  • findConversationByAddress prefers the phone-matched row over the BSUID-matched row when both exist and disagree (online-conversation.service.ts:34) — worth confirming which row "wins" in an actual conflicting-data scenario, since this determines which conversation history staff continue to see.
  • username is written unconditionally on every message (not gated by the backfill flag that guards phone/bsuid) — confirm a contact's displayed @username updates if they change their WhatsApp username, including for a conversation that also has a phone number on file.
  • The GraphQL server's isBsuid (official-whats-app.utils.js:9, a regex) and whatsapp-official's isBsuid (messages.service.ts:95, "did Meta omit wa_id") are independently implemented — there's no shared test coverage forcing them to agree. If Meta ever changes the BSUID format, only one of the two would need updating, and nothing would flag the other falling out of sync.
  • handleSendWhatsappMessage's GraphQL phone argument is now optional and effectively unused by the resolver (schema.graphql:1242, handleSendWhatsappMessage.js:37-40) — confirm no caller still depends on it being required or being echoed back anywhere.