Campaigns Module¶
Validation legend: ✅ verified in code · 📄 from business/PM source · 💻 verified in code this review · ♻️ duplicate/overlapping implementation · ⚰️ dead/legacy · ⚠️ needs verification / open bug
1. Overview¶
What: The Campaigns module is Prism's engine for manual, one-time outbound marketing across four channels — WhatsApp, SMS, App Push, Web Push (plus Email). A client (restaurant brand) picks a channel, an audience (a saved segment, an RFM cohort, or an uploaded list), a template, a sender ID and a schedule; Prism debits the business wallet per message and hands the send off to channel queues. Delivery, clicks and downstream revenue (ROI) are tracked back to the campaign.
Why it matters: Campaigns are the direct-monetization surface of Prism. They consume the client's prepaid business wallet (📄 wallet is debited per send — Business-Flow §5), and their measured ROI is the number that justifies the client's spend and renewal. Getting the numbers right is a P0 concern — see open bugs E1.1 (ROI rectification) and E1.5 (campaign builder budgeting & scheduling) in the Roadmap.
Three coexisting pipelines ♻️ (this is the single most important architectural fact about this module):
| Pipeline | Controller | Purpose | Storage |
|---|---|---|---|
| Traditional ✅ | campaignController.js (50+ fns) |
Standard SMS / Push / WhatsApp / Email campaigns; full lifecycle (create → schedule → send → track). | addo_campaigns, campaign_mis, sms_summary |
| Dynamic / Animated ✅ | campaignControllerDynamic.js (4 fns) |
Custom-code / animated microsite campaigns with per-campaign dynamic coupon codes. | Custom_Code_Campaign_MIS, Dyanamic_Coupon_Campaign_* |
| Automated / Journeys ✅ | automatedCampaignController.js |
Event-triggered journeys (Welcome, Birthday, …). Documented separately — see Automated-Journeys. | campaign_journeys, campaign_types |
All three write into the same channel queues (sms_queue_*, whatsapp_queue_*, push_noti_queue_*) and roll up into the same MIS collections. The traditional and dynamic pipelines are scheduled/executed by processCampaignController.js (traditional) and hit directly via API (dynamic). The Roadmap A2.1 (P0) goal is to merge Campaigns + Journeys into a single Central Marketing Hub.
2. Business Flow (product view)¶
flowchart LR
A[Select Channel<br/>WA / SMS / App-Push / Web-Push] --> B[Audience<br/>segment / RFM cohort / upload]
B --> C[Template + variables]
C --> D[Sender ID + ROI window]
D --> E[Schedule date/time]
E --> F[Save campaign → addo_campaigns status:0]
F --> G[processCampaignController picks it every 5 min]
G --> H[processLibrary/processCampaign builds audience → channel queue]
H --> I[Delivery / Read / Click tracking]
I --> J[ROI attribution: revenue in X-hour window]
J --> K[Campaign Analytics / MIS widgets]
Example journey — a Diwali WhatsApp blast:
1. Client opens Campaigns, picks WhatsApp, selects the "Champions" RFM cohort (auto-refreshed nightly).
2. Chooses a pre-approved WhatsApp Marketing template with a {{name}} variable and a coupon button.
3. Sets a 48-hour ROI attribution window and schedules for 10:00 the next morning.
4. On save, the campaign lands in addo_campaigns with status:0, campaignType:"4" (WhatsApp), scheduled_at:"2026-…".
5. At 10:00 processCampaignController (runs */5) picks it, processCampaign expands the cohort into per-customer rows and enqueues them into whatsapp_queue_YYYYMMDD.
6. sendWhatsappMessageCron drains that queue to Gupshup/Meta; wallet is debited per delivered message.
7. Over the next 48h, orders from those customers are attributed to the campaign by autoCampaignRoiCrons → shown as revenue / ROI in analytics.
Edge cases:
- Insufficient wallet balance → sends are blocked (📄 Business-Flow §5). ⚠️ Verify exact pre-flight check vs. per-message check in sendSms/processCampaign.
- Unsubscribed customers ✅ are logged in unsubscribed_campaigns {campaign_id, customers_id} and must be excluded from audiences.
- Blacklist / DND segments 📄 stored in business_config.blacklist_segments are excluded.
- Channel integration inactive → dashboard shows 0, never omits the channel (📄 Dashboard 3.5).
- Bulk vs non-bulk SMS ✅ — processCampaignController prioritizes non-bulk SMS campaigns before bulk to keep transactional-feel sends prompt (is_bulk_campaign).
3. Technical Flow (frontend → API → validation → logic → DB → queues → notifications → response)¶
sequenceDiagram
autonumber
participant UI as CRM Frontend
participant API as /crm_api (routes/crm.js)
participant MW as authMiddleware
participant C as campaignController.add_addo_campaign
participant DB as MongoDB (addo_campaigns)
participant SCH as processCampaignController (*/5 cron)
participant PROC as processLibrary/processCampaign
participant Q as sms/whatsapp/push queue_YYYYMMDD
participant SND as channel send crons
participant PRV as Gupshup/Meta / SMS GW / Firebase
participant ROI as autoCampaignRoiCrons
UI->>API: POST /crm_api/addCampaigns (auth header)
API->>MW: authMiddleware (sets parent_business_id, created_by)
MW->>C: req validated
C->>DB: insert campaign {campaignType, status:0, scheduled_at, target_audience, template}
C-->>UI: 200 {campaign_id}
Note over SCH: every 5 min, IST
SCH->>DB: find campaignType in [1,2,3,4] & status:0 & scheduled_at<=now
SCH->>PROC: processCampaign(allCampaigns)
PROC->>Q: expand audience → per-customer rows (execution_time)
Note over SND: SMS */3, Push */5, WA */3s+*/1s
SND->>Q: poll unprocessed
SND->>PRV: send message; debit wallet; write sms_summary/fcmjunk
PRV-->>SND: delivery/read/click callbacks
Note over ROI: hourly-ish
ROI->>DB: attribute orders in ROI window → auto_campaign_roi / campaign_mis
Channel type codes ✅ (processCampaignController, addo_campaigns.campaignType):
| Code | Channel |
|---|---|
"1" |
SMS |
"2" |
Push Notification (App / Web) |
"3" |
|
"4" |
4. Architecture Diagram¶
Component flowchart (three pipelines → shared queues → shared MIS):
flowchart TD
subgraph API[HTTP API - /crm_api]
T[campaignController<br/>traditional]
D[campaignControllerDynamic<br/>animated/custom]
CO[coldCampaignController<br/>bulk upload]
TG[campaignTagController]
end
T --> AC[(addo_campaigns)]
CO --> AC
CO --> CCC[(cold_campaign_customers)]
D --> CCM[(Custom_Code_Campaign_MIS)]
D --> DCC[(Dyanamic_Coupon_Campaign_*)]
AC --> SCH[processCampaignController<br/>*/5 cron]
SCH --> PROC[processLibrary/processCampaign]
PROC --> SQ[(sms_queue_*)]
PROC --> WQ[(whatsapp_queue_*)]
PROC --> PQ[(push_noti_queue_*)]
SQ --> SS[(sms_summary)]
PROC --> MIS[(campaign_mis)]
PROC --> AGG[campaignAggregation]
subgraph ROI[autoCampaignRoiCrons]
R1[autoCampaignRoi]
R2[uengageProcessDynamicCouponCodeROICalculationHelper]
end
AC --> ROI
DCC --> R2
ROI --> ACR[(auto_campaign_roi)]
5. Folder Structure (real files of this module)¶
uengage-crm/
├── controllers/
│ ├── campaignController.js ✅ traditional pipeline (50+ fns)
│ ├── campaignControllerDynamic.js ✅ animated/custom coupon campaigns (4 fns)
│ ├── campaignControllerMailHelper.js ✅ email delivery helper (not HTTP)
│ ├── coldCampaignController.js ✅ bulk cold-list upload / drafts
│ ├── processCampaignController.js ✅ scheduler cron (*/5)
│ └── campaignTagController.js ✅ campaign tagging
├── processLibrary/
│ ├── processCampaign.js ✅ ~70KB executor: audience → queues
│ ├── processCampaignWhatsappHelper.js ✅ WA template/media/button helper
│ ├── campaignMis.js ✅ ROI/conversion MIS → campaign_mis
│ ├── campaignAggregation.js ✅ sent/delivered/failed rollup
│ └── uengageProcessDynamicCouponCodeROICalculationHelper.js ✅ dynamic-coupon ROI
├── crons/
│ ├── processAutomatedCampaigns.js (journeys — see Automated-Journeys)
│ └── autoCampaignRoiCrons/ ✅ ROI attribution crons (see §6)
│ ├── autoCampaignRoi.js
│ ├── abandonCartRoi.js
│ ├── lostCustomersRoi.js
│ ├── welcomeMsgRoi.js
│ ├── referEarnRoi.js
│ └── roiSnapshotBackfill.js
├── models/
│ ├── campaign_mis.js ✅
│ ├── sms_summary.js ✅ (indexed: mobile_number)
│ ├── unsubscribed_campaigns.js ✅
│ ├── campaign_tags.js ✅
│ ├── campaign_journey.js (journeys)
│ └── campaign_types.js (journeys)
└── routes/
├── crm.js ✅ base mount /crm_api
└── prism_routes/campaign_tag_routes.js ✅
6. Database¶
Collections owned / heavily used by this module¶
| Collection | Purpose | Key fields | Index | Relationships |
|---|---|---|---|---|
addo_campaigns ✅ |
Master campaign record; the queue that processCampaignController drains. |
campaignType (1/2/3/4), status (0=scheduled), scheduled_at, target_audience, is_bulk_campaign, template, parent_business_id, business_id |
⚠️ none documented — add (campaignType,status,scheduled_at) |
1→many sms_summary / queue rows; referenced by ROI crons |
campaign_mis ✅ |
Per-business rolled-up campaign stats. | total_campaigns_sent, total_sms_campaigns, total_sms_sent, total_sms_price, total_push_noti_campaigns, total_android_notifications, total_ios_notifications, total_web_push_campaigns, total_web_push_notifications, parent_business_id, business_id, is_parent, created_at |
none | Aggregated from queues/sends |
sms_summary ✅ |
Per-SMS delivery log. | campaign_id, mobile_number (indexed), status (0 queued/1 delivered/2 failed), delivered_at, inserted_date |
mobile_number |
many→1 addo_campaigns (by campaign_id) |
unsubscribed_campaigns ✅ |
Opt-out log for audience exclusion. | campaign_id (ObjectId), customers_id (ObjectId) |
none | many→1 campaign, many→1 customer |
campaign_tags ✅ |
Organizational labels. | tag_name, businessId, parentBusinessId, createdBy, insertedAt, status (1 active) |
none | scoped by business |
Custom_Code_Campaign_MIS ✅ |
Dynamic/animated campaign records. | verify fields | ⚠️ | dynamic pipeline |
Dyanamic_Coupon_Campaign_* ✅ |
Per-campaign dynamic coupon code stores (note the misspelling "Dyanamic"). | verify | ⚠️ | consumed by ROI helper |
auto_campaign_roi ✅ |
ROI attribution rows written by autoCampaignRoiCrons. |
campaign ref, attributed revenue/orders, window | ⚠️ | ROI reporting |
wallet_transaction ✅ |
Debit ledger for each paid send. | service_id (1=SMS,4=WhatsApp,10=Email), service_name, transaction_type:"debit", amount, sub_total, gst, total, units, campaign_id |
none | billing/reconciliation |
cold_campaign_customers ✅ |
Uploaded cold-list recipients (draft). | customers (array), campaign ref |
⚠️ | 1→1 draft addo_campaigns (status:4) |
Channel queue collections (date-partitioned): sms_queue_YYYYMMDD, push_noti_queue_YYYYMMDD, whatsapp_queue_YYYYMMDD. See SMS-Push and Queues.
Common queries¶
// Scheduler pickup (processCampaignController, verified)
db.collection("addo_campaigns").find({
campaignType: "1", scheduled_at: { $lte: nowIST }, status: 0,
$or: [{ is_bulk_campaign: { $ne: true } }, { is_bulk_campaign: { $exists: false } }]
}).sort({ target_audience: 1, scheduled_at: 1 }).limit(10)
// Delivery lookup for a customer
db.sms_summary.find({ mobile_number: "98XXXXXXXX" }) // uses the index
// Audience exclusion
db.unsubscribed_campaigns.find({ campaign_id: ObjectId(cid) }, { customers_id: 1 })
7. APIs¶
Base path: /crm_api (mounted in index.js → routes/crm.js). Auth: authMiddleware unless noted. All representative routes below are ✅ verified in routes/crm.js; treat request/response bodies as ⚠️ verify-in-controller.
| Method | Path | Controller fn | Auth | Notes |
|---|---|---|---|---|
| POST | /crm_api/addCampaigns |
campaignController.add_addo_campaign |
authMiddleware | Create a campaign (representative — verify) |
| POST | /crm_api/create/bulk/campaign |
campaignController.createBulkCampaigns |
— (file upload) | Multipart CSV bulk create |
| POST | /crm_api/get/campaign/details |
campaignController.campaignDetail |
authMiddleware | Campaign detail view |
| POST | /crm_api/get/campaigns/overview |
campaignController.campaignOverView |
authMiddleware | List/overview |
| POST | /crm_api/get/sms/summary |
campaignController.getSmsSummary |
authMiddleware | Delivery log |
| POST | /crm_api/cancel_retry_campaign |
campaignController.cancelRetryCampaign |
authMiddleware | Cancel / retry |
| POST | /crm_api/update/template |
campaignController.updateTemplate |
— (file upload) | Template update |
| POST | /crm_api/save/email/campaign |
campaignController.saveEmailCampaign |
authMiddleware | Email campaign |
| POST | /crm_api/create/whatsapp/campaign |
campaignController.createWhatsappCampaign |
verify | WhatsApp campaign |
| POST | /crm_api/campaign/campaign_count |
campaignController.calculateCampaignCount |
authMiddleware | Audience size pre-flight |
| POST | /crm_api/coldCampaign/upload/campaign/cold/data |
coldCampaignController.uploadCampaignColdData |
— (file upload) | Bulk cold list |
| POST | /crm_api/coldCampaign/delete/draft/campaign |
coldCampaignController.deleteDraftCampaign |
authMiddleware | Delete draft |
| GET | /crm_api/getAll/customCouponCampaign |
campaignControllerDynamic.getAllDynamicCampaigns |
authMiddleware | Dynamic campaigns list |
| GET | /crm_api/getDetails/getROIForCampaign |
campaignControllerDynamic.getROIForCampaign |
authMiddleware | Dynamic ROI |
| GET | /crm_api/campaignHit/AnimatedCampaign |
campaignControllerDynamic.AnimatedCampaignHit |
none ⚠️ | Public hit tracker for animated microsite |
| POST | /crm_api/campaignHit/campaignData |
campaignControllerDynamic.animatedCampaignData |
authMiddleware | Animated campaign data |
| POST | /crm_api/track/push/clicks |
campaignController.trackPushCampaignClicks |
verify | Push click tracking |
| POST | /crm_api/create/campaign_tag |
campaignTagController.createCampaignTag |
authMiddleware | Create tag |
| POST | /crm_api/get/campaign_tags |
campaignTagController.getCampaignTags |
authMiddleware | List tags |
Example payload (representative — ⚠️ verify field names against add_addo_campaign)¶
POST /crm_api/addCampaigns
Authorization: <api-token>
Content-Type: application/json
{
"campaignType": "4", // 4 = WhatsApp
"template": "diwali_champions_v2",
"target_audience": "segment:champions",
"scheduled_at": "2026-07-04 10:00:00",
"sender_id": "UENGAG",
"roi_window_hours": 48,
"parent_business_id": "123",
"business_id": "456"
}
Failure cases: 401/403 fail-closed on auth error (Request-Lifecycle §4); 400 on missing template/audience (verify); insufficient wallet → send blocked downstream (📄); scheduling in the past → picked immediately on next */5 tick.
8. Code Walkthrough¶
processCampaignController.js ✅ (the scheduler — verified this review):
- Cron "*/5 * * * *", timezone Asia/Calcutta.
- Loops campaign types ["1","2","4","3"]. SMS limit = 10, all others = 3 per tick.
- For SMS, fetches non-bulk first (is_bulk_campaign not true / not exists), then fills the remaining slots with bulk campaigns — so transactional-feeling SMS aren't starved by huge bulk blasts.
- All queries: status: 0, scheduled_at: { $lte: now }, sort({ target_audience: 1, scheduled_at: 1 }).
- Collects allCampaigns and calls processCampaign(allCampaigns) (in processLibrary/).
processLibrary/processCampaign.js ✅ (~70KB executor): expands each campaign's target_audience into per-customer rows, applies exclusions (unsubscribe/DND), substitutes template variables (WhatsApp via processCampaignWhatsappHelper.js), and inserts rows into the channel queue collections with an execution_time. Writes MIS via campaignMis.js / campaignAggregation.js.
campaignController.js ✅ (traditional hub, 50+ fns): representative exports seen in routes — add_addo_campaign, createBulkCampaigns, campaignDetail, campaignOverView, getSmsSummary, updateTemplate, saveEmailCampaign, createWhatsappCampaign, calculateCampaignCount, trackPushCampaignClicks, trackPushCampaignOrders, cancelRetryCampaign, cleanPushNotificationTokensForAllCustomerSegment, repushNotificationTokensForAllCustomerSegment, add_config (creates business config), uploadLowWalletBalance. ⚠️ Full per-function contracts to be verified in code.
campaignControllerDynamic.js ✅ (4 fns): getAllDynamicCampaigns, getROIForCampaign, AnimatedCampaignHit (public microsite hit → increments Custom_Code_Campaign_MIS), animatedCampaignData. Uses dynamic coupon stores Dyanamic_Coupon_Campaign_*.
coldCampaignController.js ✅: uploadCampaignColdData (parses uploaded Excel, validates Indian mobiles with /^[6-9]\d{9}$/, max 200,000 records, writes recipients to cold_campaign_customers, creates a draft campaign addo_campaigns status:4; goes live as status:0 after approval), deleteDraftCampaign.
Call hierarchy:
routes/crm.js
└─ campaignController.add_addo_campaign → addo_campaigns (status:0)
processCampaignController (*/5)
└─ processCampaign(allCampaigns)
├─ processCampaignWhatsappHelper (WA templates)
├─ → sms_queue_* / whatsapp_queue_* / push_noti_queue_*
├─ campaignMis / campaignAggregation → campaign_mis
└─ sendSms → wallet_transaction (debit) [see SMS-Push]
autoCampaignRoiCrons/* → auto_campaign_roi
└─ uengageProcessDynamicCouponCodeROICalculationHelper (dynamic coupons)
ROI crons ✅ (crons/autoCampaignRoiCrons/, all early-morning IST):
| Cron | Schedule | Attributes ROI for |
|---|---|---|
autoCampaignRoi.js |
0 6 * * * (6:00) |
Birthday, Anniversary, Wallet-Expiry |
abandonCartRoi.js |
30 6 * * * (6:30) |
Abandoned Cart |
welcomeMsgRoi.js |
30 7 * * * (7:30) |
Welcome |
lostCustomersRoi.js |
0 0 * * * (midnight) |
Lost Customers (MOBILE_BATCH 1000 / PAGE 5000) |
referEarnRoi.js |
30 5 * * * (5:30) |
Refer & Earn (MySQL + Mongo) |
roiSnapshotBackfill.js |
backfill | Journeys missing roi_snapshot (dedup by addo_campaign_id) |
These attribute post-send order revenue back to campaigns/journeys within the attribution window. ⚠️ E1.1 (ROI rectification, P0) is open — validate windows and dedup before trusting these numbers.
Dynamic-coupon ROI ✅ — processLibrary/uengageProcessDynamicCouponCodeROICalculationHelper.js dynamicCouponCodeRoICalculationUpdation(data): on order completion with a dynamic coupon, looks up MySQL addo_promo_code_engine for campaignId+parent_business_id, marks Dyanamic_Coupon_Campaign_{parent}_{campaign}.orderCompleted=true and increments Custom_Code_Campaign_MIS.orderValue/countOrders.
9. Business Rules (hidden logic, configs, limits)¶
- Wallet debit per send 📄✅ — each message debits
business_config.wallet_balance($inc: -total) and writes awallet_transaction. Rates: SMS basewallet_config.sms_cost(default 0.145); WhatsApp utility 0.17 / marketing 0.91 (default marketing); Emailservice_id:10. GST applied unlessbusiness_config.gst_deduction == 0. Sends blocked on insufficient balance. - ROI attribution window 📄 — per-campaign configurable window (hours); revenue from orders inside the window is credited to the campaign. Dynamic per-campaign window is Roadmap B3.7 (Picked).
- Unsubscribe handling ✅ — customers in
unsubscribed_campaignsare excluded from that campaign's audience. - DND / blacklist 📄 —
business_config.blacklist_segmentsexcluded; SMS respectsdnd_flag=0, WhatsApp respectsoptin=1, Push needs FCM tokens (percrons/segments_count.js). - Scheduler throughput cap ✅ — max 10 SMS + 3 Push + 3 WA + 3 Email campaigns per 5-min tick; large libraries drain over multiple ticks (a known cause of "campaign sent late" — relates to E1.5).
- Bulk deprioritization ✅ — bulk SMS campaigns yield to non-bulk within the same tick.
- Web push vs App push ✅ — tracked separately in
campaign_mis(total_web_push_*vstotal_android_notifications/total_ios_notifications). See SMS-Push. - Permissions ✅ — most routes require
authMiddleware; a few tracking/hit endpoints (AnimatedCampaignHit,getSegmentDataCount,send/test/*) are intentionally open ⚠️. - Open bugs 📄 — E1.1 ROI, E1.5 builder budgeting/scheduling, E1.3 WhatsApp volume reports, E1.4 CRM stats. Treat MIS numbers as suspect until validated.
10. Performance¶
- Polling architecture ✅ — no broker;
processCampaignControllerpollsaddo_campaignsevery 5 min. Channel drain cadence: SMS*/3min (batch 8000), Push*/5min, WhatsApp*/3s(marketing) +*/1s(transactional). The 1-second WhatsApp poll is the hottest loop in the system. - Bottlenecks: (1) 5-min scheduler tick + per-tick caps → tail latency for big libraries; (2) unindexed
addo_campaignsscans; (3)processCampaign(~70KB) is synchronous audience expansion — large cohorts block the tick; (4) date-partitioned queues require the send crons to know today's/tomorrow's date. - Scaling: audience expansion is the heaviest step; wallet query for segments paginates (
LIMIT 1000).
11. Logging¶
trace_idcorrelates only synchronous HTTP requests (AsyncLocalStorage) — crons log their own lifecycle lines ("cron has picked N campaigns","cron has not picked any campaigns"✅).sms_summaryis the SMS delivery audit;fcmjunklogs push batches/errors;wallet_transactionis the money audit trail.- Grep patterns:
grep "picked .* campaigns" logs, and bycampaign_idacrosssms_summary/fcmjunk.
12. Monitoring¶
- Queue depth:
db.sms_queue_YYYYMMDD.countDocuments({processed:false}), same forpush_noti_queue_*(processed:false) andwhatsapp_queue_*(process_status:0). - Cron freshness:
cron_status/sms_cron_status/push_cron_status— check lateststart_time/end_time/status. Stuck-cron detection is reactive (~20 min) with email alert. - Backlog signal:
addo_campaignsdocs withstatus:0andscheduled_atwell in the past → scheduler falling behind.
13. Troubleshooting¶
| Issue | Root cause | Resolution | Command |
|---|---|---|---|
| Campaign never sent | status != 0 or scheduled_at in future; scheduler behind |
Verify doc state; check tick caps | db.addo_campaigns.find({_id:ObjectId(id)},{status:1,scheduled_at:1,campaignType:1}) |
| SMS sent but not delivered | Provider/DLT rejection | Inspect sms_summary.status (2=failed) |
db.sms_summary.find({campaign_id:id,status:2}) |
| Sent to unsubscribed user | Exclusion not applied | Confirm entry exists; audit processCampaign exclusion |
db.unsubscribed_campaigns.find({campaign_id:ObjectId(id)}) |
| ROI looks wrong | E1.1 open bug; window/dedup | Cross-check auto_campaign_roi vs orders in window |
inspect crons/autoCampaignRoiCrons/autoCampaignRoi.js |
| Wallet debited, no send | Crash between debit and enqueue (no distributed txn) | Reconcile wallet_transaction vs queue rows |
db.wallet_transaction.find({campaign_id:id}) |
| Big blast trickling out | Per-tick caps (10 SMS / 3 others) | Expected; monitor drain | db.sms_queue_*.countDocuments({processed:false}) |
14. FAQs¶
- Why three campaign pipelines? Historical accretion (♻️); Roadmap A2.1 merges Campaigns+Journeys into one hub.
- Where is the money debited? In
sendSms/ channel send path →wallet_transaction+business_config.wallet_balance. See SMS-Push §9. - How is App push different from Web push? Different token types/OS; counted separately in
campaign_mis. - Why is my campaign 15 minutes late? 5-min scheduler tick + per-tick caps + queue drain cadence stack up.
- Can I trust the ROI number? Not yet — E1.1 is an open P0.
15. Cheat Sheet¶
campaignType: 1=SMS 2=Push 3=Email 4=WhatsApp
status: 0 = scheduled/unsent
Scheduler: processCampaignController "*/5 * * * *" IST
Per-tick caps: SMS 10 (non-bulk first), Push/Email/WA 3
Executor: processLibrary/processCampaign.js
Queues: sms_queue_* (*/3m,8000) push_noti_queue_* (*/5m) whatsapp_queue_* (*/3s + */1s)
MIS: campaign_mis · sms_summary(mobile_number idx) · campaign_aggregation
ROI: crons/autoCampaignRoiCrons/* → auto_campaign_roi (E1.1 OPEN)
Money: wallet_transaction (debit) + business_config.wallet_balance
Exclude: unsubscribed_campaigns · blacklist_segments · dnd_flag/optin
Base path: /crm_api (routes/crm.js) auth: authMiddleware
Related Modules¶
- Automated-Journeys — event-triggered pipeline (shares queues & ROI crons)
- SMS-Push — channel delivery, wallet debit, DLT, FCM
- WhatsApp — WA send path (Gupshup/Meta), templates
- Loyalty — coupon/points tie-in for ROI
- Customer-Intelligence-RFM — audience segments/cohorts
- Database · Queues · API · Roadmap
Knowledge Tests¶
Level 1 — MCQs¶
- Which
campaignTypecode is WhatsApp? A) 1 B) 2 C) 3 D) 4 → D. (1=SMS, 2=Push, 3=Email, 4=WhatsApp; verified inprocessCampaignController.) - How often does the campaign scheduler run? A) 1s B) 3min C) 5min D) hourly → C.
"*/5 * * * *". - Which collection excludes opt-outs? A)
campaign_misB)unsubscribed_campaignsC)campaign_tagsD)sms_summary→ B. - Which field on
sms_summaryis indexed? A)campaign_idB)statusC)mobile_numberD)delivered_at→ C. - Per 5-min tick, max SMS campaigns picked? A) 3 B) 8000 C) 10 D) unlimited → C (others get 3; batch of 8000 is the SMS queue drain size, not campaign count).
Level 2 — Scenarios¶
- A client says a WhatsApp blast to 40k Champions "half went out this morning, half at noon." Explain why using the scheduler + queue architecture, and where you'd look. (Per-tick cap of 3 WA campaigns and audience expansion +
*/3sdrain; checkwhatsapp_queue_*process_status,addo_campaigns.status.) - Finance reports campaign ROI is double-counting revenue. Which cron and bug ID? (autoCampaignRoiCrons; E1.1, P0 — check attribution window & dedup.)
Level 3 — Code Reading¶
Open controllers/processCampaignController.js. (a) Why are non-bulk SMS campaigns fetched before bulk? (b) What happens if 15 SMS campaigns are due in one tick? (c) Which processLibrary function receives the collected campaigns?
Level 4 — Architecture¶
The three campaign pipelines write to the same channel queues but through different code paths. Propose how Roadmap A2.1 ("Central Marketing Hub") could unify them without breaking auto_campaign_roi attribution. Address audience expansion, MIS, and per-campaign ROI windows.
Level 5 — Production Debugging¶
A client's wallet was debited ₹4,200 but recipients report no SMS. sms_summary has rows with status:0. Walk through: (1) is the send cron running? (2) queue backlog? (3) provider/DLT error? (4) the missing-distributed-transaction risk between wallet_transaction and queue insertion. Give the exact Mongo commands you'd run.
Practical Assignments¶
- Trace a campaign end-to-end: create a test SMS campaign via
/crm_api/addCampaigns, then follow it throughaddo_campaigns→processCampaign→sms_queue_*→sms_summary, noting timestamps at each hop. - Add a missing index (proposal): write the
createIndexforaddo_campaigns(campaignType, status, scheduled_at)and estimate the scheduler-query improvement; do NOT run in prod. - ROI audit: for one campaign, manually compute attributed revenue from
addo_orderswithin the ROI window and compare toauto_campaign_roi; document any discrepancy against E1.1.