huly-mcp-sdk 0.5.0 → 0.5.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "huly-mcp-sdk",
3
- "version": "0.5.0",
4
- "description": "MCP server for Huly connect Claude Desktop to your Huly workspace via the native WebSocket SDK",
3
+ "version": "0.5.1",
4
+ "description": "MCP server for Huly \u2014 connect Claude Desktop to your Huly workspace via the native WebSocket SDK",
5
5
  "keywords": [
6
6
  "huly",
7
7
  "mcp",
@@ -1,70 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Delete the ASCII art comments from the 4 epic issues (MCARE-4..7)
4
- * Run: node --env-file=.env scripts/clean-comments.mjs
5
- */
6
- import accountClientPkg from '@hcengineering/account-client'
7
- import corePkg from '@hcengineering/core'
8
- import serverClientPkg from '@hcengineering/server-client'
9
- import trackerPkg from '@hcengineering/tracker'
10
- import chunterPkg from '@hcengineering/chunter'
11
-
12
- const { getClient: getRawAccountClient } = accountClientPkg
13
- const { TxOperations } = corePkg
14
- const { createClient } = serverClientPkg
15
- const tracker = trackerPkg.default ?? trackerPkg
16
- const chunter = chunterPkg.default ?? chunterPkg
17
-
18
- const ACCOUNTS_URL = process.env.HULY_ACCOUNTS_URL ?? 'https://account.huly.app'
19
-
20
- async function connect () {
21
- const workspaceUrl = process.env.HULY_WORKSPACE
22
- const hulyToken = process.env.HULY_TOKEN
23
- if (!workspaceUrl || !hulyToken) throw new Error('HULY_WORKSPACE and HULY_TOKEN required')
24
-
25
- const authedClient = getRawAccountClient(ACCOUNTS_URL, hulyToken)
26
- const info = await authedClient.getLoginInfoByToken()
27
- if (!info) throw new Error('Token invalid')
28
-
29
- let socialId, endpoint, wsToken
30
-
31
- if ('endpoint' in info && info.endpoint) {
32
- socialId = info.socialId; endpoint = info.endpoint; wsToken = info.token
33
- } else {
34
- socialId = info.socialId
35
- const wsInfo = await authedClient.selectWorkspace(workspaceUrl, 'external')
36
- endpoint = wsInfo.endpoint; wsToken = wsInfo.token
37
- }
38
-
39
- const rawConnection = await createClient(endpoint, wsToken)
40
- const txClient = new TxOperations(rawConnection, socialId)
41
- return { txClient, rawConnection }
42
- }
43
-
44
- const { txClient, rawConnection } = await connect()
45
- console.log('Connected.')
46
-
47
- const EPICS = ['MCARE-4', 'MCARE-5', 'MCARE-6', 'MCARE-7']
48
-
49
- for (const identifier of EPICS) {
50
- const issue = await txClient.findOne(tracker.class.Issue, { identifier })
51
- if (!issue) { console.log(`${identifier}: NOT FOUND`); continue }
52
-
53
- const comments = await txClient.findAll(chunter.class.ChatMessage, { attachedTo: issue._id })
54
- if (comments.length === 0) { console.log(`${identifier}: no comments`); continue }
55
-
56
- for (const comment of comments) {
57
- await txClient.removeCollection(
58
- chunter.class.ChatMessage,
59
- issue.space,
60
- comment._id,
61
- issue._id,
62
- tracker.class.Issue,
63
- 'comments'
64
- )
65
- console.log(`${identifier}: deleted comment ${comment._id}`)
66
- }
67
- }
68
-
69
- await rawConnection.close()
70
- console.log('Done.')
@@ -1,73 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Link EP1-EP4 documents to their corresponding epic issues via relations
4
- * Run: node --env-file=.env scripts/link-docs-to-issues.mjs
5
- */
6
- import accountClientPkg from '@hcengineering/account-client'
7
- import corePkg from '@hcengineering/core'
8
- import serverClientPkg from '@hcengineering/server-client'
9
- import trackerPkg from '@hcengineering/tracker'
10
- import documentPkg from '@hcengineering/document'
11
-
12
- const { getClient: getRawAccountClient } = accountClientPkg
13
- const { TxOperations } = corePkg
14
- const { createClient } = serverClientPkg
15
- const tracker = trackerPkg.default ?? trackerPkg
16
- const document = documentPkg.default ?? documentPkg
17
-
18
- const ACCOUNTS_URL = process.env.HULY_ACCOUNTS_URL ?? 'https://account.huly.app'
19
-
20
- async function connect () {
21
- const workspaceUrl = process.env.HULY_WORKSPACE
22
- const hulyToken = process.env.HULY_TOKEN
23
- if (!workspaceUrl || !hulyToken) throw new Error('HULY_WORKSPACE and HULY_TOKEN required')
24
-
25
- const authedClient = getRawAccountClient(ACCOUNTS_URL, hulyToken)
26
- const info = await authedClient.getLoginInfoByToken()
27
- if (!info) throw new Error('Token invalid')
28
-
29
- let socialId, endpoint, wsToken
30
- if ('endpoint' in info && info.endpoint) {
31
- socialId = info.socialId; endpoint = info.endpoint; wsToken = info.token
32
- } else {
33
- socialId = info.socialId
34
- const wsInfo = await authedClient.selectWorkspace(workspaceUrl, 'external')
35
- endpoint = wsInfo.endpoint; wsToken = wsInfo.token
36
- }
37
-
38
- const rawConnection = await createClient(endpoint, wsToken)
39
- const txClient = new TxOperations(rawConnection, socialId)
40
- return { txClient, rawConnection }
41
- }
42
-
43
- const LINKS = [
44
- { issue: 'MCARE-4', docId: '69b9a635cf60316d6ce21a50' }, // EP1
45
- { issue: 'MCARE-5', docId: '69b9a63bcf60316d6ce21a52' }, // EP2
46
- { issue: 'MCARE-6', docId: '69b9a640cf60316d6ce21a54' }, // EP3
47
- { issue: 'MCARE-7', docId: '69b9a645cf60316d6ce21a56' }, // EP4
48
- ]
49
-
50
- const { txClient, rawConnection } = await connect()
51
- console.log('Connected.')
52
-
53
- for (const { issue: identifier, docId } of LINKS) {
54
- const issue = await txClient.findOne(tracker.class.Issue, { identifier })
55
- if (!issue) { console.log(`${identifier}: NOT FOUND`); continue }
56
-
57
- const doc = await txClient.findOne(document.class.Document, { _id: docId })
58
- if (!doc) { console.log(`${identifier}: document ${docId} NOT FOUND`); continue }
59
-
60
- // Build updated relations array (keep existing + add the document link)
61
- const existing = issue.relations ?? []
62
- const alreadyLinked = existing.some(r => r._id === docId)
63
- if (alreadyLinked) { console.log(`${identifier}: already linked`); continue }
64
-
65
- await txClient.updateDoc(tracker.class.Issue, issue.space, issue._id, {
66
- relations: [...existing, { _id: doc._id, _class: doc._class }]
67
- })
68
-
69
- console.log(`${identifier} ↔ "${doc.title}" ✅`)
70
- }
71
-
72
- await rawConnection.close()
73
- console.log('Done.')
@@ -1,505 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * One-off script: populate the 4 medics-care epic documents with Mermaid diagrams
4
- * Run: node --env-file=.env scripts/populate-docs.mjs
5
- */
6
- import accountClientPkg from '@hcengineering/account-client'
7
- import corePkg from '@hcengineering/core'
8
- import serverClientPkg from '@hcengineering/server-client'
9
- import documentPkg from '@hcengineering/document'
10
-
11
- const { getClient: getRawAccountClient } = accountClientPkg
12
- const { TxOperations } = corePkg
13
- const { createClient } = serverClientPkg
14
- const document = documentPkg.default ?? documentPkg
15
-
16
- const DATALAKE_URL = 'https://dl-eu.huly.app'
17
- const ACCOUNTS_URL = process.env.HULY_ACCOUNTS_URL ?? 'https://account.huly.app'
18
-
19
- // ── Connect ───────────────────────────────────────────────────────────────────
20
-
21
- async function connect () {
22
- const workspaceUrl = process.env.HULY_WORKSPACE
23
- const hulyToken = process.env.HULY_TOKEN
24
-
25
- if (!workspaceUrl || !hulyToken) throw new Error('HULY_WORKSPACE and HULY_TOKEN are required')
26
-
27
- const authedClient = getRawAccountClient(ACCOUNTS_URL, hulyToken)
28
- const info = await authedClient.getLoginInfoByToken()
29
- if (!info) throw new Error('HULY_TOKEN invalid or expired')
30
-
31
- let socialId, endpoint, wsToken, workspaceUuid
32
-
33
- if ('endpoint' in info && info.endpoint) {
34
- socialId = info.socialId
35
- endpoint = info.endpoint
36
- wsToken = info.token
37
- workspaceUuid = String(info.workspace ?? '')
38
- } else if ('token' in info && info.token) {
39
- socialId = info.socialId
40
- const wsInfo = await authedClient.selectWorkspace(workspaceUrl, 'external')
41
- if (!wsInfo.endpoint || !wsInfo.token) throw new Error(`Workspace '${workspaceUrl}' not accessible`)
42
- endpoint = wsInfo.endpoint
43
- wsToken = wsInfo.token
44
- workspaceUuid = String(wsInfo.workspace ?? '')
45
- } else {
46
- throw new Error('Unexpected token response')
47
- }
48
-
49
- const rawConnection = await createClient(endpoint, wsToken)
50
- const txClient = new TxOperations(rawConnection, socialId)
51
- return { txClient, rawConnection, wsToken, workspaceUuid }
52
- }
53
-
54
- // ── Upload blob ───────────────────────────────────────────────────────────────
55
-
56
- async function uploadContent (wsToken, workspaceUuid, docId, content) {
57
- const blobId = `${docId}-content-${Date.now()}`
58
- const form = new FormData()
59
- form.append('file', new Blob([content], { type: 'application/json' }), blobId)
60
-
61
- const res = await fetch(`${DATALAKE_URL}/upload/form-data/${workspaceUuid}`, {
62
- method: 'POST',
63
- headers: { Authorization: `Bearer ${wsToken}` },
64
- body: form
65
- })
66
-
67
- if (!res.ok) throw new Error(`Upload failed (${res.status}): ${await res.text()}`)
68
- const json = await res.json()
69
- return json[0]?.id ?? blobId
70
- }
71
-
72
- // ── Markdown → ProseMirror ────────────────────────────────────────────────────
73
-
74
- function parseInline (text) {
75
- const nodes = []
76
- const re = /\*\*(.+?)\*\*|`(.+?)`|([^`*]+)/g
77
- let m
78
- while ((m = re.exec(text)) !== null) {
79
- if (m[1] != null) nodes.push({ type: 'text', text: m[1], marks: [{ type: 'bold' }] })
80
- else if (m[2] != null) nodes.push({ type: 'text', text: m[2], marks: [{ type: 'code' }] })
81
- else if (m[3]?.length > 0) nodes.push({ type: 'text', text: m[3] })
82
- }
83
- return nodes.length > 0 ? nodes : [{ type: 'text', text }]
84
- }
85
-
86
- function buildTable (rows) {
87
- const [headerRow, ...bodyRows] = rows
88
- const tableRows = []
89
- if (headerRow) {
90
- tableRows.push({
91
- type: 'tableRow',
92
- content: headerRow.map(cell => ({
93
- type: 'tableHeader',
94
- content: [{ type: 'paragraph', content: parseInline(cell) }]
95
- }))
96
- })
97
- }
98
- for (const row of bodyRows) {
99
- tableRows.push({
100
- type: 'tableRow',
101
- content: row.map(cell => ({
102
- type: 'tableCell',
103
- content: [{ type: 'paragraph', content: parseInline(cell) }]
104
- }))
105
- })
106
- }
107
- return { type: 'table', content: tableRows }
108
- }
109
-
110
- function markdownToProseMirror (md) {
111
- const lines = md.split('\n')
112
- const nodes = []
113
- let i = 0
114
-
115
- while (i < lines.length) {
116
- const line = lines[i]
117
-
118
- // Fenced code block
119
- if (line.startsWith('```')) {
120
- const lang = line.slice(3).trim()
121
- const codeLines = []
122
- i++
123
- while (i < lines.length && !lines[i].startsWith('```')) {
124
- codeLines.push(lines[i])
125
- i++
126
- }
127
- i++
128
- // Use Huly's native 'mermaid' node type so diagrams render properly
129
- const nodeType = lang === 'mermaid' ? 'mermaid' : 'codeBlock'
130
- nodes.push({
131
- type: nodeType,
132
- attrs: { language: lang || null },
133
- content: [{ type: 'text', text: codeLines.join('\n') }]
134
- })
135
- continue
136
- }
137
-
138
- // Heading
139
- const hm = line.match(/^(#{1,6})\s+(.+)$/)
140
- if (hm) {
141
- nodes.push({ type: 'heading', attrs: { level: hm[1].length }, content: parseInline(hm[2]) })
142
- i++; continue
143
- }
144
-
145
- // Table
146
- if (line.includes('|') && line.trim().startsWith('|')) {
147
- const tableRows = []
148
- while (i < lines.length && lines[i].includes('|') && lines[i].trim().startsWith('|')) {
149
- const row = lines[i].trim().replace(/^\||\|$/g, '').split('|').map(c => c.trim())
150
- if (!row.every(c => /^[-:]+$/.test(c))) tableRows.push(row)
151
- i++
152
- }
153
- if (tableRows.length > 0) nodes.push(buildTable(tableRows))
154
- continue
155
- }
156
-
157
- // Bullet list
158
- if (/^(\s*[-*+])\s/.test(line)) {
159
- const items = []
160
- while (i < lines.length && /^(\s*[-*+])\s/.test(lines[i])) {
161
- const text = lines[i].replace(/^\s*[-*+]\s/, '')
162
- items.push({ type: 'listItem', content: [{ type: 'paragraph', content: parseInline(text) }] })
163
- i++
164
- }
165
- nodes.push({ type: 'bulletList', content: items })
166
- continue
167
- }
168
-
169
- if (line.trim() === '') { i++; continue }
170
-
171
- nodes.push({ type: 'paragraph', content: parseInline(line) })
172
- i++
173
- }
174
-
175
- if (nodes.length === 0) nodes.push({ type: 'paragraph', content: [] })
176
- return { type: 'doc', content: nodes }
177
- }
178
-
179
- // ── Document content ──────────────────────────────────────────────────────────
180
-
181
- const EP1_MD = `# EP1 — Service Ordering Flow
182
-
183
- ## Architecture Flow
184
-
185
- \`\`\`mermaid
186
- flowchart TD
187
- A([Patient Opens App]) --> B{What to order?}
188
-
189
- B --> C[Health Packages\\ne.g. Full Body, Cardiac]
190
- B --> D[Individual Lab Tests\\ne.g. CBC, HbA1c]
191
-
192
- C --> E[Browse Catalogue]
193
- D --> E
194
-
195
- E --> F[Select Service]
196
- F --> G[Select Patient\\nSelf or Family Member]
197
- G --> H{Home Collection\\nAvailable?}
198
-
199
- H -- Yes --> I[Choose: Home / Walk-in]
200
- H -- No --> J[Walk-in Only]
201
-
202
- I --> K[Pick Date & Slot]
203
- J --> K
204
-
205
- K --> L[Review Order]
206
- L --> M[Payment via Razorpay]
207
-
208
- M -- Success --> N[Order Confirmed ✅]
209
- M -- Failed --> O[Retry Payment ❌]
210
-
211
- N --> P[Sync to HIS]
212
- N --> Q[Push Notification Sent]
213
- N --> R[Order Tracking Active]
214
-
215
- R --> S[CONFIRMED]
216
- S --> T[SAMPLE COLLECTED]
217
- T --> U[PROCESSING]
218
- U --> V[REPORT READY 🎉]
219
- V --> W[View Report in App]
220
- \`\`\`
221
-
222
- ## Business Rules
223
-
224
- | Rule | Detail |
225
- |------|--------|
226
- | Service visibility | Only services flagged \`canBeOrderedFromApp: true\` in HIS |
227
- | Home collection | Checked per service + patient pin code |
228
- | Payment | Mandatory before order confirmation |
229
- | HIS sync | All orders synced back to HIS for lab/collection team |
230
- | Family orders | All linked family members eligible |
231
- `
232
-
233
- const EP2_MD = `# EP2 — Medicine Ordering & Refills
234
-
235
- ## Flow
236
-
237
- \`\`\`mermaid
238
- flowchart TD
239
- A([Entry Point]) --> B{Source}
240
-
241
- B -- From Visit --> C[Prescription Medicines]
242
- B -- Past Order --> D[Order History]
243
- B -- Refill Reminder --> E[Refill Screen]
244
-
245
- C --> F[Medicine List]
246
- D --> F
247
- E --> F
248
-
249
- F --> G{Medicine Type?}
250
-
251
- G -- OTC --> H[✅ Allow Order\\nNo Rx needed]
252
- G -- Rx-Required --> I{Valid Prescription\\nExists?}
253
-
254
- I -- Yes, Active --> J{Is it a Refill?}
255
- I -- No / Expired --> K[❌ BLOCKED\\nShow: Prescription required\\nOffer: Book Follow-up]
256
-
257
- J -- Within Validity --> L[✅ Allow Refill]
258
- J -- Expired --> M[❌ BLOCKED\\nShow: Prescription expired\\nOffer: Book Follow-up]
259
-
260
- H --> N[Select Patient]
261
- L --> N
262
-
263
- N --> O[Choose Delivery Address\\nSaved or New]
264
- O --> P[Review Cart]
265
- P --> Q[Payment]
266
-
267
- Q -- Success --> R[Order Confirmed ✅]
268
- Q -- Failed --> S[Retry ❌]
269
-
270
- R --> T[PENDING]
271
- T --> U[CONFIRMED]
272
- U --> V[DISPATCHED 🚚]
273
- V --> W[DELIVERED 🎉]
274
- \`\`\`
275
-
276
- ## Prescription Validation Matrix
277
-
278
- | Medicine Type | Valid Rx | Expired Rx | No Rx |
279
- |--------------|----------|------------|-------|
280
- | Rx-Required | ✅ Allow | ❌ Block + suggest follow-up | ❌ Block |
281
- | OTC | ✅ Allow | ✅ Allow | ✅ Allow |
282
- | Refill | ✅ Allow (within validity) | ❌ Block | ❌ Block |
283
-
284
- ## Business Rules
285
-
286
- - Prescription validity period set at time of issue (30 / 60 / 90 days)
287
- - Partial orders allowed: OTC items proceed even if Rx items are blocked
288
- - Expired prescription → prompt to book a follow-up appointment
289
- - All orders linked to patient + family member context
290
- `
291
-
292
- const EP3_MD = `# EP3 — Deep Linking Architecture
293
-
294
- ## Link Resolution Flow
295
-
296
- \`\`\`mermaid
297
- flowchart TD
298
- A([Link Received\\nSMS / WhatsApp / Email / Push]) --> B[Smart Link URL\\nhttps://novacare.medicsprime.in/link/...]
299
-
300
- B --> C{App Installed?}
301
-
302
- C -- Yes --> D[Open Native App\\niOS Universal Link\\nAndroid App Link]
303
- C -- No --> E[Open in Browser\\nWeb Fallback]
304
-
305
- D --> F[Native Deep Link Handler]
306
- F --> G{Authenticated?}
307
-
308
- G -- Yes --> L[Route to Screen]
309
- G -- No --> H[OTP Login\\nPre-fill phone if known]
310
- H --> L
311
-
312
- E --> I{Auto-Login Token\\nin URL?}
313
-
314
- I -- Valid Token --> J[Auto-Login\\nSingle-use, 24h expiry]
315
- I -- No / Expired --> K[OTP Login Page\\nPre-fill phone if known]
316
-
317
- J --> L
318
- K --> L
319
-
320
- L --> M{Link Type}
321
-
322
- M -- /book --> N[Appointment Booking\\npre-filled doctor/speciality]
323
- M -- /apt --> O[Appointment Detail]
324
- M -- /rx --> P[Prescription View]
325
- M -- /report --> Q[Lab Report View]
326
- M -- /package --> R[Health Package Order]
327
- M -- /refill --> S[Medicine Refill Flow]
328
- M -- /bill --> T[Bill & Payment]
329
- \`\`\`
330
-
331
- ## Link Format
332
-
333
- \`\`\`
334
- https://{hospital}.medicsprime.in/link/{type}?id={resourceId}&t={autoLoginToken}&ph={phoneHint}
335
- \`\`\`
336
-
337
- | Parameter | Description |
338
- |-----------|-------------|
339
- | \`hospital\` | novacare / sarji / cura / khushi |
340
- | \`type\` | apt / rx / report / package / refill / bill / book |
341
- | \`id\` | Resource ID (optional for /book) |
342
- | \`t\` | Auto-login token (24h, single-use, optional) |
343
- | \`ph\` | Phone number hint for OTP pre-fill (optional) |
344
-
345
- ## Auto-Login Token Security
346
-
347
- \`\`\`mermaid
348
- flowchart LR
349
- A[Token Generated] --> B[Signed with HMAC-SHA256]
350
- B --> C[Contains: userId + expiry + resourceId]
351
- C --> D[24-hour expiry]
352
- D --> E[Single-use — invalidated on first use]
353
- E --> F[Scoped to linked resource only]
354
- \`\`\`
355
-
356
- ## Per-Hospital URL Schemes
357
-
358
- | Hospital | Web Domain | Native Scheme |
359
- |----------|-----------|---------------|
360
- | NovaCare | novacare.medicsprime.in | novacare:// |
361
- | Sarji | sarji.medicsprime.in | sarji:// |
362
- | Cura | cura.medicsprime.in | cura:// |
363
- | Khushi | khushi.medicsprime.in | khushi:// |
364
- `
365
-
366
- const EP4_MD = `# EP4 — Push Notifications
367
-
368
- ## Notification Delivery Architecture
369
-
370
- \`\`\`mermaid
371
- flowchart TD
372
- A([Trigger]) --> B{Trigger Type}
373
-
374
- B -- Hospital Event --> C[Appointment Confirmed\\nCancelled / Lab Ready\\nPrescription Ready\\nBill Generated]
375
- B -- Scheduled Job --> D[Appointment Reminder\\nMedicine Dose\\nRefill Due\\nRx Expiry\\nCheckup Due]
376
- B -- Patient Action --> E[Payment Success\\nOrder Placed\\nMedicine Ordered]
377
-
378
- C --> F[Notification Engine\\nBackend Service]
379
- D --> F
380
- E --> F
381
-
382
- F --> G{Check Opt-in\\nPreferences}
383
- G -- Opted Out --> Z[Skip ✗]
384
- G -- Opted In --> H{Quiet Hours?\\n10pm – 7am}
385
-
386
- H -- Critical Alert --> I[Send Immediately\\nAppt Cancelled only]
387
- H -- Non-Critical + Quiet --> J[Queue for 7am]
388
- H -- Normal Hours --> K{Daily Limit\\nReached? max 3}
389
-
390
- K -- Under Limit --> L[Send Now]
391
- K -- At Limit --> M[Queue for Next Day]
392
-
393
- I --> N[FCM / Firebase]
394
- J --> N
395
- L --> N
396
-
397
- N --> O{Platform}
398
- O -- iOS/Android --> P[Capacitor Push\\nNative Alert]
399
- O -- Web --> Q[Firebase VAPID\\nBrowser Notification]
400
- O -- Critical Only --> R[SMS Fallback\\nvia Communication Service]
401
-
402
- P --> S[Tap → Deep Link\\nto Relevant Screen]
403
- Q --> S
404
- \`\`\`
405
-
406
- ## All Notification Scenarios
407
-
408
- \`\`\`mermaid
409
- mindmap
410
- root((Push\\nNotifications))
411
- Appointments
412
- Confirmed ✅
413
- Reminder 24h ⏰
414
- Reminder 2h ⏰
415
- Cancelled ❗
416
- Rescheduled ❗
417
- Doctor Late 🕐
418
- Lab and Reports
419
- Lab Result Ready 📋
420
- Prescription Ready 💊
421
- Home Collection Morning 🏠
422
- Order Confirmed 📦
423
- Medicine
424
- Dose Reminder 💊
425
- Missed Dose ⚠️
426
- Refill Due 🔄
427
- Rx Expiry 7 days 📅
428
- Order Dispatched 🚚
429
- Order Delivered 🎉
430
- Billing
431
- Bill Generated 💰
432
- Payment Success ✅
433
- Payment Due ⏳
434
- Preventive Health
435
- Annual Checkup 📅
436
- Follow-up Due 👨‍⚕️
437
- Vaccination Due 💉
438
- \`\`\`
439
-
440
- ## Notification Preferences
441
-
442
- \`\`\`mermaid
443
- flowchart LR
444
- A[Patient Settings] --> B[Notification Preferences]
445
- B --> C[🔔 Appointments\\nDefault: ON]
446
- B --> D[💊 Medicine\\nDefault: ON]
447
- B --> E[📦 Orders\\nDefault: ON]
448
- B --> F[💰 Billing\\nDefault: ON]
449
- B --> G[🏥 Preventive Health\\nDefault: ON]
450
- B --> H[📣 Promotional\\nDefault: OFF]
451
- \`\`\`
452
-
453
- ## Scenario Reference Table
454
-
455
- | Scenario | Trigger | Channel | Priority | Deep Link |
456
- |----------|---------|---------|----------|-----------|
457
- | Appt Confirmed | Event | Push | High | /apt?id=X |
458
- | Appt Reminder 24h | Scheduled | Push + SMS | High | /apt?id=X |
459
- | Appt Reminder 2h | Scheduled | Push | High | /apt?id=X |
460
- | Appt Cancelled | Event | Push + SMS | **Critical** | /apt?id=X |
461
- | Doctor Running Late | Event | Push | Normal | /apt?id=X |
462
- | Lab Result Ready | Event | Push | High | /report?id=X |
463
- | Prescription Ready | Event | Push | High | /rx?id=X |
464
- | Medicine Dose Due | Scheduled | Push | Normal | Reminder |
465
- | Missed Dose | Scheduled | Push | Normal | Reminder |
466
- | Refill Due | Scheduled | Push | High | /refill?med=X |
467
- | Rx Expiry (7 days) | Scheduled | Push | High | /rx?id=X |
468
- | Bill Generated | Event | Push | High | /bill?id=X |
469
- | Payment Success | Event | Push | Normal | /bill?id=X |
470
- | Payment Due | Scheduled | Push | Normal | /bill?id=X |
471
- | Annual Checkup | Scheduled | Push | Low | Book |
472
- | Follow-up Due | Scheduled | Push | Normal | Book |
473
- | Vaccination Due | Scheduled | Push | Normal | Reminder |
474
- `
475
-
476
- // ── Document IDs (created in previous session) ────────────────────────────────
477
- const DOCS = [
478
- { id: '69b9a635cf60316d6ce21a50', title: 'EP1 — Service Ordering', md: EP1_MD },
479
- { id: '69b9a63bcf60316d6ce21a52', title: 'EP2 — Medicine Ordering', md: EP2_MD },
480
- { id: '69b9a640cf60316d6ce21a54', title: 'EP3 — Deep Linking', md: EP3_MD },
481
- { id: '69b9a645cf60316d6ce21a56', title: 'EP4 — Push Notifications', md: EP4_MD }
482
- ]
483
-
484
- // ── Main ──────────────────────────────────────────────────────────────────────
485
-
486
- const { txClient, rawConnection, wsToken, workspaceUuid } = await connect()
487
- console.log(`Connected. Workspace UUID: ${workspaceUuid}`)
488
-
489
- for (const { id, title, md } of DOCS) {
490
- process.stdout.write(`Updating "${title}" ... `)
491
- try {
492
- const doc = await txClient.findOne(document.class.Document, { _id: id })
493
- if (!doc) { console.log('NOT FOUND — skipping'); continue }
494
-
495
- const prosemirror = markdownToProseMirror(md)
496
- const blobId = await uploadContent(wsToken, workspaceUuid, id, JSON.stringify(prosemirror))
497
- await txClient.updateDoc(document.class.Document, doc.space, doc._id, { content: blobId })
498
- console.log(`✅ blob: ${blobId}`)
499
- } catch (err) {
500
- console.log(`❌ ${err.message}`)
501
- }
502
- }
503
-
504
- await rawConnection.close()
505
- console.log('\nDone.')