Skip to content

fix(chatwoot-adapter): send the correct phone number on new contacts#47

Merged
rmyndharis merged 4 commits into
rmyndharis:mainfrom
i350:fix/send-correct-phone-number-to-chatwoot
Jul 23, 2026
Merged

fix(chatwoot-adapter): send the correct phone number on new contacts#47
rmyndharis merged 4 commits into
rmyndharis:mainfrom
i350:fix/send-correct-phone-number-to-chatwoot

Conversation

@i350

@i350 i350 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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 populated msg.senderPhone — and the host only populates that field for @lid senders under the RESOLVE_LID_TO_PHONE=true flag. Plain @c.us chats and @lid chats 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 in relay.ts and 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:

phone: msg.isGroup ? undefined : msg.senderPhone ?? undefined

That left three legitimate contact populations without a phone:

  1. Plain @c.us chats. The host's senderPhone field 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).
  2. @lid chats whose lid→pn mapping has already been warmed by any earlier reply (in‑memory canonicalChatId returns the <phone>@c.us form, but the previous code never read it).
  3. Bulk backfill sweep. Plain @c.us chats reaching the contact stage via backfillAllChats had no source for a phone at all.

Plus, msg.contact?.number is intentionally never consulted: when WEBHOOK_CONTACT_DETAILS=true (off by default) is on for an @lid sender, 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) in chatwoot-adapter/relay.ts consolidates the decision logic. Sources, in priority order:

  1. msg.senderPhone — host's pre‑resolved @lid MSISDN (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 +).
  2. User‑part of canonicalChatId when it ends with @c.us — covers warmed lid mappings and every plain @c.us chat. canonicalChatId is also @lid‑aware and resolves via the engine's in‑memory lidMappingStore (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.
  • Unresolved @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?.number is documented as LID digits for @lid senders 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 exported resolvePhone(msg, canonicalChatId): string | undefined.
  • chatwoot-adapter/inbound.ts:97 — replace msg.isGroup ? undefined : msg.senderPhone ?? undefined with resolvePhone(msg, canonicalChatId).
  • chatwoot-adapter/backfill.ts:70 — same replacement on the bulk‑sweep path; chat.id is already in the neutral dialect on entry, so the helper is given it directly (no engine call needed).
  • chatwoot-adapter/relay.test.tsnew 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 the contact.number trap.
  • 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+ — extend makeDeps to allow overriding client, plus one new test exercising all three bulk‑path branches.

Risk & Migration Notes

  • No contact retroactively gets a phone. This changes phone values on new contacts only; existing rows in Chatwoot are not touched. Operators who want existing no‑phone contacts filled can run the bulk sweep after upgrading (it is idempotent), but note that contacts already created without a phone may have been auto‑merged in Chatwoot by an identifier‑only search — re‑importing will not split those merges.
  • msg.senderPhone priority is intentional. If an operator runs with RESOLVE_LID_TO_PHONE=true and has a stale mapping in the engine's lidMappingStore, senderPhone wins 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.
  • Edge case: '00' and similar collapsed forms. A host‑side normaliser might collapse '+00' to '00', which becomes +00 here. 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 new relay.test.ts, inbound.test.ts, and backfill.test.ts cases.
  • Manual smoke: with RESOLVE_LID_TO_PHONE=true, send a DM from a known @lid contact. Inspect the Chatwoot contact: phone_number should now equal +<msisdn> (previously empty).
  • Manual smoke (bulk): run a backfillAllChats against a session with a mix of plain @c.us chats, groups, and unresolved @lid chats. Inspect the resulting contacts; the first should have a phone, the last two should not.

i350 and others added 4 commits July 23, 2026 07:54
…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).
@rmyndharis
rmyndharis merged commit 9720f04 into rmyndharis:main Jul 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants