Skip to content

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" Email
"4" WhatsApp

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.jsroutes/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"
}
// 200
{ "status": true, "campaign_id": "665f...", "message": "Campaign scheduled" }

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 a wallet_transaction. Rates: SMS base wallet_config.sms_cost (default 0.145); WhatsApp utility 0.17 / marketing 0.91 (default marketing); Email service_id:10. GST applied unless business_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_campaigns are excluded from that campaign's audience.
  • DND / blacklist 📄 — business_config.blacklist_segments excluded; SMS respects dnd_flag=0, WhatsApp respects optin=1, Push needs FCM tokens (per crons/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_* vs total_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; processCampaignController polls addo_campaigns every 5 min. Channel drain cadence: SMS */3 min (batch 8000), Push */5 min, 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_campaigns scans; (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_id correlates only synchronous HTTP requests (AsyncLocalStorage) — crons log their own lifecycle lines ("cron has picked N campaigns", "cron has not picked any campaigns" ✅).
  • sms_summary is the SMS delivery audit; fcmjunk logs push batches/errors; wallet_transaction is the money audit trail.
  • Grep patterns: grep "picked .* campaigns" logs, and by campaign_id across sms_summary / fcmjunk.

12. Monitoring

  • Queue depth: db.sms_queue_YYYYMMDD.countDocuments({processed:false}), same for push_noti_queue_* (processed:false) and whatsapp_queue_* (process_status:0).
  • Cron freshness: cron_status / sms_cron_status / push_cron_status — check latest start_time/end_time/status. Stuck-cron detection is reactive (~20 min) with email alert.
  • Backlog signal: addo_campaigns docs with status:0 and scheduled_at well 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


Knowledge Tests

Level 1 — MCQs

  1. Which campaignType code is WhatsApp? A) 1 B) 2 C) 3 D) 4 → D. (1=SMS, 2=Push, 3=Email, 4=WhatsApp; verified in processCampaignController.)
  2. How often does the campaign scheduler run? A) 1s B) 3min C) 5min D) hourly → C. "*/5 * * * *".
  3. Which collection excludes opt-outs? A) campaign_mis B) unsubscribed_campaigns C) campaign_tags D) sms_summaryB.
  4. Which field on sms_summary is indexed? A) campaign_id B) status C) mobile_number D) delivered_atC.
  5. 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

  1. 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 + */3s drain; check whatsapp_queue_* process_status, addo_campaigns.status.)
  2. 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

  1. Trace a campaign end-to-end: create a test SMS campaign via /crm_api/addCampaigns, then follow it through addo_campaignsprocessCampaignsms_queue_*sms_summary, noting timestamps at each hop.
  2. Add a missing index (proposal): write the createIndex for addo_campaigns (campaignType, status, scheduled_at) and estimate the scheduler-query improvement; do NOT run in prod.
  3. ROI audit: for one campaign, manually compute attributed revenue from addo_orders within the ROI window and compare to auto_campaign_roi; document any discrepancy against E1.1.