Fraud Prevention — Velocity Limits, Blocklists & Alerts¶
Validation legend: ✅ verified in code · 📄 Excel/spec-only · 💻 code-only · ♻️ duplicate · ⚰️ dead code · ⚠️ risk/known-issue
[!NOTE] Fraud Prevention protects the loyalty economy: it stops customers from farming/burning points faster than allowed, and lets clients blocklist bad actors. It is tightly coupled to Loyalty — every point allocation and redemption calls into
fraudRulesController.checkCustomerIsBlocked/checkCustomerFraud. Roadmap A2.2 merges this module into loyalty as "Loyalty & Trust" (P1).
1. Overview¶
Two enforcement mechanisms + alerting:
- Velocity limits (
checkCustomerFraud) — cap redemptions and point-earning events per period (hour/day/week) per customer, configured in MySQLwallet_rules. Breaching flags the customer into MongoDBfraudulent_customers. - Blocklist (
checkCustomerIsBlocked) — hard block by phone number (manual entry or CSV bulk upload), stored inblocklist_contactsand reflected oncustomers.is_blocked. - Alerts (
fraud_alert_settings+sendAlert) — notify configured recipients (SMS/WhatsApp) when fraud is detected; the send debits the business wallet.
fraudRulesController exports (✅): configureFraudWalletRules, configureFraudAlertSettings, getFraudConfiguration, addBlocklistContacts, blockUnblockAndResolveContacts, markAndResolveContacts, deleteBlocklistContacts, listBlocklistContacts, checkCustomerFraud, sendAlert, checkCustomerIsBlocked, getCustomerBlocked, evaluateCustomerFraud, sendAlertForFraudCustomer, viewFraudulentCustomer, groupListFraudContacts, fraudListCustomersMis.
2. Business Flow¶
2.1 Example journey¶
flowchart LR
A[Admin sets velocity limits<br/>configureFraudWalletRules] --> B[Customer earns/redeems]
B --> C[Loyalty engine calls<br/>checkCustomerIsBlocked + checkCustomerFraud]
C --> D{limit breached?}
D -->|no| E[Allow]
D -->|yes| F[Flag fraudulent_customers<br/>fraud_type + block_reason]
F --> G[sendAlert SMS/WA<br/>debits business wallet]
G --> H[Admin reviews →<br/>block / unblock / resolve]
2.2 Edge cases¶
| Case | Rule | Validation |
|---|---|---|
| Redemption over limit | count txn_type=2 in period > redemption_limit_value → flag "Redemption Frequency Limit" |
✅ |
| Earning over limit | count txn_type=1 in period > points_earning_limit_value → flag "Points Earning Limit" |
✅ |
| Blocklisted phone | checkCustomerIsBlocked returns null → loyalty action denied |
✅ |
| CSV upload garbage | phone must be 10 digits, not starting 0–5 (landline filter); batch 1000 | ✅ |
| System vs manual flag | blocked_by = "System" (auto) or user id (manual) |
✅ |
| Alert with low wallet | send skipped if wallet_balance < channel_cost |
✅ |
| Block/unblock | status 2=block (customers.is_blocked=1), 3=unblock; both resolve the flag |
✅ |
3. Technical Flow¶
CONFIG: configureFraudWalletRules → wallet_rules (redemption/earning limit value+period)
configureFraudAlertSettings → fraud_alert_settings (recipients, channel, template)
addBlocklistContacts (CSV/manual) → blocklist_contacts
ENFORCE (called by Loyalty engine on every earn/redeem):
checkCustomerIsBlocked(parent_business_id, mobileNo, customer_details) → null if blocked
checkCustomerFraud(connection, wallet_rules, wallet_id, parent_business_id, child_business_id, mobileNo, type)
type="redemption" → count wallet_history txn_type=2 in period vs redemption_limit_value
type="allocation" → count wallet_history txn_type=1 in period vs points_earning_limit_value
breach → insert fraudulent_customers {fraud_type, block_reason, blocked_by:"System", status:1}
ALERT: sendAlert(parent_business_id, db, contacts)
SMS: deduct sms_cost+gst → sms_queue_YYYYMMDD + wallet_transactions
WA: deduct whatsapp_cost/utility+gst → whatsapp_queue_YYYYMMDD + wallet_transactions
REVIEW: blockUnblockAndResolveContacts (status 2 block / 3 unblock) → customers.is_blocked ; fraudulent_customers.status=2
MIS: fraudListCustomersMis, groupListFraudContacts, viewFraudulentCustomer
Related revocation crons (protect the economy):
- posRedemptionRevokeCron (/5 min) — reverts POS loyalty/coupon redemptions where the POS order never arrived (pending_redemptions/pending_coupon_redemptions past expires_at) → restores points, wallet_history status=2.
- whatsappRefund* (00:00) — refunds failed WhatsApp sends (also touches fraud-alert sends).
4. Architecture Diagram¶
flowchart TB
subgraph cfg["Config (Admin)"]
WR[(wallet_rules<br/>redemption/earning limits + period)]
FAS[(fraud_alert_settings<br/>recipients/channel/template)]
BL[(blocklist_contacts<br/>manual + CSV)]
end
subgraph enforce["Enforcement (per loyalty action)"]
CIB[checkCustomerIsBlocked] --> BL
CCF[checkCustomerFraud] --> WR
CCF --> WH[(wallet_history<br/>count txn_type by period)]
end
LOY[Loyalty earn/redeem] --> CIB
LOY --> CCF
CCF -->|breach| FC[(fraudulent_customers<br/>fraud_type, blocked_by System)]
FC --> AL[sendAlert SMS/WA]
AL --> WT[(wallet_transactions debit)]
FC --> REV[Admin review<br/>block/unblock/resolve]
REV --> CUST[(customers.is_blocked)]
PRC[posRedemptionRevokeCron */5] --> WH
stateDiagram-v2
[*] --> Clean
Clean --> Flagged: velocity breach (status=1, blocked_by=System)
Flagged --> Blocked: admin status=2 (is_blocked=1)
Flagged --> Resolved: admin status=2 (resolve, comment)
Blocked --> Unblocked: admin status=3 (is_blocked unset)
Unblocked --> Clean
5. Folder Structure (real files)¶
uengage-crm/
├── controllers/
│ └── fraudRulesController.js ✅ config, blocklist, checkCustomerFraud, checkCustomerIsBlocked, alerts, MIS
├── routes/prism_routes/
│ └── fraud_rules_routes.js ✅ all fraud routes (authMiddleware; upload uses file middleware)
├── crons/
│ ├── posRedemptionRevokeCron.js ✅ */5 revert stale POS redemptions/coupons
│ └── whatsappRefund.js ✅ 00:00 refund failed WA sends
└── (consumers) loyaltyController.js, prismIntegration.js, posistLoyaltyIntegration.js, handleWalletPointsFlow.js
✅ all call checkCustomerIsBlocked / checkCustomerFraud
6. Database¶
| Store | Object | Key fields | Purpose |
|---|---|---|---|
| MySQL | wallet_rules |
redemption_limit_value, redemption_limit_period, points_earning_limit_value, points_earning_limit_period (period: hour/day/week) |
velocity config |
| MySQL | wallet_history |
wallet_id, parentBusinessId, txn_type (1/2), event_date/event_hour/event_month |
velocity counting source |
| MongoDB | fraudulent_customers |
contact, name, fraud_type ("Redemption Frequency Limit"/"Points Earning Limit"), parent_business_id, child_business_id, block_reason, blocked_by (System/user), status (1=pending,2=resolved), comment, resolved_comment, submitted_by, created_at, updated_at |
flagged customers |
| MongoDB | blocklist_contacts |
parent_business_id, contact_number, source (manual/csv), timestamps |
hard blocklist |
| MongoDB | fraud_alert_settings |
parent_business_id, alert_recipients[], channel_type (sms/whatsapp), template, sender_id, template_type (UTILITY/MARKETING) |
alerting config |
| MongoDB | customers |
is_blocked |
block enforcement flag |
Relationships: fraudulent_customers.contact / blocklist_contacts.contact_number = 10-digit mobileNo → customers. Velocity counts join wallet_history by wallet_id + parentBusinessId.
Indexes: ⚠️ wallet_history(wallet_id, parentBusinessId, txn_type, event_date) critical — velocity check runs on every earn/redeem. blocklist_contacts(parent_business_id, contact_number) for O(1) block lookup.
Common query (velocity — ✅ checkCustomerFraud, simplified):
SELECT COUNT(*) FROM wallet_history
WHERE wallet_id=? AND parentBusinessId=? AND txn_type=2 -- redemptions
AND event_date >= :period_start; -- hour/day/week window
-- breach if COUNT >= redemption_limit_value
7. APIs¶
routes/prism_routes/fraud_rules_routes.js— allauthMiddlewareexcept CSV upload (file middleware). Verify.
| Method | Path | Handler |
|---|---|---|
| POST | /configure_fraud_wallet_rules |
configureFraudWalletRules |
| POST | /configure_fraud_alert_settings |
configureFraudAlertSettings |
| POST | /get_fraud_configuration |
getFraudConfiguration |
| POST (multipart) | /upload_blocklist_contacts |
addBlocklistContacts |
| POST | /delete_blocklist_contacts |
deleteBlocklistContacts |
| POST | /block_unblock_resolve_contacts |
blockUnblockAndResolveContacts |
| POST | /mark_resolve_contacts |
markAndResolveContacts |
| POST | /get_blocklist_contacts |
listBlocklistContacts |
| GET | /fraud/check-customer-blocked?contact= |
getCustomerBlocked |
| POST | /api/v1/fraud/evaluate-customer |
evaluateCustomerFraud |
| POST | /send_fraud_alert |
sendAlertForFraudCustomer |
| POST | /view_fraud_customers |
viewFraudulentCustomer |
| POST | /group_list_fraud_contacts |
groupListFraudContacts |
| POST | /get_fraud_list_mis |
fraudListCustomersMis |
Example — configure_fraud_wallet_rules (verify):
// request
{ "parentBusinessId": 123,
"redemption_limit_value": 3, "redemption_limit_period": "day",
"points_earning_limit_value": 5, "points_earning_limit_period": "day" }
// response
{ "status": true, "message": "Fraud wallet rules configured" }
Phone; each value 10 digits, not starting 0–5; processed in 1000-row batches; source="csv".
Failures: invalid phone rows skipped/reported; alert send skipped if wallet_balance < channel_cost.
8. Code Walkthrough¶
checkCustomerFraud(connection, wallet_rules, wallet_id, parent_business_id, child_business_id, mobileNo, type)
├─ type "redemption": COUNT wallet_history txn_type=2 in redemption_limit_period vs redemption_limit_value
├─ type "allocation": COUNT wallet_history txn_type=1 in points_earning_limit_period vs points_earning_limit_value
└─ breach → insert fraudulent_customers {fraud_type, block_reason, blocked_by:"System", status:1}
returns { status_code, status, fraud_detected, action_taken }
checkCustomerIsBlocked(parent_business_id, mobileNo, customer_details)
→ lookup blocklist_contacts / customers.is_blocked → null if blocked, else { status: 1 }
addBlocklistContacts (CSV or manual)
→ validate phones (10 digits, not 0-5 start) → insert blocklist_contacts in 1000 batches (source csv/manual)
blockUnblockAndResolveContacts (status 2 block / 3 unblock)
→ set/unset customers.is_blocked ; fraudulent_customers.status=2 (+comment, submitted_by)
sendAlert(parent_business_id, db, contacts)
→ wallet_balance check → deduct (findOneAndUpdate $inc) → sms_queue_* / whatsapp_queue_* + wallet_transactions
Callers (enforcement points): loyaltyController (allocation/redemption), prismIntegration (configureLoyaltyPoints, purchaseRewards, purchaseLoyaltyCards), posistLoyaltyIntegration, handleWalletPointsFlow. Dependencies: MySQL wallet_rules/wallet_history, MongoDB fraudulent_customers/blocklist_contacts/fraud_alert_settings/customers/wallet_transactions, SMS/WA queues, CSV parser.
9. Business Rules¶
| Rule | Value | Validation |
|---|---|---|
| Redemption velocity | redemption_limit_value per redemption_limit_period (hour/day/week) |
✅ |
| Earning velocity | points_earning_limit_value per points_earning_limit_period |
✅ |
| Auto-flag | breach → fraudulent_customers blocked_by="System", status=1 |
✅ |
| Hard block | blocklist_contacts / customers.is_blocked denies loyalty actions |
✅ |
| CSV rules | 10-digit, not starting 0–5; batch 1000; source="csv" |
✅ |
| Block/unblock | status 2 block / 3 unblock; both resolve flag | ✅ |
| Alert channels | SMS or WhatsApp; debits business wallet; skipped if insufficient | ✅ |
Feature flags/permissions: all config = Admin (authMiddleware); enforcement is automatic on every loyalty action.
Hidden logic (💻/⚠️): checkCustomerFraud inserts a flag as a side effect during a normal loyalty transaction — a read that writes. Blocklist landline filter (reject phones starting 0–5) can drop valid mobiles in rare numbering edge cases. GET /fraud/check-customer-blocked is a GET (query param) while everything else is POST — auth/exposure worth reviewing (A2.2).
10. Performance¶
checkCustomerFraudruns awallet_historyCOUNT on every earn/redeem ⚠️ — without(wallet_id, txn_type, event_date)index this is a hot scan; a top loyalty performance concern.- Blocklist lookup per action needs
(parent_business_id, contact_number)index. - CSV upload batches 1000 — fine; alerts go through the async SMS/WA queues (non-blocking).
11. Logging¶
- Auto-flags are self-documenting rows in
fraudulent_customers(fraud_type,block_reason,blocked_by,created_at). - Block/unblock/resolve capture
submitted_by,comment,resolved_comment— full admin audit. - Alert sends logged in
wallet_transactions(debit) + the channel queues.
12. Monitoring¶
- Pending flags:
db.fraudulent_customers.count({ status: 1 })(unreviewed). - Auto vs manual: group by
blocked_by. - Blocklist size:
db.blocklist_contacts.count({ parent_business_id: pid }). - Velocity hotspots: businesses with many
fraud_type="Points Earning Limit"flags = possible farming attack. - Revocation cron:
cron_statuses {cron_data:"pos_redemption_revoke"}forposRedemptionRevokeCron.
13. Troubleshooting¶
| Issue | Cause | Fix | Commands |
|---|---|---|---|
| Legit customer can't earn/redeem | on blocklist_contacts or auto-flagged |
verify + unblock (status 3) | db.fraudulent_customers.find({contact:"98..."}) |
| Fraud not caught | limits unset or period wrong | set *_limit_value/_period |
SELECT redemption_limit_value,redemption_limit_period FROM wallet_rules WHERE parentBusinessId=?; |
| Alerts not sent | insufficient wallet or no recipients/config | recharge; set fraud_alert_settings |
db.fraud_alert_settings.findOne({parent_business_id:pid}) |
| CSV rows dropped | phone !=10 digits or starts 0–5 | fix source file | grep -n "10\|Phone" controllers/fraudRulesController.js |
| Loyalty slow under load | checkCustomerFraud COUNT scan |
add wallet_history composite index |
see §10 |
| Points not restored after POS cancel | posRedemptionRevokeCron not run / not stale yet |
wait 5-min cron; check pending_redemptions.expires_at |
grep -n "*/5" crons/posRedemptionRevokeCron.js |
14. FAQs¶
Q: Difference between checkCustomerIsBlocked and checkCustomerFraud? The former is a hard block lookup (blocklist/is_blocked); the latter is a velocity check that flags on breach.
Q: Does a velocity check block immediately? It flags into fraudulent_customers (status=1) and can trigger an alert; an admin then decides to hard-block (status=2).
Q: What periods are supported? hour / day / week.
Q: Do fraud alerts cost money? Yes — SMS/WhatsApp sends debit the business wallet.
Q: Where is this heading? Merged into loyalty as "Loyalty & Trust" (Roadmap A2.2, P1).
15. Cheat Sheet¶
Two gates: checkCustomerIsBlocked (hard block) + checkCustomerFraud (velocity)
Velocity: wallet_rules redemption/earning limit value+period (hour/day/week) ; counts wallet_history txn_type
Flag: fraudulent_customers (fraud_type, blocked_by System, status 1)
Block: blocklist_contacts + customers.is_blocked ; block=status2 / unblock=status3
CSV: Phone col, 10 digits, not 0-5 start, batch 1000, source=csv
Alerts: fraud_alert_settings → SMS/WA (debits business wallet)
Crons: posRedemptionRevokeCron */5 (revert stale) · whatsappRefund 00:00
Perf risk: checkCustomerFraud COUNT scans wallet_history on EVERY earn/redeem → index it
Routes: prism_routes/fraud_rules_routes.js (GET /fraud/check-customer-blocked; rest POST)
Roadmap: A2.2 → Loyalty & Trust
Related Modules¶
- Loyalty — every earn/redeem calls the fraud gates; A2.2 merges them.
- Referral — relies on this engine for abuse control (no referral-specific guard).
- Wallet — fraud alerts debit the business wallet.
- Customer-CRM —
customers.is_blockedlives on the profile. - Queues · Database · Roadmap A2.2 — Loyalty & Trust
Knowledge Tests¶
Level 1 — MCQs¶
-
checkCustomerFraud(type="redemption")compares against… a)points_earning_limit_valueb)redemption_limit_valueover the period c)wallet_balanced) LTV Answer: b. -
A velocity breach results in… a) instant hard block b) a
fraudulent_customersflag (status=1, blocked_by=System) c) order cancellation d) a refund Answer: b. Admin then decides to block. -
CSV blocklist upload rejects phone numbers that… a) contain letters only b) are 10 digits c) start with 0–5 or aren't 10 digits d) are duplicates Answer: c. Landline/invalid filter.
-
To hard-block a customer, an admin sets status… a) 1 b) 2 (
customers.is_blocked=1) c) 3 d) 0 Answer: b. status 3 unblocks. -
Fraud Prevention's roadmap destiny is… a) deletion b) merge into "Loyalty & Trust" (A2.2) c) move to Kafka d) frontend-only Answer: b.
Level 2 — Scenarios¶
-
A brand reports customers "farming" welcome/earning points via repeat fake orders. Expected: set
points_earning_limit_value/periodinwallet_rules;checkCustomerFraud("allocation")will flag offenders intofraudulent_customers; enable alerts; admin hard-blocks repeat offenders; consider CSV bulk blocklist for known numbers. -
After enabling tight limits, legit high-frequency customers get flagged. Expected: the velocity threshold/period is too strict; raise
*_limit_valueor widen_period; unblock falsely-flagged (status 3); note the read-that-writes side effect creates flags during normal txns.
Level 3 — Code Reading¶
checkCustomerFraud counts wallet_history rows by txn_type within a period and, on breach, inserts into fraudulent_customers. Question: why is calling this on the hot loyalty path both a correctness convenience and a performance/observability risk? Expected: convenient (enforcement + flagging in one call, at the exact transaction) but it's a read that writes on every earn/redeem — needs a wallet_history composite index, and side-effect writes make the function non-idempotent under retries (double flags).
Level 4 — Architecture¶
Fraud checks run synchronously inside every loyalty allocation/redemption. Roadmap A2.2 merges Fraud into "Loyalty & Trust". Propose how to keep enforcement authoritative while removing the per-transaction COUNT scan. Expected: maintain rolling counters (Redis/materialized per customer-period) updated on each txn instead of COUNT-scanning wallet_history; async flagging via event; idempotent flag writes; keep hard-block lookup O(1) with an indexed blocklist cache.
Level 5 — Production Debugging¶
Ticket: "Loyalty API latency spiked 5× for one large brand after they enabled fraud rules." Expected reasoning: checkCustomerFraud now runs a wallet_history COUNT on every earn/redeem; that brand has huge wallet_history and no (wallet_id, txn_type, event_date) index → full scans; MySQL jobsPool/webPool saturates. Fix: add the composite index (or counter cache), verify EXPLAIN, watch pool stats; the latency is the fraud scan, not the loyalty math.
Practical Assignments¶
- Index the hot path. Capture the
checkCustomerFraudCOUNT query, EXPLAIN it against a largewallet_history, add the composite index, and measure before/after latency. - Blocklist round-trip. Bulk-upload a CSV of 3 numbers, confirm
blocklist_contacts+ validation rejects a landline, then verify a loyalty earn is denied for a blocked number and allowed after unblock (status 3). - Fraud MIS dashboard. Using
fraudListCustomersMis/groupListFraudContacts, build a report of flags perfraud_typeper parent business over 30 days and identify the top farming targets; cross-link findings to Loyalty and Referral.