Skip to content

WhatsApp Module

One-line: The WhatsApp module owns everything from WABA onboarding (two provider paths — Gupshup and Meta TSP), template lifecycle, opt-in/opt-out compliance, catalogue + login helpers, through to the high-frequency send pipeline (whatsapp_queue_* collections drained by 1-sec / 3-sec crons → Gupshup or Meta Graph API) and click / message-limit / retry bookkeeping.

Validation legend

Symbol Meaning
Verified in code this review
📄 From PM Excel / business docs (not code)
💻 Code detail (function/route/field confirmed)
♻️ Duplicated / overlapping implementation
⚰️ Legacy / being deprecated
⚠️ Bug, risk, or footgun

1. Overview

WhatsApp is the primary retention channel in Prism (📄 Communication Layer sheet). A brand must have a WABA (WhatsApp Business Account) and pre-approved templates before any marketing/utility message can be sent. Prism supports two provider integrations:

  • Gupshup (BSP path) ✅ — controllers/gupshupOnboardingController.js. Partner-API onboarding, template create/edit/delete, media handle upload, business-profile management, opt-in tracking. whatsapp_configuration_type = "gupshup".
  • Meta TSP (direct Cloud API path) ✅ — controllers/metaTSP.js. OAuth access-token exchange, WABA subscribe, phone register, template create (TEXT/IMAGE/VIDEO/CAROUSEL), inbound webhook. whatsapp_configuration_type = "facebook". Roadmap B3.13 Meta TSP — In Progress 📄.

Supporting surfaces: catalogue (whatsappCatalogueController.js — WhatsApp ordering menu + FB product catalog) and WhatsApp login (whatsappLoginControler.jswa.me deep links).

Green Tick / WABA / DLT context 📄: Green Tick is the WhatsApp verified-business badge; WABA is the messaging account; DLT is the Indian SMS registration (SMS-only, see SMS-Push). WhatsApp does not use DLT but does enforce per-number messaging-limit tiers (waba_limit / meta_waba_limit).

⚠️ Known bug E1.3 — "WhatsApp volume reports inaccurate" (P0) 📄 (Roadmap). Sent/delivered volume shown on dashboards does not reconcile with whatsapp_queue_* process_status counts. Root causes suspected in this module: (a) delivery webhooks not fully reconciled (consumer-receipts Lambda is stubbed ⚠️ — see High-Level-Architecture §7); (b) process_status=3 ("sent, pending webhook") counted as delivered.


2. Business Flow

Example journey (marketer sends a WhatsApp campaign)

  1. Onboarding (one-time) 📄 — Client provides WABA + gets templates approved; Ops maps them in Prism. Go-Live checklist step 4 (Business-Flow §1).
  2. Marketer builds a campaign in Campaigns → audience + approved template + variables.
  3. Campaign engine enqueues one row per recipient into whatsapp_queue_YYYYMMDD with process_status:0.
  4. sendWhatsappMessageCron (marketing) polls every 3s, hands the batch to processWhatsappMsgData.
  5. Per message: opt-in checked (unless cold campaign), provider chosen (gupshup/facebook), template rendered, message POSTed to provider. process_status → 3.
  6. Provider fires a delivery/read webhook → (monolith webhook handler + api-receipts Lambda). Opt-outs and failures reconciled by the daily crons.

Edge cases

  • Not opted-in ✅ — For non-cold Gupshup sends, if comm_array lacks "whatsapp", whatsappMsgProcess adds it via updateCommArrayAcrossCollections() before sending (implicit opt-in on send).
  • Opt-out / block ✅ — Provider error codes 131026 (user opt-out) or 470 (blocked) → whatsappOptOutCron strips "whatsapp" from the customer comm_array.
  • Welcome-message race ✅ — For parent 7175, if the customer placed an order between inserted_at and scheduled_at, the welcome message is skipped (process_status=4) to avoid a redundant greeting.
  • Failed marketing units ✅ — whatsappRetryCampaignCron refunds non-retryable failures to the business wallet and re-queues retryable ones as a free (amount:0) campaign.
  • Media too large / wrong type ✅ — validateGupshupMediaImage rejects non-image/png|jpeg or > 2 MB.

3. Technical Flow

Source → API → Validation → Logic → DB → Events → Queue → Notification → Response
  • Source ✅ — Campaign/journey engine (processCampaign.js, automatedCampaignQueueNew.js), transactional producers (Digital Bill, Feedback, low-rating), or onboarding UI.
  • API ✅ — Onboarding/template/profile routes under routes/crm.js (auth); provider webhooks (/whatsapp/webhook/callback, no auth).
  • Validation ✅ — Mobile 10-digit [6-9]; media validated by validateGupshupMediaImage; opt-in check in whatsappMsgProcess.
  • Logic ✅ — processWhatsappMsgData selects provider by whatsapp_configuration_type, staggers sends 66 ms apart, renders templates via processCampaignWhatsappHelper (variable + JWT button substitution).
  • DB ✅ — Reads/writes whatsapp_queue_YYYYMMDD, business_configs, customers_${parentId}, wallet_transactions, addo_campaigns.
  • Events / Queue ✅ — process_status state machine: 0 → 2 → 3 → 4. Fast-path polling (no broker) — Queues.
  • Notification ✅ — Actual send to Gupshup https://api.gupshup.io/wa/api/v1/template/msg or Meta graph.facebook.com/v19.0/{phoneNumberId}/messages.
  • Response ✅ — Provider messageId stored on the queue row; delivery/read arrives asynchronously via webhook.

4. Architecture Diagram

Flowchart

flowchart TD
    subgraph Onboard["Onboarding & Config (control plane)"]
        GUP[gupshupOnboardingController]
        META[metaTSP]
        CAT[whatsappCatalogueController]
        LOGIN[whatsappLoginControler]
    end
    GUP -->|Partner API| GAPI[(Gupshup Partner API)]
    META -->|OAuth + Graph API| MAPI[(Meta Graph API v22.0)]

    subgraph Send["Send pipeline (data plane)"]
        PROD[Campaign / Journey / Txn producers]
        WQ[(whatsapp_queue_YYYYMMDD)]
        C1[sendWhatsappMessageCron 3s\nmarketing: type absent]
        C2[sendWhatsappTransactionalCron 1s\ntxn: type present]
        C3[sendWhatsappRemainingCron 0 0 * * *]
        PROC[processWhatsappMsgData]
    end
    PROD --> WQ
    WQ --> C1 --> PROC
    WQ --> C2 --> PROC
    WQ --> C3 --> PROC
    PROC -->|facebook| MSEND[(Meta messages API v19.0)]
    PROC -->|gupshup| GSEND[(Gupshup template/msg)]

    subgraph Recon["Reconciliation crons"]
        CLICK[whatsappClick 15m]
        OPTOUT[whatsappOptOutCron 0 0]
        LIMIT[whatsappMsgLimit 1 AM]
        RETRY[whatsappRetryCampaignCron hourly]
    end
    GSEND -.webhook.-> WEBHOOK[monolith webhook + api-receipts λ ⚠️stub]
    WEBHOOK --> WQ
    CLICK --> CAMP[(addo_campaigns.links_clicked)]
    OPTOUT --> CUST[(customers_parentId.comm_array)]
    LIMIT --> CFG[(business_configs.waba_limit)]
    RETRY --> WALLET[(wallet_transactions refund)]

Send + receipt sequence

sequenceDiagram
    autonumber
    participant PROD as Campaign/Txn producer
    participant WQ as whatsapp_queue_YYYYMMDD
    participant CR as sendWhatsapp*Cron
    participant PR as processWhatsappMsgData
    participant PROV as Gupshup / Meta
    participant WH as Webhook (monolith + api-receipts λ)
    participant OO as whatsappOptOutCron

    PROD->>WQ: insert {process_status:0, whatsapp_configuration_type, template, type?}
    CR->>WQ: poll {scheduled_at<=now, process_status:0}
    CR->>PR: batch (≤10000)
    PR->>WQ: set process_status=2 (picked)
    PR->>PR: opt-in check (skip if cold_campaign)
    PR->>PROV: POST template message (66ms stagger)
    PROV-->>PR: {status:"submitted", messageId}
    PR->>WQ: set process_status=3 + messageId
    PROV->>WH: delivery / read / error webhook
    Note over WH: consumer-receipts λ log-only ⚠️ (E1.3)
    WH->>WQ: update messageError / read_count
    OO->>WQ: nightly scan errorCode∈{131026,470}
    OO->>WQ: strip "whatsapp" from comm_array

5. Folder Structure (real files)

uengage-crm/
├── controllers/
│   ├── gupshupOnboardingController.js   ✅ Gupshup onboarding, templates, profile, opt-in
│   ├── metaTSP.js                       ✅ Meta TSP OAuth, templates, webhook
│   ├── whatsappCatalogueController.js   ✅ WhatsApp ordering menu + FB catalog
│   └── whatsappLoginControler.js        ✅ wa.me login deep links
├── crons/
│   ├── sendWhatsappMessageCron.js       ✅ marketing sends (every 3s)
│   ├── sendWhatsappTransactionalCron.js ✅ transactional sends (every 1s)
│   ├── sendWhatsappRemainingCron.js     ✅ prior-day sweep (midnight)
│   ├── whatsappClick.js                 ✅ link-click aggregation (15m)
│   ├── whatsappOptOutCron.js            ✅ opt-out reconciliation (midnight)
│   ├── whatsappMsgLimit.js              ✅ WABA limit sync (1 AM)
│   └── whatsappRetryCampaignCron.js     ✅ failed-campaign retry + refund (hourly)
├── processLibrary/
│   ├── whatsappMsgProcess.js            ✅ consumer: provider select + send
│   └── processCampaignWhatsappHelper.js ✅ template var + JWT button substitution
└── commonFunctions/
    ├── validateGupshupMediaImage.js     ✅ media type/size validation
    └── metaFileUploading.js             ✅ multer storage for Meta media upload

Related prism-services (serverless) files handle the send/receipt Kafka path but the consumers are stubbed ♻️⚠️ — see map-05 / High-Level-Architecture.


6. Database

Collections / tables

Store Name Purpose Key fields ✅ Indexes
Mongo whatsapp_queue_YYYYMMDD Date-partitioned send queue destination, scheduled_at, template, media_data, campaign_id, parent_business_id, business_id, whatsapp_configuration_type, process_status, type, messageError, read_count none defined ⚠️ (verify)
Mongo whatsapp_cron_status Cron run-lock / crash detection status (false/true/"crashed"), start_time none
Mongo business_configs Per-business WABA config whatsapp_configuration.gupshup/.facebook, gupshup_app_id, gupshup_api_key, gupshup_onboarding_status, waba_limit, meta_phone_number_id, waba_id, meta_access_token, phone_quality, whatsapp_account_status none
Mongo customers_${parentId} Customer profiles comm_array (channel opt-ins), whatsapp_optin verify
Mongo gupshup_media / template stores Media handle IDs handle id, url none
Mongo pre_approved_templates Food/salon pre-approved templates template code, body none
Mongo addo_campaigns Campaign stats links_clicked, campaignType, retry_enabled, retry_hours, retry_processed, failed verify
Mongo wallet_transactions Send debits / retry refunds transaction_type, amount, parent_business_id none
Mongo link_hits_queue Raw click events campaign_id, processed, isBot verify
MySQL addo_business whatsapp_login flag, business name/phone whatsapp_login, name PK id
MySQL addo_menu_sections, addo_menu_items Catalogue source type, status, viewType, sp verify

Relationships

business_configs (by parent_business_id+child_business_id) ⇄ whatsapp_queue_* (by business_id) ⇄ customers_${parentId} (by mobile). Campaign rows in addo_campaigns link to queue rows via campaign_id.

Common queries ✅

// Marketing cron poll
db.whatsapp_queue_20260703.find({ scheduled_at: { $lte: now }, process_status: 0, type: { $exists: false } }).limit(10000)
// Transactional cron poll
db.whatsapp_queue_20260703.find({ scheduled_at: { $lte: now }, process_status: 0, type: { $exists: true } })
// Opt-out scan (prior day)
db.whatsapp_queue_20260702.find({ process_status: { $in: [3,4] }, "messageError.code": { $in: [131026, 470] } })

7. APIs

Auth = standard authMiddleware unless noted. All POST. Representative routes marked (verify) where the exact path/middleware should be re-confirmed against routes/crm.js.

Gupshup onboarding

Route Method Auth Purpose
/save/onboarding/details POST ✅ yes Save Gupshup + ordering onboarding
/get/onboarding/details POST ✅ yes Read onboarding status
/update/onboarding/status POST ✅ yes Approve/reject (is_approved 0/1)
/get/onboarded/business/list POST ✅ yes Paginated list (20/page)
/create/template POST ⚠️ no auth Create Gupshup template (verify)
/get/handleId POST ⚠️ no auth (multipart) Upload media → handle id
/update/business/optins POST ⚠️ no auth Update opt-in
/send/whatsapp/test POST ✅ yes Send test message

Meta TSP

Route Method Auth Purpose
/get/business/access/token POST ✅ yes OAuth code → access token
/create/meta/template POST ✅ yes Create Meta template
/get/meta/handleId POST ⚠️ no auth (multipart) Upload media to Meta
/get/meta/template POST ✅ yes Fetch approved templates
/whatsapp/webhook/callback ALL ⚠️ no auth Inbound messages + status webhook

Catalogue & login

Route Method Auth Purpose
/get/sections POST ⚠️ no auth Menu sections (verify)
/create/catalog POST ✅ yes Create FB product catalog
/check/whatsapp/login POST JWT Return wa.me login link
/uen_rider/whatsapp/login POST no auth uEngage-sender rider login (parent 64671)

Representative example — send test WhatsApp (verify)

// POST /send/whatsapp/test   Headers: { token: <auth> }
{ "business_id": "12345", "parent_business_id": "999",
  "mobile_number": "9876543210", "template_id": "invoice_bill",
  "params": ["Ravi", "ORD-8842"] }
// 200 → { "status": 1, "messageId": "gs_abc123" }
Validation ✅: mobile [6-9]\d{9}; media via validateGupshupMediaImage. Failures: 400 invalid mobile; 402 insufficient wallet; provider non-submitted → row not advanced to 3.


8. Code Walkthrough

Send path (call hierarchy)

sendWhatsappMessageCron (3s) ─┐
sendWhatsappTransactionalCron (1s) ─┼─▶ whatsappMsgLibrary.processWhatsappMsgData(result, cron_id)
sendWhatsappRemainingCron (midnight) ┘        │
                                              ├─ per msg: set process_status=2
                                              ├─ if whatsapp_configuration_type==="facebook" → sendFacebookMsg()
                                              ├─ else "gupshup" → opt-in check → sendGupshupMsg()
                                              ├─ processCampaignWhatsappHelper.checkIfTemplateNeedsToBeUpdated()
                                              │     • ue_cust_name / ue_cust_mobile / ue_cust_eid
                                              │     • ue_animated_campaign → generatePromo() → https://uen.io/{code}
                                              │     • ue_jwt_token (button slots) secret uengage@govmwypq43
                                              └─ on success: process_status=3 + messageId
- processWhatsappMsgData staggers sends setTimeout(fn, j*66) — crude client-side rate limiting ✅ (no server-side per-business cap ⚠️, see map-04 §5.10). - Provider selection is purely whatsapp_configuration_type on the queue row ✅. - Retry ✅ — whatsappRetryCampaignCron: campaigns campaignType:"4", retry_enabled:1, status:1, failed>0, wait scheduled_at + retry_hours <= now; refunds non-retryable (messageError.code ≠ 131049) and re-queues retryable as free campaign.

Dependencies: axios (provider HTTP), jsonwebtoken (login + button JWTs), multer (metaFileUploading.js), MySQL shortenedurls/addo_promo_code_engine (promo codes).


9. Business Rules

Rule Value ✅ Source
Marketing poll interval every 3s (*/3 * * * *) sendWhatsappMessageCron
Transactional poll interval every 1s (*/1 * * * *) sendWhatsappTransactionalCron
Marketing vs transactional split type field absent = marketing; present = transactional queue schema
Batch limit per run 10,000 rows send crons
Send stagger 66 ms/message whatsappMsgProcess
process_status machine 0 scheduled → 2 picked → 3 sent(pending webhook) → 4 done/skip
Opt-out error codes 131026 (opt-out), 470 (blocked) whatsappOptOutCron
Retry non-retryable code 131049 treated as retryable-exclusion whatsappRetryCampaignCron
Media limits image/png|image/jpeg, ≤ 2 MB validateGupshupMediaImage
WhatsApp cost (marketing) ₹0.91 + 18% GST 📄/✅ wallet_config
WhatsApp cost (utility) ₹0.17 + 18% GST ✅ wallet_config
WABA limit sync daily 1 AM whatsappMsgLimit
Cron crash detection stuck > 20 min → status "crashed" send crons

Hidden logic ⚠️: parent 7175 has bespoke welcome-message suppression; cold campaigns bypass opt-in entirely. Several onboarding/webhook routes are unauthenticated — treat as security-review items (Security).


10. Performance

  • Fast-path polling (1s/3s) with no message broker → constant DB churn even when idle ⚠️ (map-04 §5.3).
  • No indexes on whatsapp_queue_* process_status/scheduled_at ⚠️ — each poll is a partial scan; date-partitioning caps collection size.
  • 66 ms stagger ≈ ~15 msg/s per batch; no per-business rate cap risks provider throttle at scale ⚠️.

11. Logging

  • Winston rotating logs (monolith standard). Cron runs tracked in whatsapp_cron_status.
  • Meta inbound webhook logs → whatsapp_webhook_logs ✅.
  • Provider responses (messageId / error) persisted on the queue row (messageError).

12. Monitoring

  • Watch whatsapp_cron_status.status for "crashed" / long-running locks.
  • Reconcile process_status counts vs dashboard volume to detect E1.3 drift.
  • whatsappMsgLimit output (waba_limit, phone_quality, whatsapp_account_status) is the health signal for provider tier/quality.

13. Troubleshooting

Issue Likely cause Fix Command
Messages stuck at process_status:0 Cron lock status:false (crashed run) Reset lock db.whatsapp_cron_status.updateOne({},{ $set:{status:true}})
Dashboard volume ≠ queue counts (E1.3) status:3 counted as delivered; webhook not reconciled Count by explicit status db.whatsapp_queue_20260703.aggregate([{$group:{_id:"$process_status",n:{$sum:1}}}])
Customer stopped receiving WA Opt-out (131026/470) removed "whatsapp" Confirm + re-opt-in per consent db.customers_999.findOne({mobileNo:"9876543210"},{comm_array:1})
Template send fails Media invalid / template not approved Run media validator; re-approve template check validateGupshupMediaImage return
Provider throttling No rate cap, 1s/3s bursts Throttle batch / raise stagger inspect provider 429s in logs

14. FAQs

  • Gupshup vs Meta TSP — which sends? Whatever whatsapp_configuration_type says on the queue row ✅.
  • Why two send crons? Latency isolation — transactional (1s) must beat marketing (3s).
  • Is opt-in enforced? For non-cold Gupshup sends yes; cold campaigns skip it ⚠️.
  • Where do receipts land? Monolith webhook updates the queue row; the prism-services consumer-receipts Lambda is log-only ⚠️.

15. Cheat Sheet

Marketing queue poll : type absent  · every 3s · limit 10000
Txn queue poll       : type present · every 1s
status: 0 sched → 2 picked → 3 sent(webhook pending) → 4 done
opt-out codes        : 131026, 470   (whatsappOptOutCron, midnight)
media                : png/jpeg ≤ 2MB (validateGupshupMediaImage)
retry                : campaignType "4", retry_enabled 1, hourly, refunds non-131049
WABA limit sync      : 1 AM (whatsappMsgLimit)
provider select      : whatsapp_configuration_type = gupshup | facebook
cost                 : marketing ₹0.91 · utility ₹0.17 (+18% GST)
KNOWN BUG            : E1.3 WhatsApp volume reports (P0)

Knowledge Tests

Level 1 — MCQs

  1. A queue row has no type field. Which cron sends it? a) transactional (1s) b) marketing (3s) c) remaining d) retry — b. type absent = marketing.
  2. process_status=3 means? a) delivered b) skipped c) sent, webhook pending d) queued — c. It is not confirmed delivery (relates to E1.3).
  3. Error code 131026 triggers? a) retry b) refund c) opt-out removal d) template delete — c. whatsappOptOutCron strips "whatsapp" from comm_array.
  4. Provider selection is decided by? a) parent_business_id b) whatsapp_configuration_type c) template type d) cron — b.
  5. Max Gupshup media image size? a) 5 MB b) 2 MB c) 1 MB d) unlimited — b (validateGupshupMediaImage).

Level 2 — Scenarios

  1. A brand reports "customers say they never got the offer, but the dashboard shows 12,000 delivered." Explain how E1.3 could produce this and which field you'd audit. (Expect: process_status=3 counted as delivered; audit real status distribution + webhook reconciliation.)
  2. Marketing sends are being throttled by Gupshup during a festival blast. Given the 3s/10,000-row cron and 66 ms stagger, what changes would you make and what is the risk of simply lowering the interval? (Expect: per-business rate cap/backoff; lowering interval worsens thundering herd + ban risk.)

Level 3 — Code reading

Read whatsappMsgProcess.js. For a cold campaign to a customer who has never opted in, trace whether the message sends and whether comm_array is modified. (Answer: cold sets sendMsgCheck=true, opt-in check skipped, message sends; comm_array not modified for cold.)

Level 4 — Architecture

The consumer-receipts Lambda is stubbed and the monolith handles receipts. Argue whether E1.3 should be fixed in the monolith webhook or by completing the Lambda consumer, considering the dual-writer risk in High-Level-Architecture.

Level 5 — Prod debugging

At 14:00 marketing WhatsApp stops flowing; whatsapp_queue_20260703 has 40k rows at process_status:0. Give your first three commands and the most likely single-line fix. (Expect: check whatsapp_cron_status for a stuck status:false/"crashed" lock; check PM2 cron process; reset the lock.)

Practical Assignments

  1. Write a read-only script that reconciles, for a given day, process_status counts against the dashboard WhatsApp volume for one parent — surfacing the E1.3 gap.
  2. Add a per-business send-rate guard (config-driven) inside the send path and document its interaction with the 66 ms stagger.
  3. Draft the design (no code) for completing consumer-receipts to persist delivery/read into whatsapp_queue_*, including dedup vs the monolith webhook.