@ak--47/dungeon-master 1.0.0 → 1.2.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 +7 -7
- package/dungeons/technical/ad-spend-schema.json +128 -0
- package/dungeons/technical/ad-spend.js +116 -0
- package/dungeons/technical/anonymous-users-schema.json +92 -0
- package/dungeons/technical/anonymous-users.js +102 -0
- package/dungeons/technical/array-of-object-lookup-schema.json +191 -0
- package/dungeons/{array-of-object-lookup.js → technical/array-of-object-lookup.js} +12 -5
- package/dungeons/technical/experiments-schema.json +203 -0
- package/dungeons/technical/experiments.js +168 -0
- package/dungeons/technical/foobar-schema.json +362 -0
- package/dungeons/{foobar.js → technical/foobar.js} +3 -3
- package/dungeons/technical/group-analytics-schema.json +241 -0
- package/dungeons/technical/group-analytics.js +151 -0
- package/dungeons/technical/mirror-strategies-schema.json +84 -0
- package/dungeons/technical/mirror-strategies.js +126 -0
- package/dungeons/technical/nested-objects-schema.json +145 -0
- package/dungeons/technical/nested-objects.js +207 -0
- package/dungeons/technical/retention-cadence-schema.json +37 -0
- package/dungeons/{retention-cadence.js → technical/retention-cadence.js} +2 -3
- package/dungeons/technical/sanity-schema.json +185 -0
- package/dungeons/{sanity.js → technical/sanity.js} +4 -2
- package/dungeons/technical/scale-test-schema.json +70 -0
- package/dungeons/technical/scale-test.js +90 -0
- package/dungeons/technical/scd-schema.json +467 -0
- package/dungeons/{scd.js → technical/scd.js} +20 -10
- package/dungeons/technical/simple-schema.json +362 -0
- package/dungeons/{simple.js → technical/simple.js} +7 -7
- package/dungeons/technical/simplest-schema.json +724 -0
- package/dungeons/{simplest.js → technical/simplest.js} +7 -7
- package/dungeons/technical/text-generation-schema.json +1062 -0
- package/dungeons/{text-generation.js → technical/text-generation.js} +24 -11
- package/dungeons/vertical/community-schema.json +579 -0
- package/dungeons/vertical/community.js +794 -0
- package/dungeons/vertical/devtools-schema.json +601 -0
- package/dungeons/vertical/devtools.js +843 -0
- package/dungeons/vertical/ecommerce-schema.json +604 -0
- package/dungeons/{ecommerce.js → vertical/ecommerce.js} +43 -5
- package/dungeons/vertical/education-schema.json +5686 -0
- package/dungeons/{education.js → vertical/education.js} +70 -39
- package/dungeons/vertical/fintech-schema.json +630 -0
- package/dungeons/{fintech.js → vertical/fintech.js} +107 -56
- package/dungeons/vertical/fitness-schema.json +530 -0
- package/dungeons/vertical/fitness.js +808 -0
- package/dungeons/vertical/food-delivery-schema.json +36728 -0
- package/dungeons/{food.js → vertical/food-delivery.js} +67 -26
- package/dungeons/vertical/gaming-schema.json +438 -0
- package/dungeons/{gaming.js → vertical/gaming.js} +29 -16
- package/dungeons/vertical/healthcare-schema.json +549 -0
- package/dungeons/vertical/healthcare.js +775 -0
- package/dungeons/vertical/insurance-application-schema.json +485 -0
- package/dungeons/{insurance-application.js → vertical/insurance-application.js} +41 -31
- package/dungeons/vertical/logistics-schema.json +574 -0
- package/dungeons/vertical/logistics.js +799 -0
- package/dungeons/vertical/marketplace-schema.json +533 -0
- package/dungeons/vertical/marketplace.js +850 -0
- package/dungeons/vertical/media-schema.json +4749 -0
- package/dungeons/{media.js → vertical/media.js} +103 -19
- package/dungeons/vertical/rpg-schema.json +2491 -0
- package/dungeons/{rpg.js → vertical/rpg.js} +87 -60
- package/dungeons/vertical/sass-schema.json +3128 -0
- package/dungeons/{sass.js → vertical/sass.js} +65 -26
- package/dungeons/vertical/social-schema.json +620 -0
- package/dungeons/{social.js → vertical/social.js} +76 -17
- package/dungeons/vertical/travel-schema.json +580 -0
- package/dungeons/vertical/travel.js +742 -0
- package/index.js +20 -16
- package/lib/core/config-validator.js +286 -1
- package/lib/core/context.js +16 -5
- package/lib/core/storage.js +2 -0
- package/lib/generators/adspend.js +3 -3
- package/lib/generators/events.js +141 -14
- package/lib/generators/funnels.js +67 -7
- package/lib/generators/text.js +2 -2
- package/lib/orchestrators/user-loop.js +444 -22
- package/lib/templates/soup-presets.js +2 -2
- package/lib/utils/utils.js +4 -5
- package/package.json +4 -3
- package/scripts/dungeon-to-json.mjs +163 -0
- package/scripts/extract-dungeon-schema.mjs +343 -0
- package/scripts/json-to-dungeon.mjs +200 -0
- package/scripts/run-dungeon.mjs +76 -0
- package/scripts/run-many.mjs +118 -0
- package/scripts/verify-runner.mjs +42 -0
- package/types.d.ts +316 -1
- package/dungeons/array-of-object-lookup-schema.json +0 -327
- package/dungeons/ecommerce-schema.json +0 -462
- package/dungeons/education-schema.json +0 -2409
- package/dungeons/fintech-schema.json +0 -14034
- package/dungeons/foobar-schema.json +0 -403
- package/dungeons/food-delivery-schema.json +0 -192
- package/dungeons/food-delivery.js +0 -602
- package/dungeons/food-schema.json +0 -1152
- package/dungeons/gaming-schema.json +0 -1270
- package/dungeons/insurance-application-schema.json +0 -204
- package/dungeons/media-schema.json +0 -906
- package/dungeons/retention-cadence-schema.json +0 -78
- package/dungeons/rpg-schema.json +0 -4526
- package/dungeons/sanity-schema.json +0 -255
- package/dungeons/sass-schema.json +0 -1291
- package/dungeons/scd-schema.json +0 -919
- package/dungeons/simple-schema.json +0 -608
- package/dungeons/simplest-schema.json +0 -1418
- package/dungeons/social-schema.json +0 -1118
- package/dungeons/text-generation-schema.json +0 -3096
|
@@ -0,0 +1,775 @@
|
|
|
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
|
+
import * as v from "ak-tools";
|
|
6
|
+
|
|
7
|
+
const SEED = "dm4-healthcare";
|
|
8
|
+
dayjs.extend(utc);
|
|
9
|
+
const chance = u.initChance(SEED);
|
|
10
|
+
const num_users = 5_000;
|
|
11
|
+
const days = 100;
|
|
12
|
+
const NOW = dayjs();
|
|
13
|
+
const DATASET_START = NOW.subtract(days, "days");
|
|
14
|
+
|
|
15
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
16
|
+
|
|
17
|
+
// Generate consistent doctor/clinic IDs at module level
|
|
18
|
+
const doctorIds = v.range(1, 120).map(() => `DR_${v.uid(6)}`);
|
|
19
|
+
const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* ═══════════════════════════════════════════════════════════════
|
|
23
|
+
* DATASET OVERVIEW
|
|
24
|
+
* ═══════════════════════════════════════════════════════════════
|
|
25
|
+
*
|
|
26
|
+
* MedConnect — a telehealth platform connecting doctors, nurses,
|
|
27
|
+
* and patients through virtual consultations, prescriptions,
|
|
28
|
+
* and secure messaging.
|
|
29
|
+
*
|
|
30
|
+
* - 5,000 users over 100 days, ~600K events
|
|
31
|
+
* - Multi-role system: doctors (5%), nurses (10%), patients (85%)
|
|
32
|
+
* - Core loop: sign up → book appointment → consultation → prescription → follow-up
|
|
33
|
+
* - Revenue: free (2 visits/month), basic ($9.99, 14-day trial), premium ($29.99 unlimited)
|
|
34
|
+
*
|
|
35
|
+
* Advanced Features:
|
|
36
|
+
* - Personas: 5 archetypes (doctor, nurse, patient_active, patient_occasional, patient_churner)
|
|
37
|
+
* - Subscription: 3-tier revenue lifecycle with trial conversion
|
|
38
|
+
* - Geo: US/EU/LATAM with timezone-aware scheduling and regional compliance
|
|
39
|
+
* - Features: video_consultation (day 30) and ai_symptom_checker (day 60)
|
|
40
|
+
*
|
|
41
|
+
* Key entities:
|
|
42
|
+
* - doctor_id: assigned doctor for a consultation
|
|
43
|
+
* - clinic_id: virtual clinic grouping
|
|
44
|
+
* - condition_type: medical specialty area
|
|
45
|
+
* - consultation_mode: phone vs video (driven by Feature rollout)
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ═══════════════════════════════════════════════════════════════
|
|
50
|
+
* ANALYTICS HOOKS (8 hooks)
|
|
51
|
+
* ═══════════════════════════════════════════════════════════════
|
|
52
|
+
*
|
|
53
|
+
* ───────────────────────────────────────────────────────────────
|
|
54
|
+
* 1. AFTER-HOURS SURGE PRICING (event hook)
|
|
55
|
+
* ───────────────────────────────────────────────────────────────
|
|
56
|
+
*
|
|
57
|
+
* PATTERN: Consultations between 7PM-7AM (after-hours) have 1.5x
|
|
58
|
+
* higher consultation_fee. Simulates urgent care premium pricing.
|
|
59
|
+
*
|
|
60
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
61
|
+
*
|
|
62
|
+
* Report 1: After-Hours Fee Premium
|
|
63
|
+
* • Report type: Insights
|
|
64
|
+
* • Event: "consultation completed"
|
|
65
|
+
* • Measure: Average of "consultation_fee"
|
|
66
|
+
* • Breakdown: "after_hours"
|
|
67
|
+
* • Expected: after_hours=true should show ~1.5x avg fee vs false
|
|
68
|
+
* (true ≈ $112, false ≈ $75)
|
|
69
|
+
*
|
|
70
|
+
* REAL-WORLD ANALOGUE: Telehealth platforms charge premiums for
|
|
71
|
+
* after-hours urgent consultations, a key revenue driver.
|
|
72
|
+
*
|
|
73
|
+
* ───────────────────────────────────────────────────────────────
|
|
74
|
+
* 2. FLU SEASON SPIKE (event hook)
|
|
75
|
+
* ───────────────────────────────────────────────────────────────
|
|
76
|
+
*
|
|
77
|
+
* PATTERN: During days 50-70 (flu season window), appointments
|
|
78
|
+
* with condition_type "respiratory" get 2x the wait_time and the
|
|
79
|
+
* condition is forced to "respiratory" 60% of the time.
|
|
80
|
+
*
|
|
81
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
82
|
+
*
|
|
83
|
+
* Report 1: Flu Season Volume
|
|
84
|
+
* • Report type: Insights
|
|
85
|
+
* • Event: "appointment booked"
|
|
86
|
+
* • Measure: Total
|
|
87
|
+
* • Filter: condition_type = "respiratory"
|
|
88
|
+
* • Line chart by week
|
|
89
|
+
* • Expected: Clear spike during flu season window (days 50-70)
|
|
90
|
+
*
|
|
91
|
+
* Report 2: Wait Time During Flu Season
|
|
92
|
+
* • Report type: Insights
|
|
93
|
+
* • Event: "appointment booked"
|
|
94
|
+
* • Measure: Average of "wait_time_hours"
|
|
95
|
+
* • Breakdown: "condition_type"
|
|
96
|
+
* • Filter: time within flu season
|
|
97
|
+
* • Expected: respiratory ~2x wait vs other conditions
|
|
98
|
+
*
|
|
99
|
+
* REAL-WORLD ANALOGUE: Seasonal illness creates predictable surges
|
|
100
|
+
* in appointment demand and wait times.
|
|
101
|
+
*
|
|
102
|
+
* ───────────────────────────────────────────────────────────────
|
|
103
|
+
* 3. EXPERIENCED DOCTOR SATISFACTION (everything hook)
|
|
104
|
+
* ───────────────────────────────────────────────────────────────
|
|
105
|
+
*
|
|
106
|
+
* PATTERN: Users who had >50 consultation events get higher avg
|
|
107
|
+
* satisfaction_score (boosted to 4.0-5.0 range) vs baseline 1-5.
|
|
108
|
+
* Simulates experienced doctors earning better reviews.
|
|
109
|
+
*
|
|
110
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
111
|
+
*
|
|
112
|
+
* Report 1: Satisfaction by Consultation Volume
|
|
113
|
+
* • Report type: Insights
|
|
114
|
+
* • Event: "consultation completed"
|
|
115
|
+
* • Measure: Average of "satisfaction_score"
|
|
116
|
+
* • Breakdown: user property "role"
|
|
117
|
+
* • Expected: Doctors (high volume users) show ~4.2 avg vs ~3.5 baseline
|
|
118
|
+
*
|
|
119
|
+
* REAL-WORLD ANALOGUE: Experienced providers develop better bedside
|
|
120
|
+
* manner and patient communication skills over time.
|
|
121
|
+
*
|
|
122
|
+
* ───────────────────────────────────────────────────────────────
|
|
123
|
+
* 4. VIDEO CONSULTATION FOLLOW-UP LIFT (everything hook)
|
|
124
|
+
* ───────────────────────────────────────────────────────────────
|
|
125
|
+
*
|
|
126
|
+
* PATTERN: Patients whose consultations used video mode (vs phone)
|
|
127
|
+
* get 2x more follow-up appointment events injected. Cloned from
|
|
128
|
+
* existing "follow up scheduled" events.
|
|
129
|
+
*
|
|
130
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
131
|
+
*
|
|
132
|
+
* Report 1: Follow-Up Rate by Consultation Mode
|
|
133
|
+
* • Report type: Insights
|
|
134
|
+
* • Event: "follow up scheduled"
|
|
135
|
+
* • Measure: Total per user
|
|
136
|
+
* • Breakdown: "consultation_mode" (from consultation completed)
|
|
137
|
+
* • Expected: video users ~2x more follow-ups than phone users
|
|
138
|
+
*
|
|
139
|
+
* REAL-WORLD ANALOGUE: Face-to-face (video) consultations build
|
|
140
|
+
* stronger patient-doctor rapport, increasing follow-up compliance.
|
|
141
|
+
*
|
|
142
|
+
* ───────────────────────────────────────────────────────────────
|
|
143
|
+
* 5. CHRONIC CONDITION REFILL CHAIN (everything hook)
|
|
144
|
+
* ───────────────────────────────────────────────────────────────
|
|
145
|
+
*
|
|
146
|
+
* PATTERN: Patients with condition_type "chronic" on any prescription
|
|
147
|
+
* event get additional cloned prescription_refill events injected
|
|
148
|
+
* every ~30 days after the original. Creates periodic refill cadence.
|
|
149
|
+
*
|
|
150
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
151
|
+
*
|
|
152
|
+
* Report 1: Refill Volume by Condition
|
|
153
|
+
* • Report type: Insights
|
|
154
|
+
* • Event: "prescription refill"
|
|
155
|
+
* • Measure: Total
|
|
156
|
+
* • Breakdown: "condition_type"
|
|
157
|
+
* • Expected: "chronic" should have ~3-4x more refills than others
|
|
158
|
+
*
|
|
159
|
+
* REAL-WORLD ANALOGUE: Chronic conditions (diabetes, hypertension)
|
|
160
|
+
* require ongoing prescriptions creating predictable refill revenue.
|
|
161
|
+
*
|
|
162
|
+
* ───────────────────────────────────────────────────────────────
|
|
163
|
+
* 6. OCCASIONAL PATIENT NO-SHOWS (everything hook)
|
|
164
|
+
* ───────────────────────────────────────────────────────────────
|
|
165
|
+
*
|
|
166
|
+
* PATTERN: Users with role "patient" and low event count (<15 events)
|
|
167
|
+
* have 25% of their "appointment booked" events dropped. Simulates
|
|
168
|
+
* occasional patients who book but don't show up.
|
|
169
|
+
*
|
|
170
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
171
|
+
*
|
|
172
|
+
* Report 1: Appointment-to-Consultation Ratio
|
|
173
|
+
* • Report type: Funnels
|
|
174
|
+
* • Steps: "appointment booked" → "consultation completed"
|
|
175
|
+
* • Breakdown: user property "role"
|
|
176
|
+
* • Expected: patients with fewer events convert ~75% vs ~95% for active
|
|
177
|
+
*
|
|
178
|
+
* REAL-WORLD ANALOGUE: Infrequent patients have higher no-show rates,
|
|
179
|
+
* a major operational cost for healthcare providers.
|
|
180
|
+
*
|
|
181
|
+
* ───────────────────────────────────────────────────────────────
|
|
182
|
+
* 7. DOCTOR PROFILE SPECIALIZATION (user hook)
|
|
183
|
+
* ───────────────────────────────────────────────────────────────
|
|
184
|
+
*
|
|
185
|
+
* PATTERN: Users with role "doctor" get specialty set to a specific
|
|
186
|
+
* value (from the existing array) and years_experience boosted to
|
|
187
|
+
* senior range (15-30). Nurses get years_experience in mid range.
|
|
188
|
+
*
|
|
189
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
190
|
+
*
|
|
191
|
+
* Report 1: Experience Distribution by Role
|
|
192
|
+
* • Report type: Insights
|
|
193
|
+
* • Event: "consultation completed"
|
|
194
|
+
* • Measure: Average of user property "years_experience"
|
|
195
|
+
* • Breakdown: user property "role"
|
|
196
|
+
* • Expected: doctors ≈ 22 years, nurses ≈ 8, patients ≈ 0
|
|
197
|
+
*
|
|
198
|
+
* REAL-WORLD ANALOGUE: Provider profiles have specialized expertise
|
|
199
|
+
* and experience levels that affect patient matching.
|
|
200
|
+
*
|
|
201
|
+
* ───────────────────────────────────────────────────────────────
|
|
202
|
+
* 8. PREMIUM BOOKING FUNNEL LIFT (funnel-pre hook)
|
|
203
|
+
* ───────────────────────────────────────────────────────────────
|
|
204
|
+
*
|
|
205
|
+
* PATTERN: Users with subscription_tier "premium" get 1.4x conversion
|
|
206
|
+
* rate on the booking funnel. Premium patients have priority scheduling.
|
|
207
|
+
*
|
|
208
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
209
|
+
*
|
|
210
|
+
* Report 1: Booking Conversion by Tier
|
|
211
|
+
* • Report type: Funnels
|
|
212
|
+
* • Steps: "symptom search" → "appointment booked" → "consultation completed"
|
|
213
|
+
* • Breakdown: "subscription_tier" (superProp)
|
|
214
|
+
* • Expected: premium ≈ 56% vs free ≈ 40% conversion
|
|
215
|
+
*
|
|
216
|
+
* REAL-WORLD ANALOGUE: Premium telehealth subscribers get priority
|
|
217
|
+
* scheduling and shorter wait times, improving conversion.
|
|
218
|
+
*
|
|
219
|
+
* ═══════════════════════════════════════════════════════════════
|
|
220
|
+
* EXPECTED METRICS SUMMARY
|
|
221
|
+
* ═══════════════════════════════════════════════════════════════
|
|
222
|
+
*
|
|
223
|
+
* Hook | Metric | Baseline | Effect | Ratio
|
|
224
|
+
* ────────────────────────────|─────────────────────|──────────|─────────|──────
|
|
225
|
+
* After-Hours Pricing | consultation_fee | $75 | $112 | 1.5x
|
|
226
|
+
* Flu Season Spike | respiratory appts | ~15% | ~60% | 4x
|
|
227
|
+
* Experienced Doctor Sat. | satisfaction_score | 3.5 | 4.2 | 1.2x
|
|
228
|
+
* Video Follow-Up Lift | follow-ups/user | 1.0 | 2.0 | 2x
|
|
229
|
+
* Chronic Refill Chain | refills (chronic) | 1 | 3-4 | 3-4x
|
|
230
|
+
* Occasional No-Shows | booking→consult | 95% | 75% | 0.79x
|
|
231
|
+
* Doctor Specialization | years_experience | 5 | 22 | 4.4x
|
|
232
|
+
* Premium Booking Lift | funnel conversion | 40% | 56% | 1.4x
|
|
233
|
+
*/
|
|
234
|
+
|
|
235
|
+
/** @type {Config} */
|
|
236
|
+
const config = {
|
|
237
|
+
token: "",
|
|
238
|
+
seed: SEED,
|
|
239
|
+
numDays: days,
|
|
240
|
+
numEvents: num_users * 120,
|
|
241
|
+
numUsers: num_users,
|
|
242
|
+
hasAnonIds: false,
|
|
243
|
+
hasSessionIds: true,
|
|
244
|
+
format: "json",
|
|
245
|
+
gzip: true,
|
|
246
|
+
alsoInferFunnels: false,
|
|
247
|
+
hasLocation: true,
|
|
248
|
+
hasAndroidDevices: true,
|
|
249
|
+
hasIOSDevices: true,
|
|
250
|
+
hasDesktopDevices: true,
|
|
251
|
+
hasBrowser: false,
|
|
252
|
+
hasCampaigns: false,
|
|
253
|
+
isAnonymous: false,
|
|
254
|
+
hasAdSpend: false,
|
|
255
|
+
percentUsersBornInDataset: 35,
|
|
256
|
+
hasAvatar: true,
|
|
257
|
+
concurrency: 1,
|
|
258
|
+
writeToDisk: false,
|
|
259
|
+
scdProps: {
|
|
260
|
+
care_plan: {
|
|
261
|
+
values: ["preventive", "routine", "chronic", "acute"],
|
|
262
|
+
frequency: "month",
|
|
263
|
+
timing: "fuzzy",
|
|
264
|
+
max: 8
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
mirrorProps: {},
|
|
268
|
+
lookupTables: [],
|
|
269
|
+
|
|
270
|
+
// ── Events (18) ──────────────────────────────────────────
|
|
271
|
+
events: [
|
|
272
|
+
{
|
|
273
|
+
event: "account created",
|
|
274
|
+
weight: 1,
|
|
275
|
+
isFirstEvent: true,
|
|
276
|
+
properties: {
|
|
277
|
+
referral_source: ["organic", "doctor_referral", "insurance_partner", "social_media", "search"],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
event: "symptom search",
|
|
282
|
+
weight: 7,
|
|
283
|
+
properties: {
|
|
284
|
+
search_term: ["headache", "fever", "cough", "back pain", "fatigue", "anxiety", "rash", "nausea", "chest pain", "joint pain"],
|
|
285
|
+
results_count: u.weighNumRange(0, 25, 0.5),
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
event: "appointment booked",
|
|
290
|
+
weight: 6,
|
|
291
|
+
properties: {
|
|
292
|
+
doctor_id: chance.pickone.bind(chance, doctorIds),
|
|
293
|
+
clinic_id: chance.pickone.bind(chance, clinicIds),
|
|
294
|
+
condition_type: ["general", "general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
|
|
295
|
+
wait_time_hours: u.weighNumRange(1, 72, 0.4),
|
|
296
|
+
appointment_type: ["new_patient", "follow_up", "follow_up", "urgent", "routine", "routine"],
|
|
297
|
+
after_hours: [false],
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
event: "consultation completed",
|
|
302
|
+
weight: 5,
|
|
303
|
+
properties: {
|
|
304
|
+
doctor_id: chance.pickone.bind(chance, doctorIds),
|
|
305
|
+
consultation_mode: ["phone"], // default; feature rollout changes to video
|
|
306
|
+
duration_minutes: u.weighNumRange(5, 60, 0.6, 15),
|
|
307
|
+
consultation_fee: u.weighNumRange(25, 200, 0.4, 75),
|
|
308
|
+
satisfaction_score: u.weighNumRange(1, 5, 0.8, 3),
|
|
309
|
+
condition_type: ["general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
|
|
310
|
+
after_hours: [false],
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
event: "prescription issued",
|
|
315
|
+
weight: 4,
|
|
316
|
+
properties: {
|
|
317
|
+
medication_type: ["antibiotic", "antiviral", "painkiller", "anti_inflammatory", "antidepressant", "inhaler", "topical", "chronic_maintenance"],
|
|
318
|
+
quantity: u.weighNumRange(1, 90, 0.3, 30),
|
|
319
|
+
condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "chronic", "pediatric"],
|
|
320
|
+
refill_count: u.weighNumRange(0, 3),
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
event: "prescription refill",
|
|
325
|
+
weight: 3,
|
|
326
|
+
properties: {
|
|
327
|
+
medication_type: ["antibiotic", "antiviral", "painkiller", "anti_inflammatory", "antidepressant", "inhaler", "topical", "chronic_maintenance"],
|
|
328
|
+
quantity: u.weighNumRange(1, 90, 0.3, 30),
|
|
329
|
+
condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "chronic", "pediatric"],
|
|
330
|
+
refill_count: u.weighNumRange(1, 6),
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
event: "follow up scheduled",
|
|
335
|
+
weight: 3,
|
|
336
|
+
properties: {
|
|
337
|
+
doctor_id: chance.pickone.bind(chance, doctorIds),
|
|
338
|
+
days_until_followup: u.weighNumRange(3, 30, 0.5, 7),
|
|
339
|
+
condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
|
|
340
|
+
consultation_mode: ["phone"],
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
event: "message sent",
|
|
345
|
+
weight: 5,
|
|
346
|
+
properties: {
|
|
347
|
+
message_type: ["question", "question", "update", "result_inquiry", "prescription_question", "scheduling"],
|
|
348
|
+
recipient_role: ["doctor", "doctor", "nurse", "support"],
|
|
349
|
+
response_time_hours: u.weighNumRange(0.1, 48, 0.3, 4),
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
event: "lab results viewed",
|
|
354
|
+
weight: 3,
|
|
355
|
+
properties: {
|
|
356
|
+
test_type: ["blood_panel", "urinalysis", "imaging", "allergy_test", "metabolic_panel", "thyroid"],
|
|
357
|
+
result_status: ["normal", "normal", "normal", "abnormal", "pending"],
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
event: "health record accessed",
|
|
362
|
+
weight: 4,
|
|
363
|
+
properties: {
|
|
364
|
+
record_type: ["visit_summary", "lab_results", "prescriptions", "immunizations", "billing"],
|
|
365
|
+
access_method: ["app", "app", "web_portal"],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
event: "insurance verified",
|
|
370
|
+
weight: 2,
|
|
371
|
+
properties: {
|
|
372
|
+
insurance_type: ["private", "private", "employer", "medicare", "medicaid", "self_pay"],
|
|
373
|
+
verification_status: ["approved", "approved", "approved", "pending", "denied"],
|
|
374
|
+
copay_amount: u.weighNumRange(0, 75, 0.5, 20),
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
event: "payment processed",
|
|
379
|
+
weight: 3,
|
|
380
|
+
properties: {
|
|
381
|
+
amount: u.weighNumRange(10, 500, 0.3, 75),
|
|
382
|
+
payment_method: ["credit_card", "credit_card", "insurance_claim", "hsa_fsa", "debit"],
|
|
383
|
+
payment_status: ["success", "success", "success", "success", "failed"],
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
event: "notification received",
|
|
388
|
+
weight: 6,
|
|
389
|
+
properties: {
|
|
390
|
+
notification_type: ["appointment_reminder", "appointment_reminder", "lab_ready", "prescription_ready", "message_received", "billing"],
|
|
391
|
+
channel: ["push", "push", "email", "sms"],
|
|
392
|
+
opened: [true, true, true, false],
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
event: "provider rated",
|
|
397
|
+
weight: 2,
|
|
398
|
+
properties: {
|
|
399
|
+
doctor_id: chance.pickone.bind(chance, doctorIds),
|
|
400
|
+
rating: u.weighNumRange(1, 5, 0.7, 4),
|
|
401
|
+
would_recommend: [true, true, true, true, false],
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
event: "support ticket created",
|
|
406
|
+
weight: 1,
|
|
407
|
+
properties: {
|
|
408
|
+
category: ["billing", "technical", "scheduling", "prescription", "insurance", "other"],
|
|
409
|
+
priority: ["low", "low", "medium", "medium", "high"],
|
|
410
|
+
resolution_hours: u.weighNumRange(1, 96, 0.4, 24),
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
event: "profile updated",
|
|
415
|
+
weight: 2,
|
|
416
|
+
properties: {
|
|
417
|
+
field_updated: ["insurance", "address", "phone", "emergency_contact", "allergies", "medications"],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
event: "app session",
|
|
422
|
+
weight: 8,
|
|
423
|
+
properties: {
|
|
424
|
+
session_duration_sec: u.weighNumRange(10, 1800, 0.4, 120),
|
|
425
|
+
pages_viewed: u.weighNumRange(1, 15, 0.5, 3),
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
event: "account deactivated",
|
|
430
|
+
weight: 1,
|
|
431
|
+
isChurnEvent: true,
|
|
432
|
+
returnLikelihood: 0.15,
|
|
433
|
+
isStrictEvent: true,
|
|
434
|
+
properties: {
|
|
435
|
+
reason: ["switched_provider", "cost", "no_longer_needed", "poor_experience", "insurance_change"],
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
|
|
440
|
+
// ── Funnels (5) ──────────────────────────────────────────
|
|
441
|
+
funnels: [
|
|
442
|
+
{
|
|
443
|
+
name: "Onboarding Flow",
|
|
444
|
+
sequence: ["account created", "insurance verified", "symptom search", "appointment booked"],
|
|
445
|
+
conversionRate: 45,
|
|
446
|
+
order: "sequential",
|
|
447
|
+
isFirstFunnel: true,
|
|
448
|
+
timeToConvert: 72,
|
|
449
|
+
weight: 3,
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: "Booking to Consultation",
|
|
453
|
+
sequence: ["symptom search", "appointment booked", "consultation completed"],
|
|
454
|
+
conversionRate: 40,
|
|
455
|
+
order: "sequential",
|
|
456
|
+
timeToConvert: 48,
|
|
457
|
+
weight: 5,
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: "Full Care Journey",
|
|
461
|
+
sequence: ["appointment booked", "consultation completed", "prescription issued", "follow up scheduled"],
|
|
462
|
+
conversionRate: 30,
|
|
463
|
+
order: "sequential",
|
|
464
|
+
timeToConvert: 168,
|
|
465
|
+
weight: 3,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: "Prescription Lifecycle",
|
|
469
|
+
sequence: ["prescription issued", "prescription refill", "payment processed"],
|
|
470
|
+
conversionRate: 55,
|
|
471
|
+
order: "sequential",
|
|
472
|
+
timeToConvert: 720,
|
|
473
|
+
weight: 2,
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "Patient Satisfaction",
|
|
477
|
+
sequence: ["consultation completed", "provider rated", "follow up scheduled"],
|
|
478
|
+
conversionRate: 25,
|
|
479
|
+
order: "sequential",
|
|
480
|
+
timeToConvert: 72,
|
|
481
|
+
weight: 2,
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
|
|
485
|
+
// ── SuperProps ──────────────────────────────────────────
|
|
486
|
+
superProps: {
|
|
487
|
+
subscription_tier: ["free", "free", "free", "basic", "basic", "premium"],
|
|
488
|
+
platform: ["ios", "android", "web"],
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
// ── UserProps ──────────────────────────────────────────
|
|
492
|
+
userProps: {
|
|
493
|
+
role: ["patient", "patient", "patient", "patient", "patient", "patient", "patient", "patient", "nurse", "doctor"],
|
|
494
|
+
specialty: ["none"],
|
|
495
|
+
years_experience: u.weighNumRange(0, 5, 0.5),
|
|
496
|
+
preferred_language: ["en", "en", "en", "en", "es", "pt", "de", "fr"],
|
|
497
|
+
has_chronic_condition: [false, false, false, true],
|
|
498
|
+
age_range: ["18-25", "26-35", "26-35", "36-45", "36-45", "46-55", "56-65", "65+"],
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
// ── Personas ──────────────────────────────────
|
|
502
|
+
personas: [
|
|
503
|
+
{
|
|
504
|
+
name: "doctor",
|
|
505
|
+
weight: 5,
|
|
506
|
+
eventMultiplier: 5.0,
|
|
507
|
+
conversionModifier: 1.8,
|
|
508
|
+
churnRate: 0.01,
|
|
509
|
+
properties: {
|
|
510
|
+
role: "doctor",
|
|
511
|
+
segment: "provider",
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: "nurse",
|
|
516
|
+
weight: 10,
|
|
517
|
+
eventMultiplier: 3.0,
|
|
518
|
+
conversionModifier: 1.5,
|
|
519
|
+
churnRate: 0.03,
|
|
520
|
+
properties: {
|
|
521
|
+
role: "nurse",
|
|
522
|
+
segment: "provider",
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: "patient_active",
|
|
527
|
+
weight: 40,
|
|
528
|
+
eventMultiplier: 1.0,
|
|
529
|
+
conversionModifier: 1.0,
|
|
530
|
+
churnRate: 0.05,
|
|
531
|
+
properties: {
|
|
532
|
+
role: "patient",
|
|
533
|
+
segment: "active_patient",
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: "patient_occasional",
|
|
538
|
+
weight: 30,
|
|
539
|
+
eventMultiplier: 0.5,
|
|
540
|
+
conversionModifier: 0.7,
|
|
541
|
+
churnRate: 0.12,
|
|
542
|
+
properties: {
|
|
543
|
+
role: "patient",
|
|
544
|
+
segment: "occasional_patient",
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
name: "patient_churner",
|
|
549
|
+
weight: 15,
|
|
550
|
+
eventMultiplier: 0.3,
|
|
551
|
+
conversionModifier: 0.3,
|
|
552
|
+
churnRate: 0.4,
|
|
553
|
+
properties: {
|
|
554
|
+
role: "patient",
|
|
555
|
+
segment: "churner",
|
|
556
|
+
},
|
|
557
|
+
activeWindow: { maxDays: 21 },
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
|
|
561
|
+
// ── Subscription ──────────────────────────────
|
|
562
|
+
subscription: {
|
|
563
|
+
plans: [
|
|
564
|
+
{ name: "free", price: 0, default: true },
|
|
565
|
+
{ name: "basic", price: 9.99, trialDays: 14 },
|
|
566
|
+
{ name: "premium", price: 29.99 },
|
|
567
|
+
],
|
|
568
|
+
lifecycle: {
|
|
569
|
+
trialToPayRate: 0.30,
|
|
570
|
+
upgradeRate: 0.08,
|
|
571
|
+
downgradeRate: 0.03,
|
|
572
|
+
churnRate: 0.05,
|
|
573
|
+
winBackRate: 0.10,
|
|
574
|
+
winBackDelay: 21,
|
|
575
|
+
paymentFailureRate: 0.02,
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
// ── Geo ──────────────────────────────────────
|
|
580
|
+
geo: {
|
|
581
|
+
sticky: true,
|
|
582
|
+
regions: [
|
|
583
|
+
{
|
|
584
|
+
name: "us",
|
|
585
|
+
countries: ["US"],
|
|
586
|
+
weight: 50,
|
|
587
|
+
timezoneOffset: -5,
|
|
588
|
+
properties: { currency: "USD", locale: "en-US" },
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: "eu",
|
|
592
|
+
countries: ["GB", "DE", "FR"],
|
|
593
|
+
weight: 30,
|
|
594
|
+
timezoneOffset: 1,
|
|
595
|
+
properties: { currency: "EUR", locale: "en-EU", gdpr_consent: true },
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: "latam",
|
|
599
|
+
countries: ["BR", "MX", "AR"],
|
|
600
|
+
weight: 20,
|
|
601
|
+
timezoneOffset: -3,
|
|
602
|
+
properties: { currency: "BRL", locale: "pt-BR" },
|
|
603
|
+
},
|
|
604
|
+
],
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
// ── Features ──────────────────────────────────
|
|
608
|
+
features: [
|
|
609
|
+
{
|
|
610
|
+
name: "video_consultation",
|
|
611
|
+
launchDay: 30,
|
|
612
|
+
adoptionCurve: "fast",
|
|
613
|
+
property: "consultation_mode",
|
|
614
|
+
values: ["phone", "video"],
|
|
615
|
+
defaultBefore: "phone",
|
|
616
|
+
affectsEvents: ["consultation completed", "follow up scheduled"],
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "ai_symptom_checker",
|
|
620
|
+
launchDay: 60,
|
|
621
|
+
adoptionCurve: { k: 0.08, midpoint: 25 },
|
|
622
|
+
property: "symptom_source",
|
|
623
|
+
values: ["manual", "ai_assisted"],
|
|
624
|
+
defaultBefore: "manual",
|
|
625
|
+
affectsEvents: ["symptom search"],
|
|
626
|
+
conversionLift: 1.12,
|
|
627
|
+
},
|
|
628
|
+
],
|
|
629
|
+
|
|
630
|
+
// ── Hook Function ──────────────────────────────────────
|
|
631
|
+
hook: function (record, type, meta) {
|
|
632
|
+
// ── HOOK 7: DOCTOR PROFILE SPECIALIZATION (user) ─────
|
|
633
|
+
// Doctors get a real specialty and senior years_experience.
|
|
634
|
+
// Nurses get mid-range experience. Patients stay at defaults.
|
|
635
|
+
if (type === "user") {
|
|
636
|
+
if (record.role === "doctor") {
|
|
637
|
+
record.specialty = chance.pickone(["cardiology", "dermatology", "pediatrics", "psychiatry", "general_practice", "pulmonology", "endocrinology"]);
|
|
638
|
+
record.years_experience = chance.integer({ min: 15, max: 30 });
|
|
639
|
+
} else if (record.role === "nurse") {
|
|
640
|
+
record.specialty = chance.pickone(["general_practice", "pediatrics", "emergency"]);
|
|
641
|
+
record.years_experience = chance.integer({ min: 3, max: 15 });
|
|
642
|
+
} else {
|
|
643
|
+
record.years_experience = 0;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ── HOOK 8: PREMIUM BOOKING FUNNEL LIFT (funnel-pre) ─
|
|
648
|
+
// Premium subscribers convert 1.4x better through booking funnel.
|
|
649
|
+
if (type === "funnel-pre") {
|
|
650
|
+
if (meta && meta.profile) {
|
|
651
|
+
const tier = meta.profile.subscription_tier;
|
|
652
|
+
if (tier === "premium") {
|
|
653
|
+
record.conversionRate = Math.min(record.conversionRate * 1.4, 95);
|
|
654
|
+
} else if (tier === "basic") {
|
|
655
|
+
record.conversionRate = Math.min(record.conversionRate * 1.15, 90);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ── HOOK 1: AFTER-HOURS SURGE PRICING (event) ────────
|
|
661
|
+
// Consultations between 7PM-7AM get 1.5x fee.
|
|
662
|
+
if (type === "event") {
|
|
663
|
+
if (record.event === "consultation completed" || record.event === "appointment booked") {
|
|
664
|
+
const hour = dayjs(record.time).hour();
|
|
665
|
+
if (hour >= 19 || hour < 7) {
|
|
666
|
+
record.after_hours = true;
|
|
667
|
+
if (record.consultation_fee) {
|
|
668
|
+
record.consultation_fee = Math.floor(record.consultation_fee * 1.5);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ── HOOK 2: FLU SEASON SPIKE (event) ─────────────
|
|
674
|
+
// Days 50-70: respiratory conditions dominate, wait times double.
|
|
675
|
+
const FLU_START = DATASET_START.add(50, "days");
|
|
676
|
+
const FLU_END = DATASET_START.add(70, "days");
|
|
677
|
+
const eventTime = dayjs(record.time);
|
|
678
|
+
if (record.event === "appointment booked" && eventTime.isAfter(FLU_START) && eventTime.isBefore(FLU_END)) {
|
|
679
|
+
if (chance.bool({ likelihood: 60 })) {
|
|
680
|
+
record.condition_type = "respiratory";
|
|
681
|
+
}
|
|
682
|
+
if (record.condition_type === "respiratory") {
|
|
683
|
+
record.wait_time_hours = Math.floor((record.wait_time_hours || 12) * 2);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ── EVERYTHING HOOKS ─────────────────────────────────
|
|
689
|
+
if (type === "everything") {
|
|
690
|
+
const events = record;
|
|
691
|
+
if (!events.length) return record;
|
|
692
|
+
|
|
693
|
+
// ── HOOK 3: EXPERIENCED DOCTOR SATISFACTION ──────
|
|
694
|
+
// Users with >50 consultation events get boosted satisfaction scores.
|
|
695
|
+
let consultCount = 0;
|
|
696
|
+
events.forEach(e => {
|
|
697
|
+
if (e.event === "consultation completed") consultCount++;
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
if (consultCount > 50) {
|
|
701
|
+
events.forEach(e => {
|
|
702
|
+
if (e.event === "consultation completed") {
|
|
703
|
+
e.satisfaction_score = chance.floating({ min: 4.0, max: 5.0, fixed: 1 });
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ── HOOK 4: VIDEO CONSULTATION FOLLOW-UP LIFT ────
|
|
709
|
+
// Patients with video consultations get 2x follow-up events.
|
|
710
|
+
const hasVideoConsult = events.some(e =>
|
|
711
|
+
e.event === "consultation completed" && e.consultation_mode === "video"
|
|
712
|
+
);
|
|
713
|
+
if (hasVideoConsult) {
|
|
714
|
+
const templateFollowUp = events.find(e => e.event === "follow up scheduled");
|
|
715
|
+
if (templateFollowUp) {
|
|
716
|
+
const videoConsults = events.filter(e =>
|
|
717
|
+
e.event === "consultation completed" && e.consultation_mode === "video"
|
|
718
|
+
);
|
|
719
|
+
videoConsults.forEach(vc => {
|
|
720
|
+
if (chance.bool({ likelihood: 60 })) {
|
|
721
|
+
events.push({
|
|
722
|
+
...templateFollowUp,
|
|
723
|
+
time: dayjs(vc.time).add(chance.integer({ min: 1, max: 7 }), "days").toISOString(),
|
|
724
|
+
user_id: vc.user_id,
|
|
725
|
+
consultation_mode: "video",
|
|
726
|
+
days_until_followup: chance.integer({ min: 3, max: 14 }),
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ── HOOK 5: CHRONIC CONDITION REFILL CHAIN ───────
|
|
734
|
+
// Patients with chronic prescriptions get refills every ~30 days.
|
|
735
|
+
const chronicRxs = events.filter(e =>
|
|
736
|
+
e.event === "prescription issued" && e.condition_type === "chronic"
|
|
737
|
+
);
|
|
738
|
+
if (chronicRxs.length > 0) {
|
|
739
|
+
const templateRefill = events.find(e => e.event === "prescription refill");
|
|
740
|
+
if (templateRefill) {
|
|
741
|
+
chronicRxs.forEach(rx => {
|
|
742
|
+
const rxTime = dayjs(rx.time);
|
|
743
|
+
const refillsToAdd = chance.integer({ min: 2, max: 4 });
|
|
744
|
+
for (let i = 1; i <= refillsToAdd; i++) {
|
|
745
|
+
events.push({
|
|
746
|
+
...templateRefill,
|
|
747
|
+
time: rxTime.add(30 * i + chance.integer({ min: -3, max: 3 }), "days").toISOString(),
|
|
748
|
+
user_id: rx.user_id,
|
|
749
|
+
condition_type: "chronic",
|
|
750
|
+
medication_type: "chronic_maintenance",
|
|
751
|
+
refill_count: i,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ── HOOK 6: OCCASIONAL PATIENT NO-SHOWS ──────────
|
|
759
|
+
// Low-activity patients (< 15 events) lose 25% of appointments.
|
|
760
|
+
if (events.length < 15) {
|
|
761
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
762
|
+
if (events[i].event === "appointment booked" && chance.bool({ likelihood: 25 })) {
|
|
763
|
+
events.splice(i, 1);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return record;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return record;
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
export default config;
|