fix(chatwoot-adapter): send the correct phone number on new contacts#47
Merged
rmyndharis merged 4 commits intoJul 23, 2026
Merged
Conversation
…elper Fold the two identical digits-strip/+prefix blocks in resolvePhone into a single local e164() and condense the doc comment, keeping the contact.number =LID trap warning. No behavior change; all resolvePhone tests stay green.
… fix Bump the manifest to 0.5.7, add the CHANGELOG entry for the resolvePhone fix, and regenerate the catalog (plugins.json, README tables, plugin README).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Contacts newly created in Chatwoot were almost always missing a
phone_number. The old expression (msg.isGroup ? undefined : msg.senderPhone ?? undefined) only set a phone when the host had explicitly populatedmsg.senderPhone— and the host only populates that field for@lidsenders under theRESOLVE_LID_TO_PHONE=trueflag. Plain@c.uschats and@lidchats with a warm (pre‑cached) lid→pn mapping both reached Chatwoot without a phone, so the contact search by phone and any downstream merge logic silently broke.This change introduces a single
resolvePhone()helper inrelay.tsand routes both the inbound path (inbound.ts) and the bulk backfill sweep (backfill.ts) through it, with a 60‑line test file covering every manifest case.Problem
Before this commit,
ensureConversation(..., phone)was called with:That left three legitimate contact populations without a phone:
@c.uschats. The host'ssenderPhonefield is documented as lid‑only, so a normal human DM never carried a phone even though the JID user‑part is the MSISDN (1234567890@c.us→+1234567890).@lidchats whose lid→pn mapping has already been warmed by any earlier reply (in‑memorycanonicalChatIdreturns the<phone>@c.usform, but the previous code never read it).@c.uschats reaching the contact stage viabackfillAllChatshad no source for a phone at all.Plus,
msg.contact?.numberis intentionally never consulted: whenWEBHOOK_CONTACT_DETAILS=true(off by default) is on for an@lidsender, that field carries the LID digits, not the MSISDN. Falsely matching on a lid‑derived number would corrupt Chatwoot's contact search and produce silent merge surprises.Fix
A pure, synchronous
resolvePhone(msg, canonicalChatId)inchatwoot-adapter/relay.tsconsolidates the decision logic. Sources, in priority order:msg.senderPhone— host's pre‑resolved@lidMSISDN (RESOLVE_LID_TO_PHONE=true). MSISDN digits with no+guaranteed; the helper strips non‑digits, prefixes+, and falls through if the result is empty (a'--'host error must not emit a bare+).canonicalChatIdwhen it ends with@c.us— covers warmed lid mappings and every plain@c.uschat.canonicalChatIdis also@lid‑aware and resolves via the engine's in‑memorylidMappingStore(no network call), so warmed lids get the real phone with no extra latency.Short‑circuits to
undefined:msg.isGroup === true— a group has no MSISDN.@lid(canonical stays@lid) — preserves pre‑fix behavior when the mapping is genuinely unknown; the contact still creates with an identifier only.@g.us,@broadcast,@newsletter, unrecognised JIDs — defensive; the contact falls back to identifier‑only.Failures of what isn't read.
msg.contact?.numberis documented as LID digits for@lidsenders and is therefore not a phone source here — using it would silently set the wrong phone on the contact and corrupt future merges. The dark‑recursive mistake is exactly the trap this comment is designed to prevent.Changes
chatwoot-adapter/relay.ts— add exportedresolvePhone(msg, canonicalChatId): string | undefined.chatwoot-adapter/inbound.ts:97— replacemsg.isGroup ? undefined : msg.senderPhone ?? undefinedwithresolvePhone(msg, canonicalChatId).chatwoot-adapter/backfill.ts:70— same replacement on the bulk‑sweep path;chat.idis already in the neutral dialect on entry, so the helper is given it directly (no engine call needed).chatwoot-adapter/relay.test.ts— new file, 60 lines, 7 tests covering groups, normalization edge cases (spaces, dashes, country‑code prefixes, empty‑digits fallback), warmed/cold lid, broadcast/newsletter/unknown, and thecontact.numbertrap.chatwoot-adapter/inbound.test.ts:213+— 5 new tests for the inbound call site across the full manifest matrix (lid+senderPhone, warmed lid, cold lid, plain@c.us, group).chatwoot-adapter/backfill.test.ts:6,38,167+— extendmakeDepsto allow overridingclient, plus one new test exercising all three bulk‑path branches.Risk & Migration Notes
msg.senderPhonepriority is intentional. If an operator runs withRESOLVE_LID_TO_PHONE=trueand has a stale mapping in the engine'slidMappingStore,senderPhonewins over the (warm) canonical id. That's deliberate — the host has just done the work for the inbound event and is the freshest signal we have.'00'and similar collapsed forms. A host‑side normaliser might collapse'+00'to'00', which becomes+00here. That's the host's responsibility; this helper does not re‑interpret country codes.Test Plan
cd chatwoot-adapter && npm test— all suites green, including the newrelay.test.ts,inbound.test.ts, andbackfill.test.tscases.RESOLVE_LID_TO_PHONE=true, send a DM from a known@lidcontact. Inspect the Chatwoot contact:phone_numbershould now equal+<msisdn>(previously empty).backfillAllChatsagainst a session with a mix of plain@c.uschats, groups, and unresolved@lidchats. Inspect the resulting contacts; the first should have a phone, the last two should not.