@ak--47/dungeon-master 1.3.0 → 1.4.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 (98) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/array-of-object-lookup.js +0 -2
  3. package/dungeons/technical/hook-helpers-verify.js +89 -0
  4. package/dungeons/technical/identity-model-verify.js +47 -0
  5. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  6. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  7. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  8. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  9. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  10. package/dungeons/technical/simple.js +3 -4
  11. package/dungeons/technical/simplest-schema.json +5 -0
  12. package/dungeons/technical/text-generation.js +1 -1
  13. package/dungeons/vertical/ai-platform.js +216 -164
  14. package/dungeons/vertical/community.js +74 -41
  15. package/dungeons/vertical/crypto.js +343 -233
  16. package/dungeons/vertical/dating.js +246 -292
  17. package/dungeons/vertical/devtools.js +179 -70
  18. package/dungeons/vertical/ecommerce.js +275 -122
  19. package/dungeons/vertical/education.js +345 -312
  20. package/dungeons/vertical/fintech.js +445 -314
  21. package/dungeons/vertical/fitness.js +199 -165
  22. package/dungeons/vertical/food-delivery.js +309 -330
  23. package/dungeons/vertical/gaming.js +387 -241
  24. package/dungeons/vertical/healthcare.js +149 -66
  25. package/dungeons/vertical/insurance-application.js +173 -75
  26. package/dungeons/vertical/logistics.js +107 -12
  27. package/dungeons/vertical/marketplace.js +171 -76
  28. package/dungeons/vertical/media.js +259 -402
  29. package/dungeons/vertical/real-estate.js +336 -318
  30. package/dungeons/vertical/sass.js +294 -288
  31. package/dungeons/vertical/social.js +289 -234
  32. package/dungeons/vertical/travel.js +66 -20
  33. package/index.js +17 -17
  34. package/lib/core/config-validator.js +226 -175
  35. package/lib/core/context.js +10 -24
  36. package/lib/core/storage.js +6 -1
  37. package/lib/generators/events.js +57 -104
  38. package/lib/generators/funnels.js +212 -93
  39. package/lib/generators/mirror.js +3 -2
  40. package/lib/generators/product-names.js +1 -1
  41. package/lib/generators/scd.js +2 -1
  42. package/lib/generators/text.js +1 -1
  43. package/lib/hook-helpers/_internal.js +23 -0
  44. package/lib/hook-helpers/cohort.js +124 -0
  45. package/lib/hook-helpers/identity.js +56 -0
  46. package/lib/hook-helpers/index.js +44 -0
  47. package/lib/hook-helpers/inject.js +99 -0
  48. package/lib/hook-helpers/mutate.js +151 -0
  49. package/lib/hook-helpers/timing.js +99 -0
  50. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  51. package/lib/hook-patterns/attributed-by-source.js +72 -0
  52. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  53. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  54. package/lib/hook-patterns/index.js +14 -0
  55. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  56. package/lib/orchestrators/user-loop.js +143 -293
  57. package/lib/utils/utils.js +97 -52
  58. package/lib/verify/emulate-breakdown.js +281 -0
  59. package/lib/verify/index.js +12 -0
  60. package/lib/verify/verify-dungeon.js +61 -0
  61. package/package.json +6 -4
  62. package/scripts/smoke-test-all.mjs +162 -0
  63. package/scripts/verify-runner.mjs +72 -24
  64. package/types.d.ts +434 -225
  65. package/dungeons/technical/ad-spend-schema.json +0 -128
  66. package/dungeons/technical/anonymous-users-schema.json +0 -92
  67. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  68. package/dungeons/technical/experiments-schema.json +0 -203
  69. package/dungeons/technical/foobar-schema.json +0 -362
  70. package/dungeons/technical/group-analytics-schema.json +0 -241
  71. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  72. package/dungeons/technical/nested-objects-schema.json +0 -145
  73. package/dungeons/technical/retention-cadence-schema.json +0 -37
  74. package/dungeons/technical/sanity-schema.json +0 -185
  75. package/dungeons/technical/scale-test-schema.json +0 -70
  76. package/dungeons/technical/scd-schema.json +0 -467
  77. package/dungeons/technical/simple-schema.json +0 -362
  78. package/dungeons/technical/text-generation-schema.json +0 -1062
  79. package/dungeons/vertical/ai-platform-schema.json +0 -617
  80. package/dungeons/vertical/community-schema.json +0 -579
  81. package/dungeons/vertical/crypto-schema.json +0 -546
  82. package/dungeons/vertical/dating-schema.json +0 -401
  83. package/dungeons/vertical/devtools-schema.json +0 -601
  84. package/dungeons/vertical/ecommerce-schema.json +0 -604
  85. package/dungeons/vertical/education-schema.json +0 -5686
  86. package/dungeons/vertical/fintech-schema.json +0 -630
  87. package/dungeons/vertical/fitness-schema.json +0 -530
  88. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  89. package/dungeons/vertical/gaming-schema.json +0 -2703
  90. package/dungeons/vertical/healthcare-schema.json +0 -549
  91. package/dungeons/vertical/insurance-application-schema.json +0 -485
  92. package/dungeons/vertical/logistics-schema.json +0 -574
  93. package/dungeons/vertical/marketplace-schema.json +0 -533
  94. package/dungeons/vertical/media-schema.json +0 -4749
  95. package/dungeons/vertical/real-estate-schema.json +0 -527
  96. package/dungeons/vertical/sass-schema.json +0 -3128
  97. package/dungeons/vertical/social-schema.json +0 -620
  98. package/dungeons/vertical/travel-schema.json +0 -580
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "dm4-marketplace";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -16,9 +16,6 @@ import * as v from "ak-tools";
16
16
 
17
17
  dayjs.extend(utc);
18
18
  const chance = u.initChance(SEED);
19
- const NOW = dayjs();
20
- const DATASET_START = NOW.subtract(num_days, "days");
21
-
22
19
  /** @typedef {import("../../types").Dungeon} Config */
23
20
 
24
21
  // Generate consistent seller store and listing IDs at module level
@@ -56,33 +53,33 @@ const listingIds = v.range(1, 500).map(() => `LST_${v.uid(8)}`);
56
53
 
57
54
  /**
58
55
  * ═══════════════════════════════════════════════════════════════
59
- * ANALYTICS HOOKS (8 hooks)
56
+ * ANALYTICS HOOKS (10 hooks)
57
+ *
58
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
59
+ * raw-prop breakdowns (segment, day, category) or behavioral cohorts.
60
+ *
61
+ * Adds 9. BROWSE TO PURCHASE TIME-TO-CONVERT and 10. MESSAGE-COUNT MAGIC
62
+ * NUMBER on top of original 8.
60
63
  * ═══════════════════════════════════════════════════════════════
61
64
  *
62
65
  * ───────────────────────────────────────────────────────────────
63
- * 1. FEE CHANGE IMPACT (event hook)
66
+ * 1. FEE CHANGE IMPACT (everything hook)
64
67
  * ───────────────────────────────────────────────────────────────
65
68
  *
66
69
  * PATTERN: After day 45 (permanent marketplace fee increase), all
67
70
  * "listing created" events get listing_fee multiplied by 1.3x.
68
- * Simulates the revenue impact of a platform fee adjustment.
71
+ * Simulates the revenue impact of a platform fee adjustment. No flag —
72
+ * discover via line chart of avg listing_fee over time.
69
73
  *
70
74
  * HOW TO FIND IT IN MIXPANEL:
71
75
  *
72
- * Report 1: Listing Fee Before vs After Fee Change
76
+ * Report 1: Listing Fee Over Time
73
77
  * • Report type: Insights
74
78
  * • Event: "listing created"
75
79
  * • Measure: Average of "listing_fee"
76
80
  * • Line chart by week
77
81
  * • Expected: Clear step-up around day 45 from ~$15 avg to ~$20 avg
78
82
  *
79
- * Report 2: Fee Change Flag Distribution
80
- * • Report type: Insights
81
- * • Event: "listing created"
82
- * • Measure: Total
83
- * • Breakdown: "fee_change"
84
- * • Expected: "increased" appears only after day 45
85
- *
86
83
  * REAL-WORLD ANALOGUE: Marketplace platforms periodically adjust
87
84
  * commission/listing fees, impacting seller economics and behavior.
88
85
  *
@@ -238,30 +235,84 @@ const listingIds = v.range(1, 500).map(() => `LST_${v.uid(8)}`);
238
235
  * REAL-WORLD ANALOGUE: Returning buyers have established trust
239
236
  * and familiarity with the platform, converting at higher rates.
240
237
  *
238
+ * ───────────────────────────────────────────────────────────────
239
+ * 9. BROWSE TO PURCHASE TIME-TO-CONVERT (funnel-post)
240
+ *
241
+ * PATTERN: Power_seller and frequent_buyer users complete the
242
+ * Browse to Purchase funnel 1.4x faster (factor 0.71); window_shopper
243
+ * 1.4x slower (factor 1.4).
244
+ *
245
+ * HOW TO FIND IT IN MIXPANEL:
246
+ *
247
+ * Report 1: Browse to Purchase Median Time-to-Convert by Segment
248
+ * - Funnels > "item searched" -> "item viewed" -> "add to cart" -> "purchase completed"
249
+ * - Measure: Median time to convert
250
+ * - Breakdown: segment
251
+ * - Expected: power/frequent ~ 0.71x; window ~ 1.4x
252
+ *
253
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel
254
+ * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
255
+ * show this — funnel-post adjusts gaps within funnel instances, not
256
+ * across the user's full event history.
257
+ *
258
+ * ───────────────────────────────────────────────────────────────
259
+ * 10. MESSAGE-COUNT MAGIC NUMBER (in-funnel, everything)
260
+ *
261
+ * PATTERN: Sweet 2-5 message-sent events between item-viewed and
262
+ * offer-received → +35% on offer_amount of offer-received events.
263
+ * Over 6+ → drop 25% of purchase-completed events (haggling deadlock).
264
+ * No flag.
265
+ *
266
+ * HOW TO FIND IT IN MIXPANEL:
267
+ *
268
+ * Report 1: Avg Offer Amount by Message Bucket
269
+ * - Cohort A: users with 2-5 messages between item-viewed and offer-received
270
+ * - Cohort B: users with 0-1
271
+ * - Event: "offer received"
272
+ * - Measure: Average of "offer_amount"
273
+ * - Expected: A ~ 1.35x B
274
+ *
275
+ * Report 2: Purchase Rate on Heavy Messengers
276
+ * - Cohort C: users with >= 6 messages
277
+ * - Cohort A: users with 2-5
278
+ * - Event: "purchase completed"
279
+ * - Measure: Total per user
280
+ * - Expected: C ~ 25% fewer purchases per user
281
+ *
282
+ * REAL-WORLD ANALOGUE: A few quick clarifying messages close deals;
283
+ * extended haggling kills conversion.
284
+ *
241
285
  * ═══════════════════════════════════════════════════════════════
242
286
  * EXPECTED METRICS SUMMARY
243
287
  * ═══════════════════════════════════════════════════════════════
244
288
  *
245
- * Hook | Metric | Baseline | Effect | Ratio
246
- * ────────────────────────────|─────────────────────|──────────|─────────|──────
247
- * Fee Change Impact | listing_fee | $15 | $20 | 1.3x
248
- * Weekend Shopping Surge | total_amount | $60 | $72 | 1.2x
249
- * Seller Success → Trust | purchases/user | 3 | 6 | 2x
250
- * Electronics Category Lift | electronics purch. | baseline | 1.5x | 1.5x
251
- * Response Time → Conversion | offer_accepted/user | 1 | 2 | 2x
252
- * New Seller Churn | events post-day-14 | 100% | 60% | 0.6x
253
- * Power Seller Profiles | total_transactions | 0 | 100-500 | --
254
- * Frequent Buyer Funnel | funnel conversion | 30% | 39% | 1.3x
289
+ * Hook | Metric | Baseline | Effect | Ratio
290
+ * ----------------------------|---------------------|----------|-----------|------
291
+ * Fee Change Impact | listing_fee post-D45| 1x | 1.3x | 1.3x
292
+ * Weekend Shopping Surge | total_amount Sat/Sun| 1x | 1.2x | 1.2x
293
+ * Seller Success → Trust | purchases/user | 1x | 2x | 2x
294
+ * Electronics Category Lift | electronics purch. | 1x | 1.5x | 1.5x
295
+ * Response Time → Conversion | offer_accepted/user | 1x | 2x | 2x
296
+ * New Seller Churn | events post-D14 | 1x | 0.6x | -40%
297
+ * Power Seller Profiles | total_transactions | 0 | 100-500 | --
298
+ * Frequent Buyer Funnel | funnel conversion | 30% | 39% | 1.3x
299
+ * Browse to Purchase T2C | median min by segment| 1x | 0.71/1.4x | ~ 2x range
300
+ * Message Magic Number | sweet offer_amount | 1x | 1.35x | 1.35x
301
+ * Message Magic Number | over purchases/user | 1x | 0.75x | -25%
255
302
  */
256
303
 
257
304
  /** @type {Config} */
258
305
  const config = {
306
+ version: 2,
259
307
  token,
260
308
  seed: SEED,
261
- numDays: num_days,
309
+ datasetStart: "2026-01-01T00:00:00Z",
310
+ datasetEnd: "2026-05-01T23:59:59Z",
311
+ // numDays: num_days,
262
312
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
263
313
  numUsers: num_users,
264
- hasAnonIds: false,
314
+ hasAnonIds: true,
315
+ avgDevicePerUser: 2,
265
316
  hasSessionIds: true,
266
317
  format: "json",
267
318
  gzip: true,
@@ -294,6 +345,7 @@ const config = {
294
345
  event: "account created",
295
346
  weight: 1,
296
347
  isFirstEvent: true,
348
+ isAuthEvent: true,
297
349
  properties: {
298
350
  signup_source: ["organic", "google_shopping", "tiktok", "seller_referral", "email_campaign", "word_of_mouth"],
299
351
  },
@@ -336,7 +388,6 @@ const config = {
336
388
  item_count: u.weighNumRange(1, 5, 0.3),
337
389
  payment_method: ["credit_card", "credit_card", "paypal", "apple_pay", "debit"],
338
390
  shipping_method: ["standard", "standard", "express", "pickup"],
339
- is_whale_purchase: [false],
340
391
  },
341
392
  },
342
393
  {
@@ -348,7 +399,6 @@ const config = {
348
399
  asking_price: u.weighNumRange(5, 500, 0.3, 50),
349
400
  condition: ["new", "new", "like_new", "good", "fair"],
350
401
  listing_fee: u.weighNumRange(5, 30, 0.5, 15),
351
- fee_change: ["none"],
352
402
  },
353
403
  },
354
404
  {
@@ -728,31 +778,34 @@ const config = {
728
778
  }
729
779
  }
730
780
 
731
- // ── HOOK 8: FREQUENT BUYER FUNNEL LIFT (funnel-pre) ──
732
- // (conversionRate filtering moved to everything hook)
733
- if (type === "funnel-pre") {
734
- // no-op: conversion filtering handled via event dropping in everything hook
735
- }
736
-
737
- // ── HOOK 1: FEE CHANGE IMPACT (event) ────────────────
738
- // Listings after day 45 get 1.3x higher listing_fee.
739
- if (type === "event") {
740
- const FEE_CHANGE_DAY = DATASET_START.add(45, "days");
741
- if (record.event === "listing created") {
742
- const eventTime = dayjs(record.time);
743
- if (eventTime.isAfter(FEE_CHANGE_DAY)) {
744
- record.listing_fee = Math.floor((record.listing_fee || 15) * 1.3);
745
- record.fee_change = "increased";
781
+ // HOOK 9 (T2C): BROWSE TO PURCHASE TIME-TO-CONVERT (funnel-post)
782
+ // Power_seller users complete Browse to Purchase funnel 1.4x faster
783
+ // (factor 0.71); window_shopper 1.4x slower (factor 1.4).
784
+ if (type === "funnel-post") {
785
+ const segment = meta?.profile?.segment;
786
+ if (Array.isArray(record) && record.length > 1) {
787
+ const factor = (
788
+ segment === "power_seller" || segment === "frequent_buyer" ? 0.71 :
789
+ segment === "window_shopper" ? 1.4 :
790
+ 1.0
791
+ );
792
+ if (factor !== 1.0) {
793
+ for (let i = 1; i < record.length; i++) {
794
+ const prev = dayjs(record[i - 1].time);
795
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
796
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
797
+ }
746
798
  }
747
799
  }
748
-
749
- // ── HOOK 2: WEEKEND SHOPPING SURGE ───────────────
750
- // Moved to the everything hook (after purchase cloning)
751
- // so the 1.2x boost applies to both original AND cloned purchases.
752
800
  }
753
801
 
802
+ // HOOK 1: FEE CHANGE IMPACT — moved to everything hook below.
803
+ // (event hook fires before bunchIntoSessions reshuffles timestamps)
804
+
754
805
  // ── EVERYTHING HOOKS ─────────────────────────────────
755
806
  if (type === "everything") {
807
+ const datasetStart = dayjs.unix(meta.datasetStart);
808
+ const FEE_CHANGE_DAY = datasetStart.add(45, "days");
756
809
  let events = record;
757
810
  if (!events.length) return record;
758
811
 
@@ -767,6 +820,14 @@ const config = {
767
820
  });
768
821
  }
769
822
 
823
+ // HOOK 1: FEE CHANGE IMPACT — listings after d45 get listing_fee 1.3x.
824
+ // In everything hook so timestamp comparison sees post-bunchIntoSessions times.
825
+ events.forEach(e => {
826
+ if (e.event === "listing created" && dayjs(e.time).isAfter(FEE_CHANGE_DAY)) {
827
+ e.listing_fee = Math.floor((e.listing_fee || 15) * 1.3);
828
+ }
829
+ });
830
+
770
831
  // ── HOOK 8: FREQUENT BUYER CONVERSION FILTER ────
771
832
  // Non-frequent-buyer users drop ~25% of "purchase completed"
772
833
  // (last step of Browse to Purchase funnel) to simulate lower conversion.
@@ -824,55 +885,89 @@ const config = {
824
885
  }
825
886
 
826
887
  // ── HOOK 5: RESPONSE TIME → CONVERSION ───────────
827
- // Sellers with avg response_time < 12 hours get more offer_accepted.
828
- // (response_time_hours centers around ~6h via weighNumRange, so a
829
- // threshold of 2h was too tight — almost no users qualified.)
888
+ // Users with low avg response_time on messages get extra
889
+ // offer_accepted clones; slow responders lose half their accepts.
830
890
  const messages = events.filter(e => e.event === "message sent" && e.response_time_hours);
831
891
  if (messages.length > 0) {
832
892
  const avgResponseTime = messages.reduce((sum, m) => sum + m.response_time_hours, 0) / messages.length;
833
- if (avgResponseTime < 12) {
834
- const templateOffer = events.find(e => e.event === "offer accepted");
835
- if (templateOffer) {
836
- const offers = events.filter(e => e.event === "offer received");
837
- offers.forEach(offer => {
838
- if (chance.bool({ likelihood: 50 })) {
839
- events.push({
840
- ...templateOffer,
841
- time: dayjs(offer.time).add(chance.integer({ min: 1, max: 4 }), "hours").toISOString(),
842
- user_id: offer.user_id,
843
- response_time_hours: chance.floating({ min: 0.1, max: 2, fixed: 1 }),
844
- });
845
- }
893
+ const templateOffer = events.find(e => e.event === "offer accepted");
894
+ if (avgResponseTime <= 11 && templateOffer) {
895
+ // Fast responders: clone every offer_received into an offer_accepted
896
+ const offers = events.filter(e => e.event === "offer received");
897
+ offers.forEach(offer => {
898
+ events.push({
899
+ ...templateOffer,
900
+ time: dayjs(offer.time).add(chance.integer({ min: 1, max: 4 }), "hours").toISOString(),
901
+ user_id: offer.user_id,
902
+ response_time_hours: chance.floating({ min: 0.1, max: 2, fixed: 1 }),
846
903
  });
904
+ });
905
+ } else if (avgResponseTime >= 25 && templateOffer) {
906
+ // Slow responders: drop half of offer_accepted events
907
+ for (let i = events.length - 1; i >= 0; i--) {
908
+ if (events[i].event === "offer accepted" && chance.bool({ likelihood: 50 })) {
909
+ events.splice(i, 1);
910
+ }
847
911
  }
848
912
  }
849
913
  }
850
914
 
851
915
  // ── HOOK 6: NEW SELLER CHURN ─────────────────────
852
- // New sellers with <10 events lose 40% of events after day 14.
853
- if (profile && profile.segment === "new_seller" && events.length < 10) {
854
- const DAY_14 = DATASET_START.add(14, "days");
855
- for (let i = events.length - 1; i >= 0; i--) {
856
- const eventTime = dayjs(events[i].time);
857
- if (eventTime.isAfter(DAY_14) && chance.bool({ likelihood: 40 })) {
858
- events.splice(i, 1);
916
+ // New sellers lose 50% of events after their first 14 days.
917
+ // Uses user's first event as anchor (not dataset start).
918
+ if (profile && profile.segment === "new_seller") {
919
+ const firstEventTime = events.length > 0 ? dayjs(events[0].time) : null;
920
+ if (firstEventTime) {
921
+ const userDay14 = firstEventTime.add(14, "days");
922
+ for (let i = events.length - 1; i >= 0; i--) {
923
+ if (dayjs(events[i].time).isAfter(userDay14) && chance.bool({ likelihood: 50 })) {
924
+ events.splice(i, 1);
925
+ }
859
926
  }
860
927
  }
861
928
  }
862
929
 
863
- // ── HOOK 2: WEEKEND SHOPPING SURGE (everything) ─────
864
- // Applied AFTER all purchase cloning (Hooks 3, 4) so the
865
- // 1.2x boost hits both original and injected purchases.
930
+ // HOOK 2: WEEKEND SHOPPING SURGE Sat/Sun purchases get
931
+ // total_amount 1.2x. Mutates raw prop. No flag.
866
932
  events.forEach(e => {
867
933
  if (e.event === "purchase completed") {
868
934
  const dow = new Date(e.time).getUTCDay();
869
- // Saturday=6, Sunday=0
870
935
  if (dow === 0 || dow === 6) {
871
936
  e.total_amount = Math.floor((e.total_amount || 60) * 1.2);
872
937
  }
873
938
  }
874
939
  });
875
940
 
941
+ // HOOK 10: MESSAGE-COUNT MAGIC NUMBER (in-funnel, no flags)
942
+ // Sweet 2-5 message-sent events between item-viewed and
943
+ // offer-received → +35% on offer-received offer_amount/asking_price.
944
+ // Over 6+ → drop 25% of purchase-completed events (over-message
945
+ // signals haggling deadlock).
946
+ const itemViewed = events.find(e => e.event === "item viewed");
947
+ const offerReceived = events.find(e => e.event === "offer received");
948
+ if (itemViewed && offerReceived) {
949
+ const aTime = dayjs(itemViewed.time);
950
+ const bTime = dayjs(offerReceived.time);
951
+ const msgBetween = events.filter(e =>
952
+ e.event === "message sent" &&
953
+ dayjs(e.time).isAfter(aTime) &&
954
+ dayjs(e.time).isBefore(bTime)
955
+ ).length;
956
+ if (msgBetween >= 2 && msgBetween <= 5) {
957
+ events.forEach(e => {
958
+ if (e.event === "offer received" && typeof e.offer_amount === "number") {
959
+ e.offer_amount = Math.round(e.offer_amount * 1.35);
960
+ }
961
+ });
962
+ } else if (msgBetween >= 6) {
963
+ for (let i = events.length - 1; i >= 0; i--) {
964
+ if (events[i].event === "purchase completed" && chance.bool({ likelihood: 25 })) {
965
+ events.splice(i, 1);
966
+ }
967
+ }
968
+ }
969
+ }
970
+
876
971
  return record;
877
972
  }
878
973