Dentolize · CRM Module Walkthrough
On this pageTest environmentHighest-value test areas (by risk)Edge cases & gotchas to probeRegression must-notsKnown gaps to log, not file as bugs

For Quality

What to test, and where the edges are. Written for QA.

Test environment

The feat-crm-module sandbox has Meta connectable and TikTok/Snapchat/Google disabled. For full integration flows without real platform apps, drive packages/platform-mock — it posts correctly signed webhooks and serves a mock Graph API (point META_GRAPH_URL at it). It also offers deterministic insight spend (campaign 5001 = 100/day, 5002 = 40/day) so CPL/ROI math is hand-checkable, a tamperSignature option to test 401 rejection, and a silent-lead endpoint to simulate a dropped webhook for reconciliation testing.

Highest-value test areas (by risk)

1. Webhook ingestion & idempotency

  • Send the same webhook twice → exactly one lead/message (dedupe on

[platform, externalEventId]).

  • Tampered signature401, no persistence.
  • Unroutable event (unknown asset id) → stored DEAD, not attributed to any company.
  • Force processing failure → event goes FAILED, retries (5, backoff), then DEAD; verify a

topic with no handler is parked as RECEIVED (not failed) and replays if enabled later.

  • Ack timing: the controller returns 200 before processing — verify a slow processor

doesn't cause the platform to see a timeout.

2. Lead dedupe & normalization

  • Same phone in Latin, Arabic-Indic (٠١٢٣), and Eastern-Arabic (۰۱۲۳) digits → one lead.
  • Phone with spaces/dashes/RTL marks → normalizes to the same E.164.
  • Existing patient vs new lead with same phone → patient wins, no duplicate lead

(EXISTING_LEAD_PHONE).

  • Round-robin: create N leads into a stage with M users → assignments rotate evenly.
  • Lead captured before campaign hierarchy sync → still attributes once hierarchy arrives

(raw-id attribution).

3. Unified inbox & the 24h window

  • Reply inside 24h → sent; reply after → blocked with MESSAGING_WINDOW_CLOSED at both

the server pre-check and the gateway (the gateway is authoritative — test that the client pre-check can be bypassed but the gateway still refuses).

  • Comment channels (FB/IG) → composer disabled.
  • WhatsApp channel → rejected by sendConversationMessage (must use the WA path); confirm

the existing WhatsApp module is unchanged (regression).

  • SLA lifecycle: inbound arms firstResponseDueAt; cross the deadline → cron flips

slaBreachedAt within ~5 min; reply → flag clears and firstRespondedAt sets.

  • Auto-create leads on → new DM from a stranger creates a lead in the default stage; off →

it doesn't.

  • Sending on a non-ACTIVE integration → INTEGRATION_INACTIVE.

4. Automation

  • Each live trigger fires: Lead created (from ad capture, manual add, QR, import,

patient→pipeline), Stage changed, Stale in stage (after staleDays).

  • Once-per-event: redeliver the source event → rule runs once ([ruleId, dedupeKey]).
  • Stale re-fire: a lead that leaves and re-enters a stage → fires again (anchored on

stagedAt); a lead sitting still → does not re-fire.

  • Depth cap: a move-stage action that would trigger a stage-changed rule → does not loop.
  • Actions execute in order; verify assign/tag/move/WA-template each apply.
  • Confirm the 5 unemitted triggers are hidden in the builder.

5. Marketing hub / ROI math

  • With deterministic mock spend, hand-check CPL (spend ÷ CRM leads), CPA/CAC (spend ÷

won leads), ROAS (collected revenue ÷ effective spend), contribution (revenueNet − spend).

  • vs 0: zero leads → CPL shows , not 0.
  • Effective spend prefers booked GL spend over platform spend when posted.
  • Revenue bounding: invoices dated before a lead's capture date must not be credited

to the campaign; only on/after count.

  • Mixed currency: ad accounts in different currencies → no summed figure, warning shown.
  • Reconciliation report surfaces platform-vs-CRM lead gaps (use silent-lead to create one,

then confirm the nightly/5-min poll heals it).

6. Reviews

  • Send request → WhatsApp with a /r/<code> link; tap → clickedAt set, status CLICKED,

302 to the review URL.

  • Cooldown: second request inside cooldown → blocked; a prior FAILED request does not

block.

  • Analytics aggregate correctly per branch/status/rating.

7. Security & retention

  • Platform tokens are never present in any GraphQL response (inspect payloads).
  • WebhookEvent.payload older than 90 days → redacted to { redacted: true }; RECEIVED

rows exempt.

  • Gateway fails to boot without SOCIAL_TOKEN_ENC_KEY.
  • yarn encrypt-wa-tokens is idempotent (re-run → skips encrypted rows).
  • Permission matrix: test each role (owner/doctor/receptionist/accountant/hrmanager from the

sandbox roster) against each CRM screen — verify VIEW_MARKETING_ROI hides revenue from those without it, etc.

Edge cases & gotchas to probe

  • Web/mobile flag asymmetry: turn a FEATURE_CRM_* flag off → web hides the surface,

mobile still shows it (permissions + leadsEnabled only). Confirm expected behavior.

  • Reconnect a platform after token expiry → asset mappings (stage/branch, enabled) survive.
  • Counter drift: manually skew a LeadStage.totalLeads → nightly reconcileCrmCounters

repairs it and reports drift to Sentry.

  • Legacy WhatsApp token: a row still holding a signed-JWT token → readSocialToken still

reads it (dual-read) before/after running the encrypt script.

  • Google no-OAuth path: wrong google_key on the lead-form webhook → rejected.

Regression must-nots

  • The existing WhatsApp conversation experience must be unchanged.
  • Existing Leads functionality must keep working while the deprecated Lead columns are

still present (the CI tripwire audit-deprecated-lead-fields.sh must stay green).

Known gaps to log, not file as bugs

TikTok/Snapchat/Google connect disabled in beta; comment replies unsupported; five automation triggers hidden; Google review ingestion not yet live; the "~1,266 req/s" figure unbacked by a repo benchmark. See Scope, Gaps & Honest Notes.