Skip to content

Referral — Refer & Earn

Validation legend: ✅ verified in code · 📄 Excel/spec-only · 💻 code-only · ♻️ duplicate · ⚰️ dead code · ⚠️ risk/known-issue

[!NOTE] Referral (Roadmap D1.5, Live New) is not a standalone module — it is a feature of Loyalty. It reuses the loyalty engine end-to-end: config lives in MySQL referral_rules, processing rides the shared crm_points_allocation_queue (type = refer_earn), and point credit goes through the same handleWalletPointsFlow. Read Loyalty first. The referral-specific surface is: config APIs in loyaltyController, the referral_queue/referral_rules MySQL tables, and two MIS/ROI crons.


1. Overview

Refer & Earn is a dual-incentive program: both the referrer and the referee earn a reward, but only after the referee completes a qualifying first purchase. This prevents gaming (no reward for a signup that never buys) and aligns the incentive with actual revenue.

Config (✅ loyaltyController): referral_bonus_type, referral_value, is_recursive (whether referred customers can themselves refer).


2. Business Flow

2.1 Example journey

flowchart LR
    A[Referrer shares code/link] --> B[Referee signs up<br/>with referral attribution]
    B --> C[Referee places<br/>QUALIFYING first order]
    C --> D[Referral qualifies →<br/>referral_queue processed]
    D --> E[Enqueue refer_earn ×2<br/>crm_points_allocation_queue]
    E --> F[handleWalletPointsFlow credits<br/>referrer + referee wallets]
    F --> G[referalMIS 04:17 counts<br/>referEarnRoi 05:30 attributes revenue]
  1. Refer. Referrer shares their code/link (surfaced via loyalty microsite / prismIntegration).
  2. Sign up. Referee registers with the referral attribution captured.
  3. Qualify. Reward is withheld until the referee's qualifying first purchase — this is the trigger, matching the D1.5 spec and Business-Flow "triggered after referee qualifying first purchase".
  4. Earn (dual). On qualification, two crm_points_allocation_queue rows (type=refer_earn) are created — one for each party — and credited via the standard loyalty flow.
  5. Measure. referalMIS counts daily referrals; referEarnRoi attributes revenue to the program.

2.2 Edge cases

Case Rule Validation
Referee signs up but never buys No reward — reward gated on qualifying first purchase ✅ (D1.5 trigger)
Referrer is blocked/fraud checkCustomerIsBlocked gates the credit ✅ (shared loyalty gate)
Recursive referral Referred customers may refer only if is_recursive set ✅ config flag
Self-referral / abuse Should be blocked by fraud/velocity rules (see Fraud-Prevention) ⚠️ verify enforcement
Non-atomic credit Same dual-store risk as loyalty (E1.10 family) ⚠️

3. Technical Flow

SOURCE:   referee's qualifying first order
TRIGGER:  referral qualifies → MySQL referral_queue row (processed=0)
QUEUE:    two crm_points_allocation_queue rows, type=refer_earn (referrer + referee)
CRON:     crmPointsAllocationQueueCron (*/2, batch 5000, retry x5)
            type refer_earn → manual wallet_history insert + wallet update
            (referral value from referral_rules; credited via loyalty ledger)
DB:       MySQL wallet / wallet_history (points) ; referral_queue (processed=1, processedAt)
MIS:      referalMIS (04:17) → MongoDB referal_mis {parent_business_id, total_referrals, date}
ROI:      referEarnRoi (05:30) → refer & earn revenue attribution
CONFIG:   loyaltyController get/save ReferalRules → MySQL referral_rules

Business flag: business_configs.refer_and_earn = 1 enables tracking/MIS.


4. Architecture Diagram

flowchart TB
    REF[Referrer code/link] --> SIGN[Referee signup + attribution]
    SIGN --> BUY[Referee QUALIFYING first order]
    BUY --> RQ[(MySQL referral_queue<br/>processed=0)]
    RQ --> Q[(crm_points_allocation_queue<br/>type=refer_earn ×2)]
    Q -->|*/2 min, batch 5000, retry x5| CRON[crmPointsAllocationQueueCron]
    CRON --> WAL[(MySQL wallet + wallet_history<br/>referrer & referee)]
    CRON --> RQD[(referral_queue processed=1)]
    RQD --> MIS[referalMIS 04:17 → referal_mis]
    RQD --> ROI[referEarnRoi 05:30 → ROI]
    CFG[(MySQL referral_rules<br/>referral_bonus_type, referral_value, is_recursive)] --> CRON

5. Folder Structure (real files)

uengage-crm/
├── controllers/
│   └── loyaltyController.js   ✅ getReferalRules, saveReferalRules,
│                                 getReferalCustomers, getReferalCustomersMIS
├── commonFunctions/
│   ├── handleWalletPointsFlow.js   ✅ credits refer_earn points (shared engine)
│   └── rewardHelper.js             ✅ promo/reward generation (shared)
├── crons/
│   ├── crmPointsAllocationQueueCron.js  ✅ processes type=refer_earn (*/2, 5000, retry x5)
│   ├── referalMIS.js                    ✅ 04:17 daily referral count → referal_mis
│   └── referEarnRoi.js                  ✅ 05:30 refer & earn ROI
└── (no standalone referralController)   ⚰️ — referral is embedded in loyalty

6. Database

Store Object Key fields Purpose
MySQL referral_rules parentBusinessId, referral_bonus_type, referral_value, is_recursive program config
MySQL referral_queue parentBusinessId, processed (0/1), processedAt qualification/processing queue
MySQL crm_points_allocation_queue type=refer_earn, status, retry_count, next_retry_at shared point-credit queue
MySQL wallet / wallet_history credited points for both parties ledger (see Loyalty)
MongoDB referal_mis parent_business_id, total_referrals, date daily MIS
MongoDB business_configs refer_and_earn flag enable/disable

Relationships: referrer & referee are both customers (mobileNoaddo_contacts_mapping), each with a wallet. referral_queue → two crm_points_allocation_queue rows.

Indexes: ⚠️ referral_queue(parentBusinessId, processed, processedAt) recommended (MIS + drain scan by these).

Common query (daily referrals — ✅ referalMIS):

SELECT COUNT(*) FROM referral_queue
WHERE parentBusinessId=? AND processed=1
  AND processedAt BETWEEN :startOfDay AND :endOfDay;


7. APIs

loyaltyController referral endpoints (prism_routes/loyalty_rewards_routes.js, authMiddleware). Verify in route file.

Path Handler Purpose
/get/referal/rules getReferalRules fetch referral_bonus_type, referral_value, is_recursive
/save/referal/rules saveReferalRules upsert referral_rules
/get/referal/customers getReferalCustomers list processed referrals (status=1)
/get/referal/customers/mis getReferalCustomersMIS referral MIS from MongoDB referal_mis

Customer-facing referral (share code, balance) is surfaced via routes/prismIntegration.js loyalty endpoints (/get_user_loyalty etc.) — see Loyalty §7.

Example — saveReferalRules (verify):

// request
{ "parentBusinessId": 123, "referral_bonus_type": "points", "referral_value": 50, "is_recursive": true }
// response
{ "status": true, "message": "Referral rules saved" }
Failures: invalid referral_value; missing parentBusinessId.


8. Code Walkthrough

loyaltyController.saveReferalRules / getReferalRules
  → MySQL referral_rules (referral_bonus_type, referral_value, is_recursive)

qualifying first purchase
  → referral_queue row (processed=0)
  → crm_points_allocation_queue: 2 rows type=refer_earn (referrer + referee)

crmPointsAllocationQueueCron  (*/2, batch 5000, MAX_RETRIES=5, 15-min backoff)
  case "refer_earn":
    read referral value ← referral_rules
    manual wallet_history insert (txn_type=1) + wallet available/total_points +=
    checkCustomerIsBlocked gate
    referral_queue.processed=1, processedAt=now

referalMIS  (04:17)  → count processed referrals/day → MongoDB referal_mis
referEarnRoi (05:30) → attribute revenue from referred customers' orders

Dependencies: shared loyalty engine (handleWalletPointsFlow, wallet/wallet_history), fraudRulesController.checkCustomerIsBlocked, MySQL referral_rules/referral_queue, MongoDB referal_mis/business_configs.


9. Business Rules

Rule Value Validation
Dual incentive both referrer + referee earn ✅ D1.5
Reward trigger referee's qualifying first purchase (not signup) ✅ D1.5
Bonus config referral_bonus_type + referral_value
Recursive referrals only if is_recursive
Enable flag business_configs.refer_and_earn=1
Fraud gate checkCustomerIsBlocked before credit
Queue drain type=refer_earn, */2, batch 5000, retry ×5

Permissions: rule config = Admin/Marketing (authMiddleware); customer share/redeem = customer (appMiddleware/prismIntegration). Hidden logic (⚠️): self-referral/abuse prevention relies on the general fraud/velocity engine, not a referral-specific check — verify coverage in Fraud-Prevention.

10. Performance

  • Referral credit inherits the loyalty queue's throughput (*/2 min, 5000/batch). Under a referral campaign spike, watch crm_points_allocation_queue depth for type=refer_earn.
  • referalMIS/referEarnRoi are daily aggregates — cheap, but scan referral_queue by date; add the composite index.

11. Logging

  • crm_points_allocation_queue status transitions are the audit trail for referral credits (filter type='refer_earn').
  • referral_queue.processed/processedAt shows qualification timing.
  • Winston logs in referalMIS/referEarnRoi.

12. Monitoring

  • Pending referral credits: SELECT COUNT(*) FROM crm_points_allocation_queue WHERE type='refer_earn' AND status='pending';
  • Daily referrals: MongoDB referal_mis trend.
  • Unqualified backlog: SELECT COUNT(*) FROM referral_queue WHERE processed=0;

13. Troubleshooting

Issue Cause Fix Commands
Referral reward not credited referee hasn't made qualifying first purchase expected until qualification SELECT * FROM referral_queue WHERE parentBusinessId=? AND processed=0;
Only one party got points one refer_earn row failed/blocked inspect both queue rows + fraud block SELECT * FROM crm_points_allocation_queue WHERE type='refer_earn' AND ...;
MIS shows 0 referrals refer_and_earn≠1 or none processed that day enable flag / check processing db.business_configs.findOne({...},{refer_and_earn:1})
Recursive referrals not chaining is_recursive false enable in referral_rules SELECT is_recursive FROM referral_rules WHERE parentBusinessId=?;
Suspected referral abuse no referral-specific abuse rule tighten velocity/blocklist see Fraud-Prevention

14. FAQs

Q: When exactly does the reward fire? On the referee's qualifying first purchase, not at signup.

Q: Do both parties earn? Yes — dual incentive, one refer_earn queue row each.

Q: Where is referral config? MySQL referral_rules (referral_bonus_type, referral_value, is_recursive).

Q: Is there a referral controller? No — referral is embedded in loyaltyController + the shared points queue.

Q: How is abuse prevented? Via the general fraud engine (checkCustomerIsBlocked + velocity limits), not a referral-specific gate. ⚠️ Verify coverage.

15. Cheat Sheet

Trigger:  referee's QUALIFYING first purchase (not signup)
Incentive: dual — referrer + referee both earn
Config:   MySQL referral_rules (referral_bonus_type, referral_value, is_recursive)
Enable:   business_configs.refer_and_earn = 1
Queue:    referral_queue → crm_points_allocation_queue type=refer_earn (×2) → handleWalletPointsFlow
Drain:    */2 min, batch 5000, retry x5
MIS:      referalMIS 04:17 → referal_mis ; referEarnRoi 05:30
APIs:     loyaltyController get/save/... ReferalRules  (no standalone controller)
Abuse:    handled by general fraud engine (verify)


Knowledge Tests

Level 1 — MCQs

  1. When does a referral reward fire? a) at signup b) on the referee's qualifying first purchase c) after 30 days d) on referrer's next order Answer: b.

  2. Referral is implemented as… a) referralController.js b) a feature of Loyalty (shared queue + engine) c) a Kafka topic d) a frontend-only feature Answer: b. No standalone controller.

  3. The queue type used for referral credits is… a) uen_order b) order_reward c) refer_earn d) welcome_point Answer: c.

  4. is_recursive controls… a) retry count b) whether referred customers can refer others c) FIFO order d) GST Answer: b.

  5. Daily referral counts are written to… a) referral_queue b) referal_mis (MongoDB) c) loyalty_mis d) Redis Answer: b. By referalMIS (04:17).

Level 2 — Scenarios

  1. A referrer says "my friend signed up with my code a week ago but I got nothing." Expected: the reward is gated on the referee's qualifying first purchase; check referral_queue for a processed=0 row (still unqualified). No purchase = no reward by design.

  2. Referral MIS shows counts, but wallets weren't credited. Expected: referral_queue.processed=1 but the crm_points_allocation_queue refer_earn rows are pending/failed (drain lag or fraud block). Inspect both queue rows.

Level 3 — Code Reading

In crmPointsAllocationQueueCron, the refer_earn case reads the referral value from referral_rules and does a manual wallet_history insert (not handleWalletPointsFlow's full earn path). Question: what invariant risk does this manual path share with uen_order, and which detector query catches it? Expected: same non-atomic dual-store drift; detector: SELECT * FROM wallet WHERE total_points <> available+used+expired; (E1.10 family).

Level 4 — Architecture

Referral has no dedicated abuse control — it leans on the shared fraud engine. Design a referral-specific anti-abuse layer (self-referral, ring fraud, device fingerprint) that fits Prism's queue-based flow and the A2.2 "Loyalty & Trust" merge. Expected: pre-qualification checks (same-device/same-phone, referrer≠referee, velocity per referrer), integrate into the refer_earn case before credit, log to fraudulent_customers.

Level 5 — Production Debugging

Ticket: "A referral campaign went out; referrals spiked but points aren't landing for one parent business." Expected reasoning: filter crm_points_allocation_queue by that business + type='refer_earn' → likely a poison row / mass failed from a missing referral_rules config or fraud block; confirm refer_and_earn=1, referral_value set, cron not stuck, jobsPool not exhausted. Fix config/poison row, re-drain.


Practical Assignments

  1. Trace a dual credit. In a test env, mark a referral_queue row processed and confirm two refer_earn rows are created and both wallets credited; document each write.
  2. Build the qualification report. Write a query joining referral_queue (processed) with referred customers' first-order date to measure time-to-qualify and qualification rate per parent business.
  3. Anti-abuse prototype. Add a check (self-referral + per-referrer daily cap) in the refer_earn path and log violations to fraudulent_customers; cross-link to Fraud-Prevention.