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 sharedcrm_points_allocation_queue(type = refer_earn), and point credit goes through the samehandleWalletPointsFlow. Read Loyalty first. The referral-specific surface is: config APIs inloyaltyController, thereferral_queue/referral_rulesMySQL 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]
- Refer. Referrer shares their code/link (surfaced via loyalty microsite /
prismIntegration). - Sign up. Referee registers with the referral attribution captured.
- 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".
- Earn (dual). On qualification, two
crm_points_allocation_queuerows (type=refer_earn) are created — one for each party — and credited via the standard loyalty flow. - Measure.
referalMIScounts daily referrals;referEarnRoiattributes 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 (mobileNo → addo_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¶
loyaltyControllerreferral 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" }
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_queuedepth fortype=refer_earn. referalMIS/referEarnRoiare daily aggregates — cheap, but scanreferral_queueby date; add the composite index.
11. Logging¶
crm_points_allocation_queuestatus transitions are the audit trail for referral credits (filtertype='refer_earn').referral_queue.processed/processedAtshows 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_mistrend. - 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)
Related Modules¶
- Loyalty — parent module; referral reuses its engine, queue, and ledger.
- Wallet — points land in the customer wallet.
- Fraud-Prevention — the only guard against referral abuse today.
- Customer-CRM — referrer/referee are customer profiles.
- Queues · Database · Roadmap D1.5
Knowledge Tests¶
Level 1 — MCQs¶
-
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.
-
Referral is implemented as… a)
referralController.jsb) a feature of Loyalty (shared queue + engine) c) a Kafka topic d) a frontend-only feature Answer: b. No standalone controller. -
The queue type used for referral credits is… a)
uen_orderb)order_rewardc)refer_earnd)welcome_pointAnswer: c. -
is_recursivecontrols… a) retry count b) whether referred customers can refer others c) FIFO order d) GST Answer: b. -
Daily referral counts are written to… a)
referral_queueb)referal_mis(MongoDB) c)loyalty_misd) Redis Answer: b. ByreferalMIS(04:17).
Level 2 — Scenarios¶
-
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_queuefor aprocessed=0row (still unqualified). No purchase = no reward by design. -
Referral MIS shows counts, but wallets weren't credited. Expected:
referral_queue.processed=1but thecrm_points_allocation_queuerefer_earnrows arepending/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¶
- Trace a dual credit. In a test env, mark a
referral_queuerow processed and confirm tworefer_earnrows are created and both wallets credited; document each write. - 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. - Anti-abuse prototype. Add a check (self-referral + per-referrer daily cap) in the
refer_earnpath and log violations tofraudulent_customers; cross-link to Fraud-Prevention.