@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.
- package/CHANGELOG.md +58 -0
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +143 -293
- package/lib/utils/utils.js +97 -52
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "dm4-healthcare";
|
|
3
|
-
const num_days =
|
|
4
|
-
const num_users =
|
|
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 doctor/clinic IDs at module level
|
|
@@ -54,7 +51,10 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
|
|
|
54
51
|
|
|
55
52
|
/**
|
|
56
53
|
* ═══════════════════════════════════════════════════════════════
|
|
57
|
-
* ANALYTICS HOOKS (
|
|
54
|
+
* ANALYTICS HOOKS (10 hooks)
|
|
55
|
+
*
|
|
56
|
+
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
|
|
57
|
+
* via raw-prop breakdowns (HOD, day, tier) or behavioral cohorts.
|
|
58
58
|
* ═══════════════════════════════════════════════════════════════
|
|
59
59
|
*
|
|
60
60
|
* ───────────────────────────────────────────────────────────────
|
|
@@ -70,9 +70,9 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
|
|
|
70
70
|
* • Report type: Insights
|
|
71
71
|
* • Event: "consultation completed"
|
|
72
72
|
* • Measure: Average of "consultation_fee"
|
|
73
|
-
* • Breakdown:
|
|
74
|
-
* • Expected:
|
|
75
|
-
* (
|
|
73
|
+
* • Breakdown: hour of day
|
|
74
|
+
* • Expected: hours 19-06 UTC ~ 1.5x avg fee vs hours 07-18
|
|
75
|
+
* (after-hours ≈ $112, business ≈ $75)
|
|
76
76
|
*
|
|
77
77
|
* REAL-WORLD ANALOGUE: Telehealth platforms charge premiums for
|
|
78
78
|
* after-hours urgent consultations, a key revenue driver.
|
|
@@ -226,30 +226,82 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
|
|
|
226
226
|
* REAL-WORLD ANALOGUE: Free-tier patients face longer wait times
|
|
227
227
|
* and limited scheduling, reducing completed consultations.
|
|
228
228
|
*
|
|
229
|
+
* ───────────────────────────────────────────────────────────────
|
|
230
|
+
* 9. BOOKING FUNNEL TIME-TO-CONVERT (funnel-post)
|
|
231
|
+
*
|
|
232
|
+
* PATTERN: Premium tier completes Booking funnel 1.35x faster
|
|
233
|
+
* (factor 0.74); Free tier 1.33x slower (factor 1.33).
|
|
234
|
+
*
|
|
235
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
236
|
+
*
|
|
237
|
+
* Report 1: Booking Funnel Median Time-to-Convert by Tier
|
|
238
|
+
* - Funnels > "symptom search" -> "appointment booked" -> "consultation completed"
|
|
239
|
+
* - Measure: Median time to convert
|
|
240
|
+
* - Breakdown: subscription_tier
|
|
241
|
+
* - Expected: premium ~ 0.74x; free ~ 1.33x
|
|
242
|
+
*
|
|
243
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel
|
|
244
|
+
* median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
|
|
245
|
+
* show this — funnel-post adjusts gaps within funnel instances, not
|
|
246
|
+
* across the user's full event history.
|
|
247
|
+
*
|
|
248
|
+
* ───────────────────────────────────────────────────────────────
|
|
249
|
+
* 10. CONSULTATION-COUNT MAGIC NUMBER (everything)
|
|
250
|
+
*
|
|
251
|
+
* PATTERN: Sweet 3-6 consultations → +25% on consultation_fee.
|
|
252
|
+
* Over 7+ → days_until_followup multiplied by 1.5 (over-consulted
|
|
253
|
+
* patients wait 50% longer for next visit). No flag.
|
|
254
|
+
*
|
|
255
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
256
|
+
*
|
|
257
|
+
* Report 1: Avg Consultation Fee by Consult-Count Bucket
|
|
258
|
+
* - Cohort A: users with 3-6 "consultation completed"
|
|
259
|
+
* - Cohort B: users with 0-2
|
|
260
|
+
* - Event: "consultation completed"
|
|
261
|
+
* - Measure: Average of "consultation_fee"
|
|
262
|
+
* - Expected: A ~ 1.25x B
|
|
263
|
+
*
|
|
264
|
+
* Report 2: Follow-Up Wait Time on Heavy Consulters
|
|
265
|
+
* - Cohort C: users with >= 7 consultations
|
|
266
|
+
* - Cohort A: users with 3-6
|
|
267
|
+
* - Event: "follow up scheduled"
|
|
268
|
+
* - Measure: Average of "days_until_followup"
|
|
269
|
+
* - Expected: C ~ 1.5x A (longer gap before next visit)
|
|
270
|
+
*
|
|
271
|
+
* REAL-WORLD ANALOGUE: Engaged patients pay more; over-engaged
|
|
272
|
+
* patients hit care-fatigue and stretch the gap to next visit.
|
|
273
|
+
*
|
|
229
274
|
* ═══════════════════════════════════════════════════════════════
|
|
230
275
|
* EXPECTED METRICS SUMMARY
|
|
231
276
|
* ═══════════════════════════════════════════════════════════════
|
|
232
277
|
*
|
|
233
|
-
* Hook | Metric | Baseline | Effect
|
|
234
|
-
*
|
|
235
|
-
* After-Hours Pricing | consultation_fee |
|
|
236
|
-
* Flu Season Spike | respiratory
|
|
237
|
-
* Experienced Doctor Sat. | satisfaction_score
|
|
238
|
-
* Video Follow-Up Lift | follow-ups/user |
|
|
239
|
-
* Chronic Refill Chain | refills (chronic) | 1 | 3-4
|
|
240
|
-
* Occasional No-Shows | booking→consult | 95% | 75%
|
|
241
|
-
* Doctor Specialization | years_experience | 5 | 22
|
|
242
|
-
* Free-Tier Conversion Drop | funnel conversion | 40% | 28%
|
|
278
|
+
* Hook | Metric | Baseline | Effect | Ratio
|
|
279
|
+
* ----------------------------|---------------------|----------|-----------|------
|
|
280
|
+
* After-Hours Pricing | consultation_fee | 1x | 1.5x | 1.5x
|
|
281
|
+
* Flu Season Spike | respiratory share | ~ 15% | ~ 60% | 4x
|
|
282
|
+
* Experienced Doctor Sat. | satisfaction_score | 3.5 | 4.2 | 1.2x
|
|
283
|
+
* Video Follow-Up Lift | follow-ups/user | 1x | 2x | 2x
|
|
284
|
+
* Chronic Refill Chain | refills (chronic) | 1 | 3-4 | 3-4x
|
|
285
|
+
* Occasional No-Shows | booking→consult | 95% | 75% | -20%
|
|
286
|
+
* Doctor Specialization | years_experience | 5 | 22 | 4.4x
|
|
287
|
+
* Free-Tier Conversion Drop | funnel conversion | 40% | 28% | -30%
|
|
288
|
+
* Booking T2C | median min by tier | 1x | 0.74/1.33x| ~ 1.8x range
|
|
289
|
+
* Consult-Count Magic Number | sweet consult fee | 1x | 1.25x | 1.25x
|
|
290
|
+
* Consult-Count Magic Number | over days_until_fu | 1x | 1.5x | +50%
|
|
243
291
|
*/
|
|
244
292
|
|
|
245
293
|
/** @type {Config} */
|
|
246
294
|
const config = {
|
|
295
|
+
version: 2,
|
|
247
296
|
token,
|
|
248
297
|
seed: SEED,
|
|
249
|
-
|
|
298
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
299
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
300
|
+
// numDays: num_days,
|
|
250
301
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
251
302
|
numUsers: num_users,
|
|
252
|
-
hasAnonIds:
|
|
303
|
+
hasAnonIds: true,
|
|
304
|
+
avgDevicePerUser: 2,
|
|
253
305
|
hasSessionIds: true,
|
|
254
306
|
format: "json",
|
|
255
307
|
gzip: true,
|
|
@@ -282,6 +334,7 @@ const config = {
|
|
|
282
334
|
event: "account created",
|
|
283
335
|
weight: 1,
|
|
284
336
|
isFirstEvent: true,
|
|
337
|
+
isAuthEvent: true,
|
|
285
338
|
properties: {
|
|
286
339
|
referral_source: ["organic", "doctor_referral", "insurance_partner", "social_media", "search"],
|
|
287
340
|
},
|
|
@@ -303,7 +356,6 @@ const config = {
|
|
|
303
356
|
condition_type: ["general", "general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
|
|
304
357
|
wait_time_hours: u.weighNumRange(1, 72, 0.4),
|
|
305
358
|
appointment_type: ["new_patient", "follow_up", "follow_up", "urgent", "routine", "routine"],
|
|
306
|
-
after_hours: [false],
|
|
307
359
|
},
|
|
308
360
|
},
|
|
309
361
|
{
|
|
@@ -311,12 +363,11 @@ const config = {
|
|
|
311
363
|
weight: 5,
|
|
312
364
|
properties: {
|
|
313
365
|
doctor_id: chance.pickone.bind(chance, doctorIds),
|
|
314
|
-
consultation_mode: ["phone"
|
|
366
|
+
consultation_mode: ["phone", "phone", "video"],
|
|
315
367
|
duration_minutes: u.weighNumRange(5, 60, 0.6, 15),
|
|
316
368
|
consultation_fee: u.weighNumRange(25, 200, 0.4, 75),
|
|
317
369
|
satisfaction_score: u.weighNumRange(1, 5, 0.8, 3),
|
|
318
370
|
condition_type: ["general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
|
|
319
|
-
after_hours: [false],
|
|
320
371
|
},
|
|
321
372
|
},
|
|
322
373
|
{
|
|
@@ -346,7 +397,7 @@ const config = {
|
|
|
346
397
|
doctor_id: chance.pickone.bind(chance, doctorIds),
|
|
347
398
|
days_until_followup: u.weighNumRange(3, 30, 0.5, 7),
|
|
348
399
|
condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
|
|
349
|
-
consultation_mode: ["phone"],
|
|
400
|
+
consultation_mode: ["phone", "phone", "video"],
|
|
350
401
|
},
|
|
351
402
|
},
|
|
352
403
|
{
|
|
@@ -577,8 +628,8 @@ const config = {
|
|
|
577
628
|
{ name: "premium", price: 29.99 },
|
|
578
629
|
],
|
|
579
630
|
lifecycle: {
|
|
580
|
-
trialToPayRate: 0.
|
|
581
|
-
upgradeRate: 0.
|
|
631
|
+
trialToPayRate: 0.55,
|
|
632
|
+
upgradeRate: 0.20,
|
|
582
633
|
downgradeRate: 0.03,
|
|
583
634
|
churnRate: 0.05,
|
|
584
635
|
winBackRate: 0.10,
|
|
@@ -655,44 +706,38 @@ const config = {
|
|
|
655
706
|
}
|
|
656
707
|
}
|
|
657
708
|
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (
|
|
670
|
-
record.
|
|
671
|
-
|
|
672
|
-
|
|
709
|
+
// HOOK 9 (T2C): BOOKING FUNNEL TIME-TO-CONVERT (funnel-post)
|
|
710
|
+
// Premium tier completes Booking funnel 1.35x faster (factor 0.74);
|
|
711
|
+
// Free tier 1.33x slower (factor 1.33).
|
|
712
|
+
if (type === "funnel-post") {
|
|
713
|
+
const segment = meta?.profile?.subscription_tier;
|
|
714
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
715
|
+
const factor = (
|
|
716
|
+
segment === "premium" ? 0.74 :
|
|
717
|
+
segment === "free" ? 1.33 :
|
|
718
|
+
1.0
|
|
719
|
+
);
|
|
720
|
+
if (factor !== 1.0) {
|
|
721
|
+
for (let i = 1; i < record.length; i++) {
|
|
722
|
+
const prev = dayjs(record[i - 1].time);
|
|
723
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
724
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
673
725
|
}
|
|
674
726
|
}
|
|
675
727
|
}
|
|
676
|
-
|
|
677
|
-
// ── HOOK 2: FLU SEASON SPIKE (event) ─────────────
|
|
678
|
-
// Days 50-70: respiratory conditions dominate, wait times double.
|
|
679
|
-
const FLU_START = DATASET_START.add(50, "days");
|
|
680
|
-
const FLU_END = DATASET_START.add(70, "days");
|
|
681
|
-
const eventTime = dayjs(record.time);
|
|
682
|
-
if (record.event === "appointment booked" && eventTime.isAfter(FLU_START) && eventTime.isBefore(FLU_END)) {
|
|
683
|
-
if (chance.bool({ likelihood: 60 })) {
|
|
684
|
-
record.condition_type = "respiratory";
|
|
685
|
-
}
|
|
686
|
-
if (record.condition_type === "respiratory") {
|
|
687
|
-
record.wait_time_hours = Math.floor((record.wait_time_hours || 12) * 2);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
728
|
}
|
|
691
729
|
|
|
730
|
+
// (HOOK 1: AFTER-HOURS SURGE PRICING moved to everything hook — hour checks
|
|
731
|
+
// must run after bunchIntoSessions redistributes timestamps)
|
|
732
|
+
// (HOOK 2: FLU SEASON SPIKE moved to everything hook — same reason)
|
|
733
|
+
|
|
692
734
|
// ── EVERYTHING HOOKS ─────────────────────────────────
|
|
693
735
|
if (type === "everything") {
|
|
694
736
|
if (!record.length) return record;
|
|
695
737
|
const profile = meta.profile;
|
|
738
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
739
|
+
const FLU_START = datasetStart.add(50, "days");
|
|
740
|
+
const FLU_END = datasetStart.add(70, "days");
|
|
696
741
|
|
|
697
742
|
// ── SUPER-PROP STAMPING ──────────────────────────
|
|
698
743
|
// Stamp superProps from profile so they are consistent per-user.
|
|
@@ -705,17 +750,36 @@ const config = {
|
|
|
705
750
|
});
|
|
706
751
|
}
|
|
707
752
|
|
|
753
|
+
// HOOK 1: AFTER-HOURS SURGE PRICING — consultations 7PM-7AM
|
|
754
|
+
// UTC get consultation_fee 1.5x. No flag — discover via HOD chart.
|
|
755
|
+
record.forEach(e => {
|
|
756
|
+
if (e.event === "consultation completed" || e.event === "appointment booked") {
|
|
757
|
+
const hour = new Date(e.time).getUTCHours();
|
|
758
|
+
if ((hour >= 19 || hour < 7) && e.consultation_fee) {
|
|
759
|
+
e.consultation_fee = Math.floor(e.consultation_fee * 1.5);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// HOOK 2: FLU SEASON SPIKE — d50-70 respiratory dominates, wait_time doubles.
|
|
765
|
+
// Runs in everything hook so timestamp checks see post-bunchIntoSessions times.
|
|
766
|
+
record.forEach(e => {
|
|
767
|
+
if (e.event !== "appointment booked") return;
|
|
768
|
+
const t = dayjs(e.time);
|
|
769
|
+
if (t.isAfter(FLU_START) && t.isBefore(FLU_END)) {
|
|
770
|
+
if (chance.bool({ likelihood: 60 })) e.condition_type = "respiratory";
|
|
771
|
+
if (e.condition_type === "respiratory") {
|
|
772
|
+
e.wait_time_hours = Math.floor((e.wait_time_hours || 12) * 2);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
|
|
708
777
|
// ── HOOK 8: FREE-TIER CONVERSION DROP ────────────
|
|
709
778
|
// Free-tier users lose ~30% of "consultation completed" events
|
|
710
779
|
// (last step of Booking to Consultation funnel), simulating
|
|
711
780
|
// lower conversion for non-paying patients.
|
|
712
|
-
if (profile && profile.subscription_tier === "free") {
|
|
713
|
-
record = record.filter(e =>
|
|
714
|
-
if (e.event === "consultation completed" && chance.bool({ likelihood: 30 })) {
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
return true;
|
|
718
|
-
});
|
|
781
|
+
if (profile && profile.subscription_tier === "free" && chance.bool({ likelihood: 30 })) {
|
|
782
|
+
record = record.filter(e => e.event !== "consultation completed");
|
|
719
783
|
}
|
|
720
784
|
|
|
721
785
|
// ── HOOK 3: EXPERIENCED DOCTOR SATISFACTION ──────
|
|
@@ -783,16 +847,35 @@ const config = {
|
|
|
783
847
|
}
|
|
784
848
|
}
|
|
785
849
|
|
|
786
|
-
//
|
|
787
|
-
//
|
|
850
|
+
// HOOK 6: OCCASIONAL PATIENT NO-SHOWS — low-activity patients
|
|
851
|
+
// (< 15 events) lose 25% of consultations (they booked but didn't show).
|
|
788
852
|
if (record.length < 15) {
|
|
789
853
|
for (let i = record.length - 1; i >= 0; i--) {
|
|
790
|
-
if (record[i].event === "
|
|
854
|
+
if (record[i].event === "consultation completed" && chance.bool({ likelihood: 25 })) {
|
|
791
855
|
record.splice(i, 1);
|
|
792
856
|
}
|
|
793
857
|
}
|
|
794
858
|
}
|
|
795
859
|
|
|
860
|
+
// HOOK 10: CONSULTATION-COUNT MAGIC NUMBER (no flags)
|
|
861
|
+
// Sweet 3-6 consultations → +25% on consultation_fee. Over 7+ →
|
|
862
|
+
// drop 30% of "follow up scheduled" events (over-consulted →
|
|
863
|
+
// follow-up fatigue).
|
|
864
|
+
const consultCt = record.filter(e => e.event === "consultation completed").length;
|
|
865
|
+
if (consultCt >= 3 && consultCt <= 6) {
|
|
866
|
+
record.forEach(e => {
|
|
867
|
+
if (e.event === "consultation completed" && typeof e.consultation_fee === "number") {
|
|
868
|
+
e.consultation_fee = Math.round(e.consultation_fee * 1.25);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
} else if (consultCt >= 7) {
|
|
872
|
+
record.forEach(e => {
|
|
873
|
+
if (e.event === "follow up scheduled" && typeof e.days_until_followup === "number") {
|
|
874
|
+
e.days_until_followup = Math.round(e.days_until_followup * 1.5);
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
796
879
|
return record;
|
|
797
880
|
}
|
|
798
881
|
|