@ak--47/dungeon-master 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +518 -0
- package/dungeons/array-of-object-lookup-schema.json +327 -0
- package/dungeons/array-of-object-lookup.js +220 -0
- package/dungeons/ecommerce-schema.json +462 -0
- package/dungeons/ecommerce.js +447 -0
- package/dungeons/education-schema.json +2409 -0
- package/dungeons/education.js +768 -0
- package/dungeons/fintech-schema.json +14034 -0
- package/dungeons/fintech.js +696 -0
- package/dungeons/foobar-schema.json +403 -0
- package/dungeons/foobar.js +296 -0
- package/dungeons/food-delivery-schema.json +192 -0
- package/dungeons/food-delivery.js +602 -0
- package/dungeons/food-schema.json +1152 -0
- package/dungeons/food.js +754 -0
- package/dungeons/gaming-schema.json +1270 -0
- package/dungeons/gaming.js +508 -0
- package/dungeons/insurance-application-schema.json +204 -0
- package/dungeons/insurance-application.js +605 -0
- package/dungeons/media-schema.json +906 -0
- package/dungeons/media.js +790 -0
- package/dungeons/retention-cadence-schema.json +78 -0
- package/dungeons/retention-cadence.js +244 -0
- package/dungeons/rpg-schema.json +4526 -0
- package/dungeons/rpg.js +919 -0
- package/dungeons/sanity-schema.json +255 -0
- package/dungeons/sanity.js +152 -0
- package/dungeons/sass-schema.json +1291 -0
- package/dungeons/sass.js +795 -0
- package/dungeons/scd-schema.json +919 -0
- package/dungeons/scd.js +277 -0
- package/dungeons/simple-schema.json +608 -0
- package/dungeons/simple.js +285 -0
- package/dungeons/simplest-schema.json +1418 -0
- package/dungeons/simplest.js +392 -0
- package/dungeons/social-schema.json +1118 -0
- package/dungeons/social.js +686 -0
- package/dungeons/text-generation-schema.json +3096 -0
- package/dungeons/text-generation.js +812 -0
- package/index.js +567 -0
- package/lib/core/config-validator.js +395 -0
- package/lib/core/context.js +204 -0
- package/lib/core/dungeon-loader.js +337 -0
- package/lib/core/storage.js +379 -0
- package/lib/generators/adspend.js +132 -0
- package/lib/generators/events.js +271 -0
- package/lib/generators/funnels.js +407 -0
- package/lib/generators/mirror.js +167 -0
- package/lib/generators/product-lookup.js +262 -0
- package/lib/generators/product-names.js +195 -0
- package/lib/generators/profiles.js +93 -0
- package/lib/generators/scd.js +124 -0
- package/lib/generators/text.js +1192 -0
- package/lib/orchestrators/mixpanel-sender.js +266 -0
- package/lib/orchestrators/user-loop.js +335 -0
- package/lib/templates/abbreviated.d.ts +169 -0
- package/lib/templates/defaults.js +1405 -0
- package/lib/templates/phrases.js +2526 -0
- package/lib/templates/schema.d.ts +173 -0
- package/lib/templates/soup-presets.js +188 -0
- package/lib/utils/function-registry.js +302 -0
- package/lib/utils/json-evaluator.js +172 -0
- package/lib/utils/logger.js +34 -0
- package/lib/utils/utils.js +1490 -0
- package/package.json +89 -0
- package/types.d.ts +865 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import utc from "dayjs/plugin/utc.js";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
import * as u from "../lib/utils/utils.js";
|
|
5
|
+
|
|
6
|
+
const SEED = "harness-fintech";
|
|
7
|
+
dayjs.extend(utc);
|
|
8
|
+
const chance = u.initChance(SEED);
|
|
9
|
+
const num_users = 5_000;
|
|
10
|
+
const days = 100;
|
|
11
|
+
|
|
12
|
+
/** @typedef {import("../types.d.ts").Dungeon} Config */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ===================================================================
|
|
16
|
+
* DATASET OVERVIEW
|
|
17
|
+
* ===================================================================
|
|
18
|
+
*
|
|
19
|
+
* NexBank — a Chime/Revolut-style neobank app. Users open accounts
|
|
20
|
+
* (personal or business), transact across 7 merchant categories,
|
|
21
|
+
* send transfers, pay bills, set budgets, invest, apply for loans,
|
|
22
|
+
* and earn tier-scaled rewards.
|
|
23
|
+
*
|
|
24
|
+
* Scale: 5,000 users · 600K events · 100 days · 18 event types
|
|
25
|
+
* Groups: 500 households
|
|
26
|
+
* Tiers: Basic (free) / Plus ($4.99/mo) / Premium ($14.99/mo)
|
|
27
|
+
*
|
|
28
|
+
* Core loop: onboarding → daily banking → financial planning →
|
|
29
|
+
* budgets & savings → investments → rewards & monetization
|
|
30
|
+
*
|
|
31
|
+
* Funnels:
|
|
32
|
+
* - Onboarding: account opened → app session → balance checked
|
|
33
|
+
* - Daily banking: app session → balance checked → transaction
|
|
34
|
+
* - Transfers: app session → transfer sent → notification opened
|
|
35
|
+
* - Bill payment: app session → bill paid → notification opened
|
|
36
|
+
* - Financial planning: budget created → budget alert → savings goal
|
|
37
|
+
* - Investment: balance checked → investment made → reward redeemed
|
|
38
|
+
* - Support: support contacted → card locked → dispute filed
|
|
39
|
+
* - Lending: loan applied → loan approved → premium upgraded
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* ===================================================================
|
|
44
|
+
* ANALYTICS HOOKS (8 architected patterns)
|
|
45
|
+
* ===================================================================
|
|
46
|
+
*
|
|
47
|
+
* 1. PERSONAL VS BUSINESS ACCOUNTS (user hook)
|
|
48
|
+
* 20% business (employee_count, revenue, industry), 80% personal
|
|
49
|
+
* (age_range, life_stage). Breakdown: account_segment.
|
|
50
|
+
* → Insights: any event by unique users, breakdown account_segment
|
|
51
|
+
* → Insights: "transaction completed" avg amount, breakdown account_segment
|
|
52
|
+
*
|
|
53
|
+
* 2. PAYDAY PATTERNS (event hook)
|
|
54
|
+
* Direct deposits 3x on 1st/15th (payday: true). Transfers 2x on
|
|
55
|
+
* days 1-3 and 15-17 (post_payday_spending: true).
|
|
56
|
+
* → Insights: "transaction completed" avg amount, filter direct_deposit, breakdown payday
|
|
57
|
+
* → Insights: "transfer sent" avg amount, breakdown post_payday_spending
|
|
58
|
+
*
|
|
59
|
+
* 3. FRAUD DETECTION (everything hook)
|
|
60
|
+
* 3% of users get a fraud burst at timeline midpoint: 3-5 rapid
|
|
61
|
+
* high-value txns → card locked → dispute → support. Tagged
|
|
62
|
+
* fraud_sequence: true.
|
|
63
|
+
* → Insights: "transaction completed" total, filter fraud_sequence = true
|
|
64
|
+
* → Funnels: card locked → dispute filed → support contacted, filter fraud_sequence = true
|
|
65
|
+
*
|
|
66
|
+
* 4. LOW BALANCE CHURN (everything hook)
|
|
67
|
+
* Users with 3+ balance checks < $15K lose 50% of events after
|
|
68
|
+
* day 30. Surviving events tagged low_balance_churn: true.
|
|
69
|
+
* → Retention: any → any, segment low_balance_churn = true vs others
|
|
70
|
+
* → Insights: any event total over time, filter low_balance_churn = true
|
|
71
|
+
*
|
|
72
|
+
* 5. BUDGET USERS SAVE MORE (everything hook)
|
|
73
|
+
* Budget creators get 2x savings contributions, 1.5x investment
|
|
74
|
+
* amounts, extra savings goals. Tagged budget_discipline: true.
|
|
75
|
+
* → Insights: "savings goal set" avg monthly_contribution, breakdown budget_discipline
|
|
76
|
+
* → Insights: "investment made" avg amount, breakdown budget_discipline
|
|
77
|
+
*
|
|
78
|
+
* 6. AUTO-PAY LOYALTY (event hook)
|
|
79
|
+
* Manual payers (auto_pay=false) miss 30% of bills (event renamed
|
|
80
|
+
* to "bill payment missed"). Auto-pay users never miss.
|
|
81
|
+
* → Insights: "bill paid" + "bill payment missed" totals side by side
|
|
82
|
+
* → Insights: "bill paid" total, breakdown manual_payment
|
|
83
|
+
*
|
|
84
|
+
* 7. PREMIUM TIER VALUE (event hook)
|
|
85
|
+
* Premium: 3x rewards, 2x investment sell returns. Plus: 1.5x
|
|
86
|
+
* rewards. Tagged premium_reward / premium_returns.
|
|
87
|
+
* → Insights: "reward redeemed" avg value, breakdown account_tier
|
|
88
|
+
* (expected: premium ~$30, plus ~$15, basic ~$10)
|
|
89
|
+
* → Insights: "investment made" avg amount, filter action=sell, breakdown premium_returns
|
|
90
|
+
*
|
|
91
|
+
* 8. MONTH-END ANXIETY (event hook)
|
|
92
|
+
* Days >= 28: sessions 40% longer, balances 30% lower. Tagged
|
|
93
|
+
* month_end_anxiety / month_end_check.
|
|
94
|
+
* → Insights: "app session" avg session_duration_sec, breakdown month_end_anxiety
|
|
95
|
+
* → Insights: "balance checked" avg account_balance, breakdown month_end_check
|
|
96
|
+
*
|
|
97
|
+
* ===================================================================
|
|
98
|
+
* ADVANCED ANALYSIS IDEAS
|
|
99
|
+
* ===================================================================
|
|
100
|
+
*
|
|
101
|
+
* Cross-hook patterns:
|
|
102
|
+
* - Budget + Low Balance: Do budget creators avoid low-balance churn?
|
|
103
|
+
* - Premium + Auto-Pay: Do premium users adopt auto-pay more?
|
|
104
|
+
* - Fraud + Churn: Do fraud victims churn more? Does resolution help?
|
|
105
|
+
* - Payday + Month-End: Do payday spenders run out by month-end?
|
|
106
|
+
* - Business vs Personal Fraud: Are business accounts more targeted?
|
|
107
|
+
*
|
|
108
|
+
* Cohort analysis:
|
|
109
|
+
* - By account_tier: upgrade paths, value realization
|
|
110
|
+
* - By signup_channel: referral retention vs organic
|
|
111
|
+
* - By income_bracket: feature adoption by income
|
|
112
|
+
* - By credit_score_range: loan approvals, tier adoption
|
|
113
|
+
*
|
|
114
|
+
* ===================================================================
|
|
115
|
+
* EXPECTED METRICS SUMMARY
|
|
116
|
+
* ===================================================================
|
|
117
|
+
*
|
|
118
|
+
* Hook | Metric | Baseline | Effect | Ratio
|
|
119
|
+
* ----------------------|-----------------------|----------|--------|------
|
|
120
|
+
* Personal vs Business | Avg transaction amt | $50 | $200+ | ~4x
|
|
121
|
+
* Payday Patterns | Deposit amount | $50 | $100 | 2x
|
|
122
|
+
* Fraud Detection | Users affected | 0% | 3% | --
|
|
123
|
+
* Low Balance Churn | D30+ event count | 100% | 50% | 0.5x
|
|
124
|
+
* Budget Discipline | Monthly contribution | $200 | $400 | 2x
|
|
125
|
+
* Auto-Pay Loyalty | Bill completion rate | 100% | 70% | 0.7x
|
|
126
|
+
* Premium Tier Value | Reward value | $10 | $30 | 3x
|
|
127
|
+
* Month-End Anxiety | Session duration | 60s | 84s | 1.4x
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/** @type {Config} */
|
|
131
|
+
const config = {
|
|
132
|
+
token: "",
|
|
133
|
+
seed: SEED,
|
|
134
|
+
numDays: days,
|
|
135
|
+
numEvents: num_users * 120,
|
|
136
|
+
numUsers: num_users,
|
|
137
|
+
hasAnonIds: false,
|
|
138
|
+
hasSessionIds: true,
|
|
139
|
+
format: "json",
|
|
140
|
+
gzip: true,
|
|
141
|
+
alsoInferFunnels: false,
|
|
142
|
+
hasLocation: true,
|
|
143
|
+
hasAndroidDevices: true,
|
|
144
|
+
hasIOSDevices: true,
|
|
145
|
+
hasDesktopDevices: true,
|
|
146
|
+
hasBrowser: false,
|
|
147
|
+
hasCampaigns: false,
|
|
148
|
+
isAnonymous: false,
|
|
149
|
+
hasAdSpend: false,
|
|
150
|
+
percentUsersBornInDataset: 50,
|
|
151
|
+
|
|
152
|
+
hasAvatar: true,
|
|
153
|
+
|
|
154
|
+
batchSize: 2_500_000,
|
|
155
|
+
concurrency: 1,
|
|
156
|
+
writeToDisk: false,
|
|
157
|
+
|
|
158
|
+
scdProps: {},
|
|
159
|
+
|
|
160
|
+
funnels: [
|
|
161
|
+
{
|
|
162
|
+
sequence: ["account opened", "app session", "balance checked"],
|
|
163
|
+
isFirstFunnel: true,
|
|
164
|
+
conversionRate: 85,
|
|
165
|
+
timeToConvert: 0.25,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
// Daily banking: check balance, view transactions - most common activity
|
|
169
|
+
sequence: ["app session", "balance checked", "transaction completed"],
|
|
170
|
+
conversionRate: 80,
|
|
171
|
+
timeToConvert: 0.5,
|
|
172
|
+
weight: 5,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
// Transfers and notifications
|
|
176
|
+
sequence: ["app session", "transfer sent", "notification opened"],
|
|
177
|
+
conversionRate: 50,
|
|
178
|
+
timeToConvert: 1,
|
|
179
|
+
weight: 3,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
// Bill payment flow
|
|
183
|
+
sequence: ["app session", "bill paid", "notification opened"],
|
|
184
|
+
conversionRate: 60,
|
|
185
|
+
timeToConvert: 1,
|
|
186
|
+
weight: 3,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
// Financial planning: budgets and savings
|
|
190
|
+
sequence: ["budget created", "budget alert", "savings goal set"],
|
|
191
|
+
conversionRate: 40,
|
|
192
|
+
timeToConvert: 12,
|
|
193
|
+
weight: 2,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
// Investment and rewards
|
|
197
|
+
sequence: ["balance checked", "investment made", "reward redeemed"],
|
|
198
|
+
conversionRate: 30,
|
|
199
|
+
timeToConvert: 5,
|
|
200
|
+
weight: 2,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
// Support and account management
|
|
204
|
+
sequence: ["support contacted", "card locked", "dispute filed"],
|
|
205
|
+
conversionRate: 35,
|
|
206
|
+
timeToConvert: 2,
|
|
207
|
+
weight: 1,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
// Lending flow
|
|
211
|
+
sequence: ["loan applied", "loan approved", "premium upgraded"],
|
|
212
|
+
conversionRate: 25,
|
|
213
|
+
timeToConvert: 10,
|
|
214
|
+
weight: 1,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
|
|
218
|
+
events: [
|
|
219
|
+
{
|
|
220
|
+
event: "account opened",
|
|
221
|
+
weight: 1,
|
|
222
|
+
isFirstEvent: true,
|
|
223
|
+
properties: {
|
|
224
|
+
"account_type": u.pickAWinner(["personal", "business", "personal"]),
|
|
225
|
+
"signup_channel": ["app", "web", "referral", "branch"],
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
event: "app session",
|
|
230
|
+
weight: 20,
|
|
231
|
+
properties: {
|
|
232
|
+
"session_duration_sec": u.weighNumRange(10, 600, 0.3, 60),
|
|
233
|
+
"pages_viewed": u.weighNumRange(1, 15, 0.5, 3),
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
event: "balance checked",
|
|
238
|
+
weight: 15,
|
|
239
|
+
properties: {
|
|
240
|
+
"account_balance": u.weighNumRange(0, 50000, 0.8, 2500),
|
|
241
|
+
"account_type": ["checking", "savings", "investment"],
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
event: "transaction completed",
|
|
246
|
+
weight: 18,
|
|
247
|
+
properties: {
|
|
248
|
+
"transaction_type": ["purchase", "atm", "direct_deposit", "refund"],
|
|
249
|
+
"amount": u.weighNumRange(1, 5000, 0.3, 50),
|
|
250
|
+
"merchant_category": ["grocery", "restaurant", "gas", "retail", "online", "subscription", "utilities"],
|
|
251
|
+
"payment_method": ["debit", "credit", "contactless", "online"],
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
event: "transfer sent",
|
|
256
|
+
weight: 8,
|
|
257
|
+
properties: {
|
|
258
|
+
"transfer_type": ["internal", "external", "p2p", "wire"],
|
|
259
|
+
"amount": u.weighNumRange(10, 10000, 0.3, 200),
|
|
260
|
+
"recipient_type": ["friend", "family", "business", "self"],
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
event: "bill paid",
|
|
265
|
+
weight: 6,
|
|
266
|
+
properties: {
|
|
267
|
+
"bill_type": ["rent", "utilities", "phone", "insurance", "subscription", "loan_payment"],
|
|
268
|
+
"amount": u.weighNumRange(20, 3000, 0.5, 150),
|
|
269
|
+
"auto_pay": u.pickAWinner([true, false], 0.4),
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
event: "budget created",
|
|
274
|
+
weight: 3,
|
|
275
|
+
properties: {
|
|
276
|
+
"category": ["food", "transport", "entertainment", "shopping", "bills", "savings"],
|
|
277
|
+
"monthly_limit": u.weighNumRange(50, 2000, 0.5, 300),
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
event: "budget alert",
|
|
282
|
+
weight: 4,
|
|
283
|
+
properties: {
|
|
284
|
+
"alert_type": ["approaching_limit", "exceeded", "on_track"],
|
|
285
|
+
"percent_used": u.weighNumRange(50, 150, 1, 90),
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
event: "savings goal set",
|
|
290
|
+
weight: 3,
|
|
291
|
+
properties: {
|
|
292
|
+
"goal_type": ["emergency", "vacation", "car", "home", "education", "retirement"],
|
|
293
|
+
"target_amount": u.weighNumRange(500, 50000, 0.3, 5000),
|
|
294
|
+
"monthly_contribution": u.weighNumRange(25, 2000, 0.5, 200),
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
event: "investment made",
|
|
299
|
+
weight: 4,
|
|
300
|
+
properties: {
|
|
301
|
+
"investment_type": ["stocks", "etf", "crypto", "bonds", "mutual_fund"],
|
|
302
|
+
"amount": u.weighNumRange(10, 10000, 0.3, 250),
|
|
303
|
+
"action": u.pickAWinner(["buy", "sell", "buy"]),
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
event: "card locked",
|
|
308
|
+
weight: 1,
|
|
309
|
+
properties: {
|
|
310
|
+
"reason": ["lost", "stolen", "suspicious_activity", "travel"],
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
event: "dispute filed",
|
|
315
|
+
weight: 1,
|
|
316
|
+
properties: {
|
|
317
|
+
"dispute_amount": u.weighNumRange(10, 2000, 0.5, 100),
|
|
318
|
+
"reason": ["unauthorized", "duplicate", "not_received", "damaged", "wrong_amount"],
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
event: "loan applied",
|
|
323
|
+
weight: 2,
|
|
324
|
+
properties: {
|
|
325
|
+
"loan_type": ["personal", "auto", "home", "student", "business"],
|
|
326
|
+
"requested_amount": u.weighNumRange(1000, 100000, 0.3, 10000),
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
event: "loan approved",
|
|
331
|
+
weight: 1,
|
|
332
|
+
properties: {
|
|
333
|
+
"loan_type": ["personal", "auto", "home", "student", "business"],
|
|
334
|
+
"approved_amount": u.weighNumRange(1000, 100000, 0.3, 10000),
|
|
335
|
+
"interest_rate": u.weighNumRange(3, 25, 1, 8),
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
event: "premium upgraded",
|
|
340
|
+
weight: 2,
|
|
341
|
+
properties: {
|
|
342
|
+
"old_tier": ["basic", "plus", "premium"],
|
|
343
|
+
"new_tier": u.pickAWinner(["plus", "premium", "premium"]),
|
|
344
|
+
"monthly_fee": u.pickAWinner([4.99, 9.99, 14.99]),
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
event: "support contacted",
|
|
349
|
+
weight: 3,
|
|
350
|
+
properties: {
|
|
351
|
+
"channel": ["chat", "phone", "email", "in_app"],
|
|
352
|
+
"issue_type": ["transaction", "account", "card", "transfer", "technical"],
|
|
353
|
+
"resolved": u.pickAWinner([true, false], 0.8),
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
event: "notification opened",
|
|
358
|
+
weight: 10,
|
|
359
|
+
properties: {
|
|
360
|
+
"notification_type": ["transaction", "low_balance", "bill_due", "reward", "security", "promo"],
|
|
361
|
+
"action_taken": u.pickAWinner([true, false], 0.6),
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
event: "reward redeemed",
|
|
366
|
+
weight: 4,
|
|
367
|
+
properties: {
|
|
368
|
+
"reward_type": ["cashback", "points", "discount", "partner_offer"],
|
|
369
|
+
"value": u.weighNumRange(1, 100, 0.5, 10),
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
],
|
|
373
|
+
|
|
374
|
+
superProps: {
|
|
375
|
+
account_tier: u.pickAWinner(["basic", "basic", "basic", "plus", "plus", "premium"]),
|
|
376
|
+
platform: ["ios", "android", "web"],
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
userProps: {
|
|
380
|
+
"credit_score_range": ["300-579", "580-669", "670-739", "740-799", "800-850"],
|
|
381
|
+
"income_bracket": ["under_30k", "30k_50k", "50k_75k", "75k_100k", "100k_150k", "over_150k"],
|
|
382
|
+
"account_age_months": u.weighNumRange(1, 60, 0.5, 12),
|
|
383
|
+
"total_balance": u.weighNumRange(0, 100000, 0.3, 5000),
|
|
384
|
+
"has_direct_deposit": u.pickAWinner([true, false], 0.6),
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
groupKeys: [
|
|
388
|
+
["household_id", 500, ["transaction completed", "transfer sent", "bill paid", "savings goal set"]],
|
|
389
|
+
],
|
|
390
|
+
|
|
391
|
+
groupProps: {
|
|
392
|
+
household_id: {
|
|
393
|
+
"household_size": u.weighNumRange(1, 6),
|
|
394
|
+
"combined_income": u.weighNumRange(20000, 300000, 0.3, 75000),
|
|
395
|
+
"financial_health_score": u.weighNumRange(1, 100, 1, 65),
|
|
396
|
+
"primary_bank": u.pickAWinner(["NexBank_only", "multi_bank", "NexBank_only"]),
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
lookupTables: [],
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* ARCHITECTED ANALYTICS HOOKS
|
|
404
|
+
*
|
|
405
|
+
* This hook function creates 8 deliberate patterns in the data:
|
|
406
|
+
*
|
|
407
|
+
* 1. PERSONAL VS BUSINESS: Business accounts get employee_count, revenue; personal get age_range, life_stage
|
|
408
|
+
* 2. PAYDAY PATTERNS: Transactions spike on 1st/15th with bigger deposits and post-payday spending
|
|
409
|
+
* 3. FRAUD DETECTION: 3% of users experience a fraud burst (rapid high-value txns -> card lock -> dispute -> support)
|
|
410
|
+
* 4. LOW BALANCE CHURN: Users with chronic low balances (<$15K) lose 50% of activity after day 30
|
|
411
|
+
* 5. BUDGET DISCIPLINE: Budget creators save 2x more and invest 1.5x more
|
|
412
|
+
* 6. AUTO-PAY LOYALTY: Auto-pay users never miss bills; manual payers miss 30%
|
|
413
|
+
* 7. PREMIUM TIER VALUE: Premium users get 3x rewards; Plus users get 1.5x; Premium investors get 2x returns
|
|
414
|
+
* 8. MONTH-END ANXIETY: Last 3 days of month see 40% longer sessions and 30% lower balances
|
|
415
|
+
*/
|
|
416
|
+
hook: function (record, type, meta) {
|
|
417
|
+
const NOW = dayjs();
|
|
418
|
+
const DATASET_START = NOW.subtract(days, "days");
|
|
419
|
+
|
|
420
|
+
// ===============================================================
|
|
421
|
+
// Hook #1: PERSONAL VS BUSINESS ACCOUNTS (user)
|
|
422
|
+
// 20% of users are randomly tagged as business accounts with
|
|
423
|
+
// employee_count, annual_revenue, and industry. The other 80% get
|
|
424
|
+
// personal attributes: age_range and life_stage.
|
|
425
|
+
// ===============================================================
|
|
426
|
+
if (type === "user") {
|
|
427
|
+
const isBusiness = chance.bool({ likelihood: 20 });
|
|
428
|
+
if (isBusiness) {
|
|
429
|
+
record.account_segment = "business";
|
|
430
|
+
record.employee_count = chance.integer({ min: 5, max: 500 });
|
|
431
|
+
record.annual_revenue = chance.integer({ min: 100000, max: 10000000 });
|
|
432
|
+
record.industry = chance.pickone(["tech", "retail", "food", "services", "healthcare"]);
|
|
433
|
+
} else {
|
|
434
|
+
record.account_segment = "personal";
|
|
435
|
+
record.age_range = `${chance.pickone([18, 25, 35, 45, 55])}-${chance.pickone([24, 34, 44, 54, 65])}`;
|
|
436
|
+
record.life_stage = chance.pickone(["student", "early_career", "established", "pre_retirement", "retired"]);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ===============================================================
|
|
441
|
+
// Hook #2: PAYDAY PATTERNS (event)
|
|
442
|
+
// On the 1st and 15th, direct deposit transactions are 2x bigger
|
|
443
|
+
// and tagged with payday: true. On days 1-3 and 15-17, transfers
|
|
444
|
+
// have a 40% chance of 1.5x boost (post-payday spending).
|
|
445
|
+
// ===============================================================
|
|
446
|
+
if (type === "event") {
|
|
447
|
+
const EVENT_TIME = dayjs(record.time);
|
|
448
|
+
const dayOfMonth = EVENT_TIME.date();
|
|
449
|
+
|
|
450
|
+
// Payday: 1st and 15th
|
|
451
|
+
if (record.event === "transaction completed" && record.transaction_type === "direct_deposit") {
|
|
452
|
+
if (dayOfMonth === 1 || dayOfMonth === 15) {
|
|
453
|
+
record.amount = Math.floor((record.amount || 50) * 3);
|
|
454
|
+
record.payday = true;
|
|
455
|
+
} else {
|
|
456
|
+
record.payday = false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Post-payday spending: days 1-3 and 15-17
|
|
461
|
+
if (record.event === "transfer sent") {
|
|
462
|
+
const isPaydayWindow = (dayOfMonth >= 1 && dayOfMonth <= 3) || (dayOfMonth >= 15 && dayOfMonth <= 17);
|
|
463
|
+
if (isPaydayWindow && chance.bool({ likelihood: 60 })) {
|
|
464
|
+
record.amount = Math.floor((record.amount || 200) * 2.0);
|
|
465
|
+
record.post_payday_spending = true;
|
|
466
|
+
} else {
|
|
467
|
+
record.post_payday_spending = false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ===============================================================
|
|
472
|
+
// Hook #6: AUTO-PAY LOYALTY (event)
|
|
473
|
+
// Users with auto_pay=false on bill paid events have a 30% chance
|
|
474
|
+
// of the event being dropped entirely (missed payment). Surviving
|
|
475
|
+
// manual payments get tagged. Auto-pay users always succeed.
|
|
476
|
+
// ===============================================================
|
|
477
|
+
if (record.event === "bill paid") {
|
|
478
|
+
if (record.auto_pay === false || record.auto_pay === undefined) {
|
|
479
|
+
if (chance.bool({ likelihood: 30 })) {
|
|
480
|
+
record.event = "bill payment missed";
|
|
481
|
+
record.missed_payment = true;
|
|
482
|
+
record.manual_payment = false;
|
|
483
|
+
} else {
|
|
484
|
+
record.missed_payment = false;
|
|
485
|
+
record.manual_payment = true;
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
record.missed_payment = false;
|
|
489
|
+
record.manual_payment = false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ===============================================================
|
|
494
|
+
// Hook #7: PREMIUM TIER VALUE (event)
|
|
495
|
+
// Premium tier users get 3x reward values and 2x investment sell
|
|
496
|
+
// returns. Plus tier users get 1.5x rewards.
|
|
497
|
+
// ===============================================================
|
|
498
|
+
if (record.event === "reward redeemed") {
|
|
499
|
+
if (record.account_tier === "premium") {
|
|
500
|
+
record.value = Math.floor((record.value || 10) * 3);
|
|
501
|
+
record.premium_reward = true;
|
|
502
|
+
} else if (record.account_tier === "plus") {
|
|
503
|
+
record.value = Math.floor((record.value || 10) * 1.5);
|
|
504
|
+
record.premium_reward = false;
|
|
505
|
+
} else {
|
|
506
|
+
record.premium_reward = false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (record.event === "investment made" && record.action === "sell") {
|
|
511
|
+
if (record.account_tier === "premium") {
|
|
512
|
+
record.amount = Math.floor((record.amount || 250) * 2);
|
|
513
|
+
record.premium_returns = true;
|
|
514
|
+
} else {
|
|
515
|
+
record.premium_returns = false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ===============================================================
|
|
520
|
+
// Hook #8: MONTH-END ANXIETY (event)
|
|
521
|
+
// Last 3 days of month (day >= 28) see 40% longer app sessions
|
|
522
|
+
// and 30% lower balances when checked.
|
|
523
|
+
// ===============================================================
|
|
524
|
+
if (record.event === "app session") {
|
|
525
|
+
if (dayOfMonth >= 28) {
|
|
526
|
+
record.session_duration_sec = Math.floor((record.session_duration_sec || 60) * 1.4);
|
|
527
|
+
record.month_end_anxiety = true;
|
|
528
|
+
} else {
|
|
529
|
+
record.month_end_anxiety = false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (record.event === "balance checked") {
|
|
534
|
+
if (dayOfMonth >= 28) {
|
|
535
|
+
record.account_balance = Math.floor((record.account_balance || 2500) * 0.7);
|
|
536
|
+
record.month_end_check = true;
|
|
537
|
+
} else {
|
|
538
|
+
record.month_end_check = false;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ===============================================================
|
|
544
|
+
// Hook #3, #4, #5: EVERYTHING - Complex behavioral patterns
|
|
545
|
+
// These hooks operate on the full event stream per user for
|
|
546
|
+
// fraud injection, low-balance churn, and budget discipline.
|
|
547
|
+
// ===============================================================
|
|
548
|
+
if (type === "everything") {
|
|
549
|
+
const userEvents = record;
|
|
550
|
+
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
551
|
+
|
|
552
|
+
// -----------------------------------------------------------
|
|
553
|
+
// Hook #3: FRAUD DETECTION PATTERN
|
|
554
|
+
// 3% of users experience a fraud event sequence: a burst of
|
|
555
|
+
// 3-5 rapid high-value transactions within 1 hour, followed by
|
|
556
|
+
// card locked, dispute filed, and support contacted events.
|
|
557
|
+
// All injected events are tagged with fraud_sequence: true.
|
|
558
|
+
// -----------------------------------------------------------
|
|
559
|
+
if (chance.bool({ likelihood: 3 })) {
|
|
560
|
+
// Find the midpoint of the user's timeline
|
|
561
|
+
if (userEvents.length >= 2) {
|
|
562
|
+
const midIdx = Math.floor(userEvents.length / 2);
|
|
563
|
+
const midEvent = userEvents[midIdx];
|
|
564
|
+
const midTime = dayjs(midEvent.time);
|
|
565
|
+
const distinctId = midEvent.user_id;
|
|
566
|
+
|
|
567
|
+
// Inject 3-5 rapid high-value transactions
|
|
568
|
+
const burstCount = chance.integer({ min: 3, max: 5 });
|
|
569
|
+
const fraudEvents = [];
|
|
570
|
+
|
|
571
|
+
for (let i = 0; i < burstCount; i++) {
|
|
572
|
+
fraudEvents.push({
|
|
573
|
+
event: "transaction completed",
|
|
574
|
+
time: midTime.add(i * 10, "minutes").toISOString(),
|
|
575
|
+
user_id: distinctId,
|
|
576
|
+
transaction_type: "purchase",
|
|
577
|
+
amount: chance.integer({ min: 500, max: 3000 }),
|
|
578
|
+
merchant_category: chance.pickone(["online", "retail"]),
|
|
579
|
+
payment_method: "credit",
|
|
580
|
+
fraud_sequence: true,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Card locked after the burst
|
|
585
|
+
fraudEvents.push({
|
|
586
|
+
event: "card locked",
|
|
587
|
+
time: midTime.add(burstCount * 10 + 5, "minutes").toISOString(),
|
|
588
|
+
user_id: distinctId,
|
|
589
|
+
reason: "suspicious_activity",
|
|
590
|
+
fraud_sequence: true,
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// Dispute filed shortly after
|
|
594
|
+
fraudEvents.push({
|
|
595
|
+
event: "dispute filed",
|
|
596
|
+
time: midTime.add(burstCount * 10 + 30, "minutes").toISOString(),
|
|
597
|
+
user_id: distinctId,
|
|
598
|
+
dispute_amount: chance.integer({ min: 500, max: 3000 }),
|
|
599
|
+
reason: "unauthorized",
|
|
600
|
+
fraud_sequence: true,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Support contacted
|
|
604
|
+
fraudEvents.push({
|
|
605
|
+
event: "support contacted",
|
|
606
|
+
time: midTime.add(burstCount * 10 + 45, "minutes").toISOString(),
|
|
607
|
+
user_id: distinctId,
|
|
608
|
+
channel: "phone",
|
|
609
|
+
issue_type: "card",
|
|
610
|
+
resolved: true,
|
|
611
|
+
fraud_sequence: true,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// Splice all fraud events into the user's timeline
|
|
615
|
+
userEvents.splice(midIdx + 1, 0, ...fraudEvents);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// -----------------------------------------------------------
|
|
620
|
+
// Hook #4: LOW BALANCE CHURN
|
|
621
|
+
// Users who have 3+ balance checks showing < $15,000 are
|
|
622
|
+
// "struggling". After day 30, 50% of their events are removed
|
|
623
|
+
// and remaining events are tagged low_balance_churn: true.
|
|
624
|
+
// -----------------------------------------------------------
|
|
625
|
+
let lowBalanceChecks = 0;
|
|
626
|
+
userEvents.forEach((event) => {
|
|
627
|
+
if (event.event === "balance checked" && (event.account_balance || 0) < 15000) {
|
|
628
|
+
lowBalanceChecks++;
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
if (lowBalanceChecks >= 3) {
|
|
633
|
+
const day30 = DATASET_START.add(30, "days");
|
|
634
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
635
|
+
const evt = userEvents[i];
|
|
636
|
+
if (dayjs(evt.time).isAfter(day30)) {
|
|
637
|
+
if (chance.bool({ likelihood: 50 })) {
|
|
638
|
+
userEvents.splice(i, 1);
|
|
639
|
+
} else {
|
|
640
|
+
evt.low_balance_churn = true;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// -----------------------------------------------------------
|
|
647
|
+
// Hook #5: BUDGET USERS SAVE MORE
|
|
648
|
+
// Users who created any budget have 2x savings contributions,
|
|
649
|
+
// 1.5x investment amounts, and extra savings goal events
|
|
650
|
+
// spliced into their timeline. Tagged budget_discipline: true.
|
|
651
|
+
// -----------------------------------------------------------
|
|
652
|
+
let hasBudget = false;
|
|
653
|
+
userEvents.forEach((event) => {
|
|
654
|
+
if (event.event === "budget created") {
|
|
655
|
+
hasBudget = true;
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
if (hasBudget) {
|
|
660
|
+
userEvents.forEach((event, idx) => {
|
|
661
|
+
const eventTime = dayjs(event.time);
|
|
662
|
+
|
|
663
|
+
// Double savings contributions
|
|
664
|
+
if (event.event === "savings goal set") {
|
|
665
|
+
event.monthly_contribution = Math.floor((event.monthly_contribution || 200) * 2);
|
|
666
|
+
event.budget_discipline = true;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 1.5x investment amounts
|
|
670
|
+
if (event.event === "investment made") {
|
|
671
|
+
event.amount = Math.floor((event.amount || 250) * 1.5);
|
|
672
|
+
event.budget_discipline = true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Splice extra savings goal events (budget-conscious users set more goals)
|
|
676
|
+
if (event.event === "budget created" && chance.bool({ likelihood: 50 })) {
|
|
677
|
+
const extraGoal = {
|
|
678
|
+
event: "savings goal set",
|
|
679
|
+
time: eventTime.add(chance.integer({ min: 1, max: 7 }), "days").toISOString(),
|
|
680
|
+
user_id: event.user_id,
|
|
681
|
+
goal_type: chance.pickone(["emergency", "vacation", "car", "home"]),
|
|
682
|
+
target_amount: chance.integer({ min: 1000, max: 20000 }),
|
|
683
|
+
monthly_contribution: chance.integer({ min: 100, max: 800 }),
|
|
684
|
+
budget_discipline: true,
|
|
685
|
+
};
|
|
686
|
+
userEvents.splice(idx + 1, 0, extraGoal);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return record;
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
export default config;
|