@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.
Files changed (66) hide show
  1. package/README.md +518 -0
  2. package/dungeons/array-of-object-lookup-schema.json +327 -0
  3. package/dungeons/array-of-object-lookup.js +220 -0
  4. package/dungeons/ecommerce-schema.json +462 -0
  5. package/dungeons/ecommerce.js +447 -0
  6. package/dungeons/education-schema.json +2409 -0
  7. package/dungeons/education.js +768 -0
  8. package/dungeons/fintech-schema.json +14034 -0
  9. package/dungeons/fintech.js +696 -0
  10. package/dungeons/foobar-schema.json +403 -0
  11. package/dungeons/foobar.js +296 -0
  12. package/dungeons/food-delivery-schema.json +192 -0
  13. package/dungeons/food-delivery.js +602 -0
  14. package/dungeons/food-schema.json +1152 -0
  15. package/dungeons/food.js +754 -0
  16. package/dungeons/gaming-schema.json +1270 -0
  17. package/dungeons/gaming.js +508 -0
  18. package/dungeons/insurance-application-schema.json +204 -0
  19. package/dungeons/insurance-application.js +605 -0
  20. package/dungeons/media-schema.json +906 -0
  21. package/dungeons/media.js +790 -0
  22. package/dungeons/retention-cadence-schema.json +78 -0
  23. package/dungeons/retention-cadence.js +244 -0
  24. package/dungeons/rpg-schema.json +4526 -0
  25. package/dungeons/rpg.js +919 -0
  26. package/dungeons/sanity-schema.json +255 -0
  27. package/dungeons/sanity.js +152 -0
  28. package/dungeons/sass-schema.json +1291 -0
  29. package/dungeons/sass.js +795 -0
  30. package/dungeons/scd-schema.json +919 -0
  31. package/dungeons/scd.js +277 -0
  32. package/dungeons/simple-schema.json +608 -0
  33. package/dungeons/simple.js +285 -0
  34. package/dungeons/simplest-schema.json +1418 -0
  35. package/dungeons/simplest.js +392 -0
  36. package/dungeons/social-schema.json +1118 -0
  37. package/dungeons/social.js +686 -0
  38. package/dungeons/text-generation-schema.json +3096 -0
  39. package/dungeons/text-generation.js +812 -0
  40. package/index.js +567 -0
  41. package/lib/core/config-validator.js +395 -0
  42. package/lib/core/context.js +204 -0
  43. package/lib/core/dungeon-loader.js +337 -0
  44. package/lib/core/storage.js +379 -0
  45. package/lib/generators/adspend.js +132 -0
  46. package/lib/generators/events.js +271 -0
  47. package/lib/generators/funnels.js +407 -0
  48. package/lib/generators/mirror.js +167 -0
  49. package/lib/generators/product-lookup.js +262 -0
  50. package/lib/generators/product-names.js +195 -0
  51. package/lib/generators/profiles.js +93 -0
  52. package/lib/generators/scd.js +124 -0
  53. package/lib/generators/text.js +1192 -0
  54. package/lib/orchestrators/mixpanel-sender.js +266 -0
  55. package/lib/orchestrators/user-loop.js +335 -0
  56. package/lib/templates/abbreviated.d.ts +169 -0
  57. package/lib/templates/defaults.js +1405 -0
  58. package/lib/templates/phrases.js +2526 -0
  59. package/lib/templates/schema.d.ts +173 -0
  60. package/lib/templates/soup-presets.js +188 -0
  61. package/lib/utils/function-registry.js +302 -0
  62. package/lib/utils/json-evaluator.js +172 -0
  63. package/lib/utils/logger.js +34 -0
  64. package/lib/utils/utils.js +1490 -0
  65. package/package.json +89 -0
  66. 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;