vintasend 0.4.0 → 0.4.7

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 (284) hide show
  1. package/README.md +5 -0
  2. package/dist/examples/vintasend-medplum-example/.env.example +11 -0
  3. package/dist/examples/vintasend-medplum-example/IMPLEMENTATION_PLAN_FILE_ATTACHMENTS.md +597 -0
  4. package/dist/examples/vintasend-medplum-example/README.md +190 -0
  5. package/dist/examples/vintasend-medplum-example/TUTORIAL_EMAIL_NOTIFICATIONS.md +2596 -0
  6. package/dist/examples/vintasend-medplum-example/bots/handlers/send-pending-notifications-bot.ts +39 -0
  7. package/dist/examples/vintasend-medplum-example/bots/handlers/send-task-assignment-email.ts +41 -0
  8. package/dist/examples/vintasend-medplum-example/bots/handlers/task-due-soon-notification-bot.ts +86 -0
  9. package/dist/examples/vintasend-medplum-example/bots/index.ts +53 -0
  10. package/dist/examples/vintasend-medplum-example/bots/services/emails/schedule-task-due-soon-email.ts +84 -0
  11. package/dist/examples/vintasend-medplum-example/bots/services/emails/send-task-assignment-email.test.ts +388 -0
  12. package/dist/examples/vintasend-medplum-example/bots/services/emails/send-task-assignment-email.ts +113 -0
  13. package/dist/examples/vintasend-medplum-example/bots/shared/task-due-soon-helpers.ts +115 -0
  14. package/dist/examples/vintasend-medplum-example/bots/task-assignment-bot.ts +41 -0
  15. package/dist/examples/vintasend-medplum-example/compiled-notification-templates.json +6 -0
  16. package/dist/examples/vintasend-medplum-example/esbuild-script.mjs +71 -0
  17. package/dist/examples/vintasend-medplum-example/index.html +14 -0
  18. package/dist/examples/vintasend-medplum-example/lib/constants.ts +32 -0
  19. package/dist/examples/vintasend-medplum-example/lib/extensions.ts +1 -0
  20. package/dist/examples/vintasend-medplum-example/lib/file-upload.test.ts +389 -0
  21. package/dist/examples/vintasend-medplum-example/lib/file-upload.ts +222 -0
  22. package/dist/examples/vintasend-medplum-example/lib/medplum-singleton.ts +18 -0
  23. package/dist/examples/vintasend-medplum-example/lib/notification-service.test.ts +293 -0
  24. package/dist/examples/vintasend-medplum-example/lib/notification-service.ts +284 -0
  25. package/dist/examples/vintasend-medplum-example/lib/patients.ts +20 -0
  26. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-assignment/body.html.pug +37 -0
  27. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-assignment/subject.txt.pug +4 -0
  28. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-due-soon/body.html.pug +34 -0
  29. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-due-soon/subject.txt.pug +4 -0
  30. package/dist/examples/vintasend-medplum-example/package.json +75 -0
  31. package/dist/examples/vintasend-medplum-example/plugins/gql-plugin.mjs +31 -0
  32. package/dist/examples/vintasend-medplum-example/postcss.config.mjs +21 -0
  33. package/dist/examples/vintasend-medplum-example/public/favicon.ico +0 -0
  34. package/dist/examples/vintasend-medplum-example/public/img/integrations/acuity.png +0 -0
  35. package/dist/examples/vintasend-medplum-example/public/img/integrations/auth0.png +0 -0
  36. package/dist/examples/vintasend-medplum-example/public/img/integrations/azure.png +0 -0
  37. package/dist/examples/vintasend-medplum-example/public/img/integrations/calcom.png +0 -0
  38. package/dist/examples/vintasend-medplum-example/public/img/integrations/candid.png +0 -0
  39. package/dist/examples/vintasend-medplum-example/public/img/integrations/claude.png +0 -0
  40. package/dist/examples/vintasend-medplum-example/public/img/integrations/datadog.png +0 -0
  41. package/dist/examples/vintasend-medplum-example/public/img/integrations/deepseek.png +0 -0
  42. package/dist/examples/vintasend-medplum-example/public/img/integrations/entra.png +0 -0
  43. package/dist/examples/vintasend-medplum-example/public/img/integrations/epic.png +0 -0
  44. package/dist/examples/vintasend-medplum-example/public/img/integrations/google.png +0 -0
  45. package/dist/examples/vintasend-medplum-example/public/img/integrations/healthgorilla.png +0 -0
  46. package/dist/examples/vintasend-medplum-example/public/img/integrations/healthie.png +0 -0
  47. package/dist/examples/vintasend-medplum-example/public/img/integrations/labcorp.png +0 -0
  48. package/dist/examples/vintasend-medplum-example/public/img/integrations/okta.png +0 -0
  49. package/dist/examples/vintasend-medplum-example/public/img/integrations/openai.png +0 -0
  50. package/dist/examples/vintasend-medplum-example/public/img/integrations/particle.png +0 -0
  51. package/dist/examples/vintasend-medplum-example/public/img/integrations/quest.png +0 -0
  52. package/dist/examples/vintasend-medplum-example/public/img/integrations/recaptcha.png +0 -0
  53. package/dist/examples/vintasend-medplum-example/public/img/integrations/snowflake.png +0 -0
  54. package/dist/examples/vintasend-medplum-example/public/img/integrations/stedi.png +0 -0
  55. package/dist/examples/vintasend-medplum-example/public/img/integrations/stripe.png +0 -0
  56. package/dist/examples/vintasend-medplum-example/public/img/integrations/sumo.png +0 -0
  57. package/dist/examples/vintasend-medplum-example/scripts/README.md +162 -0
  58. package/dist/examples/vintasend-medplum-example/scripts/client.ts +18 -0
  59. package/dist/examples/vintasend-medplum-example/scripts/deploy-bots.ts +171 -0
  60. package/dist/examples/vintasend-medplum-example/src/App.tsx +185 -0
  61. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemList.test.tsx +350 -0
  62. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemList.tsx +241 -0
  63. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemPanel.test.tsx +616 -0
  64. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemPanel.tsx +138 -0
  65. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionItem.test.tsx +92 -0
  66. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionItem.tsx +47 -0
  67. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionList.test.tsx +464 -0
  68. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionList.tsx +186 -0
  69. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionModal.test.tsx +80 -0
  70. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionModal.tsx +82 -0
  71. package/dist/examples/vintasend-medplum-example/src/components/DoseSpotIcon.test.tsx +100 -0
  72. package/dist/examples/vintasend-medplum-example/src/components/DoseSpotIcon.tsx +20 -0
  73. package/dist/examples/vintasend-medplum-example/src/components/IntegrationCard.module.css +3 -0
  74. package/dist/examples/vintasend-medplum-example/src/components/IntegrationCard.tsx +62 -0
  75. package/dist/examples/vintasend-medplum-example/src/components/MessageWithLinks.tsx +47 -0
  76. package/dist/examples/vintasend-medplum-example/src/components/PerformingLabInput.test.tsx +299 -0
  77. package/dist/examples/vintasend-medplum-example/src/components/PerformingLabInput.tsx +52 -0
  78. package/dist/examples/vintasend-medplum-example/src/components/ResourceFormWithRequiredProfile.tsx +82 -0
  79. package/dist/examples/vintasend-medplum-example/src/components/encounter/BillingTab.test.tsx +1016 -0
  80. package/dist/examples/vintasend-medplum-example/src/components/encounter/BillingTab.tsx +298 -0
  81. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterChart.test.tsx +732 -0
  82. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterChart.tsx +282 -0
  83. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterHeader.test.tsx +268 -0
  84. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterHeader.tsx +224 -0
  85. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignAddendum.test.tsx +255 -0
  86. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignAddendum.tsx +212 -0
  87. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignLockDialog.test.tsx +120 -0
  88. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignLockDialog.tsx +57 -0
  89. package/dist/examples/vintasend-medplum-example/src/components/encounter/VisitDetailsPanel.test.tsx +224 -0
  90. package/dist/examples/vintasend-medplum-example/src/components/encounter/VisitDetailsPanel.tsx +100 -0
  91. package/dist/examples/vintasend-medplum-example/src/components/labs/CoverageInput.test.tsx +431 -0
  92. package/dist/examples/vintasend-medplum-example/src/components/labs/CoverageInput.tsx +130 -0
  93. package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.module.css +31 -0
  94. package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.test.tsx +234 -0
  95. package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.tsx +143 -0
  96. package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.module.css +11 -0
  97. package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.test.tsx +875 -0
  98. package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.tsx +943 -0
  99. package/dist/examples/vintasend-medplum-example/src/components/labs/LabResultDetails.test.tsx +413 -0
  100. package/dist/examples/vintasend-medplum-example/src/components/labs/LabResultDetails.tsx +203 -0
  101. package/dist/examples/vintasend-medplum-example/src/components/labs/LabSelectEmpty.tsx +22 -0
  102. package/dist/examples/vintasend-medplum-example/src/components/labs/README.md +104 -0
  103. package/dist/examples/vintasend-medplum-example/src/components/labs/TestMetadataCardInput.test.tsx +318 -0
  104. package/dist/examples/vintasend-medplum-example/src/components/labs/TestMetadataCardInput.tsx +87 -0
  105. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatList.test.tsx +126 -0
  106. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatList.tsx +38 -0
  107. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.module.css +23 -0
  108. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.test.tsx +167 -0
  109. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.tsx +53 -0
  110. package/dist/examples/vintasend-medplum-example/src/components/messages/NewTopicDialog.test.tsx +94 -0
  111. package/dist/examples/vintasend-medplum-example/src/components/messages/NewTopicDialog.tsx +165 -0
  112. package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.module.css +8 -0
  113. package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.test.tsx +523 -0
  114. package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.tsx +230 -0
  115. package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.module.css +23 -0
  116. package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.test.tsx +567 -0
  117. package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.tsx +358 -0
  118. package/dist/examples/vintasend-medplum-example/src/components/plandefinition/AddPlanDefinition.module.css +40 -0
  119. package/dist/examples/vintasend-medplum-example/src/components/plandefinition/AddPlanDefinition.tsx +257 -0
  120. package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.module.css +7 -0
  121. package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.test.tsx +279 -0
  122. package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.tsx +156 -0
  123. package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.module.css +45 -0
  124. package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.test.tsx +90 -0
  125. package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.tsx +84 -0
  126. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourceBox.module.css +26 -0
  127. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourceBox.tsx +90 -0
  128. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourcePanel.test.tsx +305 -0
  129. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourcePanel.tsx +46 -0
  130. package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.module.css +262 -0
  131. package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.test.tsx +622 -0
  132. package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.tsx +286 -0
  133. package/dist/examples/vintasend-medplum-example/src/components/tasks/NewTaskModal.tsx +275 -0
  134. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskAttachmentList.tsx +132 -0
  135. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.module.css +45 -0
  136. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.test.tsx +749 -0
  137. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.tsx +416 -0
  138. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailPanel.test.tsx +278 -0
  139. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailPanel.tsx +133 -0
  140. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.module.css +16 -0
  141. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.test.tsx +255 -0
  142. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.tsx +203 -0
  143. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFileUpload.tsx +129 -0
  144. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.test.tsx +156 -0
  145. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.tsx +142 -0
  146. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.utils.ts +28 -0
  147. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskInputNote.test.tsx +134 -0
  148. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskInputNote.tsx +250 -0
  149. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.module.css +23 -0
  150. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.test.tsx +149 -0
  151. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.tsx +53 -0
  152. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskNoteItem.test.tsx +68 -0
  153. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskNoteItem.tsx +46 -0
  154. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskProperties.test.tsx +555 -0
  155. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskProperties.tsx +170 -0
  156. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskSelectEmpty.test.tsx +32 -0
  157. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskSelectEmpty.tsx +34 -0
  158. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/SimpleTask.test.tsx +47 -0
  159. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/SimpleTask.tsx +29 -0
  160. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskPanel.test.tsx +285 -0
  161. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskPanel.tsx +129 -0
  162. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskQuestionnaireForm.test.tsx +455 -0
  163. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskQuestionnaireForm.tsx +167 -0
  164. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskServiceRequest.test.tsx +435 -0
  165. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskServiceRequest.tsx +116 -0
  166. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.module.css +38 -0
  167. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.test.tsx +200 -0
  168. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.tsx +84 -0
  169. package/dist/examples/vintasend-medplum-example/src/components/utils.test.ts +176 -0
  170. package/dist/examples/vintasend-medplum-example/src/components/utils.ts +17 -0
  171. package/dist/examples/vintasend-medplum-example/src/config/constants.ts +3 -0
  172. package/dist/examples/vintasend-medplum-example/src/hooks/useDebouncedUpdateResource.test.tsx +166 -0
  173. package/dist/examples/vintasend-medplum-example/src/hooks/useDebouncedUpdateResource.ts +28 -0
  174. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounter.test.tsx +94 -0
  175. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounter.ts +11 -0
  176. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounterChart.test.tsx +477 -0
  177. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounterChart.ts +191 -0
  178. package/dist/examples/vintasend-medplum-example/src/hooks/usePatient.test.tsx +100 -0
  179. package/dist/examples/vintasend-medplum-example/src/hooks/usePatient.ts +18 -0
  180. package/dist/examples/vintasend-medplum-example/src/hooks/useThreadInbox.test.tsx +379 -0
  181. package/dist/examples/vintasend-medplum-example/src/hooks/useThreadInbox.ts +194 -0
  182. package/dist/examples/vintasend-medplum-example/src/index.css +8 -0
  183. package/dist/examples/vintasend-medplum-example/src/main.tsx +57 -0
  184. package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.module.css +6 -0
  185. package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.test.tsx +295 -0
  186. package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.tsx +124 -0
  187. package/dist/examples/vintasend-medplum-example/src/pages/SignInPage.test.tsx +77 -0
  188. package/dist/examples/vintasend-medplum-example/src/pages/SignInPage.tsx +22 -0
  189. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterChartPage.test.tsx +87 -0
  190. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterChartPage.tsx +27 -0
  191. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.module.css +16 -0
  192. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.test.tsx +287 -0
  193. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.tsx +151 -0
  194. package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.test.tsx +519 -0
  195. package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.tsx +179 -0
  196. package/dist/examples/vintasend-medplum-example/src/pages/integrations/FavoriteMedicationsTable.tsx +76 -0
  197. package/dist/examples/vintasend-medplum-example/src/pages/integrations/IntegrationsPage.test.tsx +234 -0
  198. package/dist/examples/vintasend-medplum-example/src/pages/integrations/IntegrationsPage.tsx +222 -0
  199. package/dist/examples/vintasend-medplum-example/src/pages/labs/OrderLabsPage.test.tsx +356 -0
  200. package/dist/examples/vintasend-medplum-example/src/pages/labs/OrderLabsPage.tsx +275 -0
  201. package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.module.css +8 -0
  202. package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.test.tsx +103 -0
  203. package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.tsx +78 -0
  204. package/dist/examples/vintasend-medplum-example/src/pages/patient/CommunicationTab.test.tsx +84 -0
  205. package/dist/examples/vintasend-medplum-example/src/pages/patient/CommunicationTab.tsx +82 -0
  206. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotAdvancedOptions.test.tsx +364 -0
  207. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotAdvancedOptions.tsx +149 -0
  208. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotTab.test.tsx +159 -0
  209. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotTab.tsx +37 -0
  210. package/dist/examples/vintasend-medplum-example/src/pages/patient/EditTab.test.tsx +140 -0
  211. package/dist/examples/vintasend-medplum-example/src/pages/patient/EditTab.tsx +72 -0
  212. package/dist/examples/vintasend-medplum-example/src/pages/patient/ExportTab.test.tsx +57 -0
  213. package/dist/examples/vintasend-medplum-example/src/pages/patient/ExportTab.tsx +14 -0
  214. package/dist/examples/vintasend-medplum-example/src/pages/patient/IntakeFormPage.test.tsx +241 -0
  215. package/dist/examples/vintasend-medplum-example/src/pages/patient/IntakeFormPage.tsx +710 -0
  216. package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.module.css +37 -0
  217. package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.test.tsx +428 -0
  218. package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.tsx +334 -0
  219. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.module.css +24 -0
  220. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.test.tsx +154 -0
  221. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.tsx +115 -0
  222. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.utils.test.ts +223 -0
  223. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.utils.ts +89 -0
  224. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientSearchPage.test.tsx +147 -0
  225. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientSearchPage.tsx +79 -0
  226. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientTabsNavigation.tsx +35 -0
  227. package/dist/examples/vintasend-medplum-example/src/pages/patient/TasksTab.test.tsx +185 -0
  228. package/dist/examples/vintasend-medplum-example/src/pages/patient/TasksTab.tsx +115 -0
  229. package/dist/examples/vintasend-medplum-example/src/pages/patient/TimelineTab.tsx +14 -0
  230. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceCreatePage.test.tsx +170 -0
  231. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceCreatePage.tsx +117 -0
  232. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceDetailPage.tsx +28 -0
  233. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceEditPage.test.tsx +131 -0
  234. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceEditPage.tsx +65 -0
  235. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceHistoryPage.test.tsx +108 -0
  236. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceHistoryPage.tsx +16 -0
  237. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.module.css +7 -0
  238. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.test.tsx +37 -0
  239. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.tsx +44 -0
  240. package/dist/examples/vintasend-medplum-example/src/pages/resource/useResourceType.ts +44 -0
  241. package/dist/examples/vintasend-medplum-example/src/pages/resource/utils.ts +9 -0
  242. package/dist/examples/vintasend-medplum-example/src/pages/schedule/SchedulePage.test.tsx +302 -0
  243. package/dist/examples/vintasend-medplum-example/src/pages/schedule/SchedulePage.tsx +416 -0
  244. package/dist/examples/vintasend-medplum-example/src/pages/spaces/ChatInput.tsx +91 -0
  245. package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.module.css +6 -0
  246. package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.test.tsx +102 -0
  247. package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.tsx +44 -0
  248. package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.module.css +7 -0
  249. package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.test.tsx +133 -0
  250. package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.tsx +91 -0
  251. package/dist/examples/vintasend-medplum-example/src/test-utils/render.tsx +20 -0
  252. package/dist/examples/vintasend-medplum-example/src/test.setup.ts +49 -0
  253. package/dist/examples/vintasend-medplum-example/src/types/encounter.ts +8 -0
  254. package/dist/examples/vintasend-medplum-example/src/types/spaces.ts +10 -0
  255. package/dist/examples/vintasend-medplum-example/src/utils/chargeitems.test.ts +141 -0
  256. package/dist/examples/vintasend-medplum-example/src/utils/chargeitems.ts +59 -0
  257. package/dist/examples/vintasend-medplum-example/src/utils/claims.test.ts +153 -0
  258. package/dist/examples/vintasend-medplum-example/src/utils/claims.ts +65 -0
  259. package/dist/examples/vintasend-medplum-example/src/utils/communication-search.ts +47 -0
  260. package/dist/examples/vintasend-medplum-example/src/utils/coverage.test.ts +48 -0
  261. package/dist/examples/vintasend-medplum-example/src/utils/coverage.ts +33 -0
  262. package/dist/examples/vintasend-medplum-example/src/utils/documentReference.test.ts +102 -0
  263. package/dist/examples/vintasend-medplum-example/src/utils/documentReference.ts +55 -0
  264. package/dist/examples/vintasend-medplum-example/src/utils/encounter.test.ts +169 -0
  265. package/dist/examples/vintasend-medplum-example/src/utils/encounter.ts +261 -0
  266. package/dist/examples/vintasend-medplum-example/src/utils/intake-form.test.ts +154 -0
  267. package/dist/examples/vintasend-medplum-example/src/utils/intake-form.ts +272 -0
  268. package/dist/examples/vintasend-medplum-example/src/utils/intake-utils.test.ts +1137 -0
  269. package/dist/examples/vintasend-medplum-example/src/utils/intake-utils.ts +827 -0
  270. package/dist/examples/vintasend-medplum-example/src/utils/notifications.test.ts +27 -0
  271. package/dist/examples/vintasend-medplum-example/src/utils/notifications.ts +15 -0
  272. package/dist/examples/vintasend-medplum-example/src/utils/spaceMessaging.ts +249 -0
  273. package/dist/examples/vintasend-medplum-example/src/utils/spacePersistence.test.ts +450 -0
  274. package/dist/examples/vintasend-medplum-example/src/utils/spacePersistence.ts +147 -0
  275. package/dist/examples/vintasend-medplum-example/src/utils/task-search.ts +63 -0
  276. package/dist/examples/vintasend-medplum-example/src/vite-env.d.ts +3 -0
  277. package/dist/examples/vintasend-medplum-example/tsconfig.bots.json +4 -0
  278. package/dist/examples/vintasend-medplum-example/tsconfig.json +19 -0
  279. package/dist/examples/vintasend-medplum-example/vercel.json +3 -0
  280. package/dist/examples/vintasend-medplum-example/vite.config.ts +44 -0
  281. package/dist/services/notification-backends/base-notification-backend.d.ts +5 -0
  282. package/dist/services/notification-service.js +11 -1
  283. package/dist/services/notification-template-renderers/base-email-template-renderer.d.ts +5 -0
  284. package/package.json +1 -1
@@ -0,0 +1,39 @@
1
+ import { BotEvent, MedplumClient } from '@medplum/core';
2
+ import { MedplumSingleton } from '../../lib/medplum-singleton';
3
+ import { buildSendGridConfig, getNotificationService } from '../../lib/notification-service';
4
+
5
+ /**
6
+ * Medplum Bot: Send Pending Notifications
7
+ *
8
+ * This bot runs periodically (every 5 minutes) to process and send
9
+ * all pending scheduled notifications that are due to be sent.
10
+ *
11
+ * It uses VintaSend's notification service to check for notifications
12
+ * where sendAfter <= current time and triggers their delivery.
13
+ *
14
+ * Cron: every 5 minutes
15
+ */
16
+
17
+ export async function handler(medplum: MedplumClient, event: BotEvent): Promise<any> {
18
+ console.log('[SendPendingNotificationsBot] Starting to process pending notifications');
19
+ const sendgridConfig = buildSendGridConfig(event);
20
+
21
+ try {
22
+ MedplumSingleton.setInstance(medplum);
23
+ const vintasend = getNotificationService(medplum, sendgridConfig);
24
+
25
+ // Send all pending notifications that are ready to be sent
26
+ const result = await vintasend.sendPendingNotifications();
27
+
28
+ console.log('[SendPendingNotificationsBot] Completed processing pending notifications');
29
+ console.log('[SendPendingNotificationsBot] Result:', JSON.stringify(result, null, 2));
30
+
31
+ return {
32
+ message: 'Pending notifications processed',
33
+ result,
34
+ };
35
+ } catch (error) {
36
+ console.error('[SendPendingNotificationsBot] Error processing pending notifications:', error);
37
+ throw error;
38
+ }
39
+ }
@@ -0,0 +1,41 @@
1
+ import { BotEvent, MedplumClient } from '@medplum/core';
2
+ import { Task } from '@medplum/fhirtypes';
3
+ import { sendTaskAssignmentEmail } from '../services/emails/send-task-assignment-email';
4
+ import { buildSendGridConfig } from '../../lib/notification-service';
5
+
6
+ /**
7
+ * Medplum Bot: Task Assignment Email Notification
8
+ *
9
+ * This bot is triggered by a subscription when a Task is created or updated
10
+ * with an owner. It sends an email notification to the assigned practitioner.
11
+ *
12
+ * Subscription Criteria: Task?owner:exists=true
13
+ * Triggers: create, update
14
+ */
15
+
16
+ export async function handler(medplum: MedplumClient, event: BotEvent): Promise<Task> {
17
+ const task = event.input as Task;
18
+ const sendGridVariables = buildSendGridConfig(event);
19
+
20
+ console.log(`[TaskAssignmentBot] Processing task: ${task.id}`);
21
+ console.log(`[TaskAssignmentBot] Owner: ${task.owner?.reference}`);
22
+
23
+ // Only send email if task has an owner
24
+ if (task.owner?.reference) {
25
+ const appBaseUrl = process.env.APP_BASE_URL || 'https://your-app-url.com';
26
+
27
+ try {
28
+ await sendTaskAssignmentEmail(medplum, task, appBaseUrl, sendGridVariables);
29
+ console.log(`[TaskAssignmentBot] Email notification sent successfully for task: ${task.id}`);
30
+ } catch (error) {
31
+ console.error(`[TaskAssignmentBot] Failed to send email for task: ${task.id}`, error);
32
+ // Don't throw - we don't want the subscription to fail
33
+ // The notification will be logged in Medplum as failed
34
+ }
35
+ } else {
36
+ console.log(`[TaskAssignmentBot] Task ${task.id} has no owner, skipping email notification`);
37
+ }
38
+
39
+ // Return the task unchanged
40
+ return task;
41
+ }
@@ -0,0 +1,86 @@
1
+ import { BotEvent, MedplumClient } from '@medplum/core';
2
+ import { Task } from '@medplum/fhirtypes';
3
+ import { scheduleTaskDueSoonEmail } from '../services/emails/schedule-task-due-soon-email';
4
+ import { buildSendGridConfig } from '../../lib/notification-service';
5
+ import { getTaskDueSoonSchedulingReason } from '../shared/task-due-soon-helpers';
6
+
7
+ /**
8
+ * Medplum Bot: Task Due Soon Notification
9
+ *
10
+ * This bot triggers on Task creation/update and schedules email notifications
11
+ * to be sent 24 hours before the task due date.
12
+ *
13
+ * The bot uses VintaSend's scheduled messages (sendAfter) to ensure
14
+ * notifications are sent at the appropriate time. The actual sending is
15
+ * handled by the send-pending-notifications-bot.
16
+ *
17
+ * Subscription: Task (create/update)
18
+ */
19
+
20
+ export async function handler(medplum: MedplumClient, event: BotEvent): Promise<any> {
21
+ const task = event.input as Task;
22
+ const result = getTaskDueSoonSchedulingReason(task);
23
+
24
+ switch (result.kind) {
25
+ case 'invalidResource':
26
+ console.warn('[TaskDueSoonNotificationBot] Invalid task resource received');
27
+ return { message: 'Invalid task resource' };
28
+ case 'noDueDate':
29
+ console.log(`[TaskDueSoonNotificationBot] Task ${task?.id} has no due date, skipping`);
30
+ return { message: 'No due date set', taskId: task?.id };
31
+ case 'invalidDueDate':
32
+ console.warn(
33
+ `[TaskDueSoonNotificationBot] Task ${task?.id} has invalid due date: ${result.dueDate}, skipping`
34
+ );
35
+ return { message: 'Invalid due date', taskId: task?.id, dueDate: result.dueDate };
36
+ case 'finalState':
37
+ console.log(
38
+ `[TaskDueSoonNotificationBot] Task ${task?.id} is in final state (${result.status}), skipping`
39
+ );
40
+ return { message: `Task in final state: ${result.status}`, taskId: task?.id };
41
+ case 'noOwner':
42
+ console.log(`[TaskDueSoonNotificationBot] Task ${task?.id} has no owner, skipping`);
43
+ return { message: 'No owner assigned', taskId: task?.id };
44
+ case 'tooSoon':
45
+ console.log(
46
+ `[TaskDueSoonNotificationBot] Task ${task?.id} is due in ${result.hoursUntilDue.toFixed(
47
+ 2
48
+ )} hours (less than 24), skipping`
49
+ );
50
+ return {
51
+ message: 'Due date is less than 24 hours away',
52
+ taskId: task?.id,
53
+ hoursUntilDue: result.hoursUntilDue,
54
+ };
55
+ case 'ok':
56
+ break;
57
+ }
58
+
59
+ const appBaseUrl = process.env.APP_BASE_URL;
60
+ if (!appBaseUrl) {
61
+ console.error('[TaskDueSoonNotificationBot] APP_BASE_URL environment variable is not set');
62
+ throw new Error('APP_BASE_URL must be configured');
63
+ }
64
+
65
+ const sendgridConfig = buildSendGridConfig(event);
66
+
67
+ try {
68
+ console.log(
69
+ `[TaskDueSoonNotificationBot] Scheduling notification for task ${task.id}, due in ${result.hoursUntilDue.toFixed(
70
+ 2
71
+ )} hours`
72
+ );
73
+ await scheduleTaskDueSoonEmail(medplum, task, appBaseUrl, sendgridConfig);
74
+
75
+ return {
76
+ message: 'Notification scheduled successfully',
77
+ taskId: task.id,
78
+ dueDate: task.restriction?.period?.end,
79
+ hoursUntilDue: result.hoursUntilDue.toFixed(2),
80
+ };
81
+ } catch (error) {
82
+ console.error(`[TaskDueSoonNotificationBot] Error scheduling notification for task ${task.id}:`, error);
83
+ throw error;
84
+ }
85
+ }
86
+
@@ -0,0 +1,53 @@
1
+ import { Extension } from '@medplum/fhirtypes';
2
+
3
+ export interface BotDescription {
4
+ name: string;
5
+ criteria?: string;
6
+ extension?: Extension[];
7
+ needsAdminMembership?: boolean;
8
+ runAsUser?: boolean;
9
+ timeout?: number;
10
+ cronString?: string;
11
+ }
12
+
13
+ export const BOTS: BotDescription[] = [
14
+ {
15
+ name: 'send-task-assignment-email',
16
+ needsAdminMembership: true,
17
+ runAsUser: true,
18
+ criteria: 'Task?owner:missing=false',
19
+ extension: [
20
+ {
21
+ url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
22
+ valueCode: 'create',
23
+ },
24
+ {
25
+ url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
26
+ valueCode: 'update',
27
+ },
28
+ ],
29
+ },
30
+ {
31
+ name: 'task-due-soon-notification-bot',
32
+ needsAdminMembership: true,
33
+ runAsUser: true,
34
+ criteria: 'Task?owner:missing=false',
35
+ extension: [
36
+ {
37
+ url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
38
+ valueCode: 'create',
39
+ },
40
+ {
41
+ url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
42
+ valueCode: 'update',
43
+ },
44
+ ],
45
+ },
46
+ {
47
+ name: 'send-pending-notifications-bot',
48
+ needsAdminMembership: true,
49
+ runAsUser: true,
50
+ cronString: '*/5 * * * *', // Run every 5 minutes
51
+ timeout: 300, // 5 minutes timeout
52
+ },
53
+ ];
@@ -0,0 +1,84 @@
1
+ import { MedplumClient } from '@medplum/core';
2
+ import { Task } from '@medplum/fhirtypes';
3
+ import { MedplumSingleton } from '../../../lib/medplum-singleton';
4
+ import { getNotificationService, SendGridConfig } from '../../../lib/notification-service';
5
+ import {
6
+ assertTaskOwnerReference,
7
+ getValidTaskDueDate,
8
+ parseOwnerReference,
9
+ computeReminderTime,
10
+ } from '../../shared/task-due-soon-helpers';
11
+
12
+ export async function scheduleTaskDueSoonEmail(
13
+ medplum: MedplumClient,
14
+ task: Task,
15
+ taskLinkBaseUrl: string,
16
+ sendgridConfig: SendGridConfig
17
+ ) {
18
+ /* sends a task due soon reminder email to a practitioner 24 hours before the task is due */
19
+
20
+ const ownerRef = assertTaskOwnerReference(task);
21
+ const parsedOwner = parseOwnerReference(ownerRef);
22
+ if (!parsedOwner) {
23
+ return;
24
+ }
25
+
26
+ const dueDate = getValidTaskDueDate(task);
27
+
28
+ if (!task.id) {
29
+ // eslint-disable-next-line no-console
30
+ console.error('[scheduleTaskDueSoonEmail] Task has no id');
31
+ throw new Error('Task must have an id to send task due soon email');
32
+ }
33
+
34
+ const sendAfter = computeReminderTime(dueDate, 24);
35
+ if (!sendAfter) {
36
+ return;
37
+ }
38
+
39
+ MedplumSingleton.setInstance(medplum);
40
+ const vintasend = getNotificationService(medplum, sendgridConfig);
41
+
42
+ const taskTitle = task.code?.text || task.description || 'Task';
43
+ const taskLink = `${taskLinkBaseUrl}/Task/${task.id}`;
44
+ const taskIsUrgent = task.priority === 'urgent';
45
+ const formattedDueDate = dueDate.toLocaleString('en-US', {
46
+ weekday: 'long',
47
+ year: 'numeric',
48
+ month: 'long',
49
+ day: 'numeric',
50
+ hour: '2-digit',
51
+ minute: '2-digit',
52
+ });
53
+
54
+ try {
55
+ await vintasend.createNotification({
56
+ userId: ownerRef,
57
+ notificationType: 'EMAIL' as const,
58
+ title: 'Task Due Soon Reminder',
59
+ contextName: 'taskDueSoon' as const,
60
+ contextParameters: {
61
+ userId: ownerRef,
62
+ taskTitle,
63
+ taskDescription: task.description || '',
64
+ taskIsUrgent,
65
+ taskLink,
66
+ dueDate: formattedDueDate,
67
+ },
68
+ sendAfter,
69
+ bodyTemplate: 'emails/task-due-soon/body.html.pug',
70
+ subjectTemplate: 'emails/task-due-soon/subject.txt.pug',
71
+ extraParams: {},
72
+ });
73
+
74
+ // eslint-disable-next-line no-console
75
+ console.log(
76
+ `[scheduleTaskDueSoonEmail] Email scheduled for ${sendAfter.toISOString()} to: ${ownerRef} for task due on ${dueDate.toISOString()}`
77
+ );
78
+ } catch (error) {
79
+ // eslint-disable-next-line no-console
80
+ console.error('[scheduleTaskDueSoonEmail] Error creating/sending notification:', error);
81
+ throw error;
82
+ }
83
+ }
84
+
@@ -0,0 +1,388 @@
1
+ // @ts-nocheck - MockClient type compatibility with MedplumClient
2
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
3
+ import { MockClient } from '@medplum/mock';
4
+ import type { Task, Media, Binary, Practitioner } from '@medplum/fhirtypes';
5
+ import { sendTaskAssignmentEmail } from './send-task-assignment-email';
6
+ import type { SendGridConfig } from '../../../lib/notification-service';
7
+
8
+ // Mock the notification service
9
+ vi.mock('../../../lib/notification-service', async () => {
10
+ const actual = await vi.importActual('../../../lib/notification-service');
11
+ return {
12
+ ...actual,
13
+ getNotificationService: vi.fn(() => ({
14
+ createNotification: vi.fn().mockResolvedValue(undefined),
15
+ })),
16
+ };
17
+ });
18
+
19
+ // Mock the MedplumSingleton
20
+ vi.mock('../../../lib/medplum-singleton', () => ({
21
+ MedplumSingleton: {
22
+ setInstance: vi.fn(),
23
+ getInstance: vi.fn(),
24
+ },
25
+ }));
26
+
27
+ describe('sendTaskAssignmentEmail with Attachments', () => {
28
+ let medplum: MockClient;
29
+ let sendgridConfig: SendGridConfig;
30
+ const taskLinkBaseUrl = 'https://example.com';
31
+
32
+ beforeEach(() => {
33
+ medplum = new MockClient();
34
+ sendgridConfig = {
35
+ SENDGRID_API_KEY: 'test-api-key',
36
+ SENDGRID_FROM_EMAIL: 'noreply@example.com',
37
+ SENDGRID_FROM_NAME: 'Test App',
38
+ };
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ it('should send email with single attachment', async () => {
43
+ // Create a practitioner
44
+ const practitioner = await medplum.createResource<Practitioner>({
45
+ resourceType: 'Practitioner',
46
+ name: [{ given: ['John'], family: 'Doe' }],
47
+ });
48
+
49
+ // Create a binary resource
50
+ const binary = await medplum.createResource<Binary>({
51
+ resourceType: 'Binary',
52
+ contentType: 'application/pdf',
53
+ data: Buffer.from('test pdf content').toString('base64'),
54
+ });
55
+
56
+ // Create a media resource
57
+ const media = await medplum.createResource<Media>({
58
+ resourceType: 'Media',
59
+ status: 'completed',
60
+ content: {
61
+ contentType: 'application/pdf',
62
+ url: `Binary/${binary.id}`,
63
+ title: 'test-document.pdf',
64
+ },
65
+ });
66
+
67
+ // Create a task with attachment
68
+ const task = await medplum.createResource<Task>({
69
+ resourceType: 'Task',
70
+ status: 'requested',
71
+ intent: 'order',
72
+ description: 'Test task with attachment',
73
+ code: { text: 'Review Document' },
74
+ owner: { reference: `Practitioner/${practitioner.id}` },
75
+ priority: 'routine',
76
+ input: [
77
+ {
78
+ type: {
79
+ coding: [
80
+ {
81
+ system: 'http://your-app-url.com/task-input-types',
82
+ code: 'attachment',
83
+ display: 'File Attachment',
84
+ },
85
+ ],
86
+ },
87
+ valueReference: { reference: `Media/${media.id}` },
88
+ },
89
+ ],
90
+ });
91
+
92
+ const { getNotificationService } = await import('../../../lib/notification-service');
93
+ const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
94
+ (getNotificationService as any).mockReturnValue({
95
+ createNotification: mockCreateNotification,
96
+ });
97
+
98
+ await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
99
+
100
+ expect(mockCreateNotification).toHaveBeenCalledTimes(1);
101
+ const callArgs = mockCreateNotification.mock.calls[0][0];
102
+
103
+ expect(callArgs.contextParameters.attachmentCount).toBe(1);
104
+ expect(callArgs.attachments).toBeDefined();
105
+ expect(callArgs.attachments.length).toBe(1);
106
+ expect(callArgs.attachments[0].fileId).toBe(media.id);
107
+ expect(callArgs.attachments[0].description).toBe('test-document.pdf');
108
+ });
109
+
110
+ it('should send email with multiple attachments', async () => {
111
+ const practitioner = await medplum.createResource<Practitioner>({
112
+ resourceType: 'Practitioner',
113
+ name: [{ given: ['Jane'], family: 'Smith' }],
114
+ });
115
+
116
+ // Create multiple binary resources
117
+ const binary1 = await medplum.createResource<Binary>({
118
+ resourceType: 'Binary',
119
+ contentType: 'application/pdf',
120
+ data: Buffer.from('pdf content').toString('base64'),
121
+ });
122
+
123
+ const binary2 = await medplum.createResource<Binary>({
124
+ resourceType: 'Binary',
125
+ contentType: 'image/jpeg',
126
+ data: Buffer.from('jpeg content').toString('base64'),
127
+ });
128
+
129
+ // Create multiple media resources
130
+ const media1 = await medplum.createResource<Media>({
131
+ resourceType: 'Media',
132
+ status: 'completed',
133
+ content: {
134
+ contentType: 'application/pdf',
135
+ url: `Binary/${binary1.id}`,
136
+ title: 'document.pdf',
137
+ },
138
+ });
139
+
140
+ const media2 = await medplum.createResource<Media>({
141
+ resourceType: 'Media',
142
+ status: 'completed',
143
+ content: {
144
+ contentType: 'image/jpeg',
145
+ url: `Binary/${binary2.id}`,
146
+ title: 'image.jpg',
147
+ },
148
+ });
149
+
150
+ // Create task with multiple attachments
151
+ const task = await medplum.createResource<Task>({
152
+ resourceType: 'Task',
153
+ status: 'requested',
154
+ intent: 'order',
155
+ description: 'Task with multiple attachments',
156
+ code: { text: 'Review Files' },
157
+ owner: { reference: `Practitioner/${practitioner.id}` },
158
+ input: [
159
+ {
160
+ type: {
161
+ coding: [
162
+ {
163
+ system: 'http://your-app-url.com/task-input-types',
164
+ code: 'attachment',
165
+ display: 'File Attachment',
166
+ },
167
+ ],
168
+ },
169
+ valueReference: { reference: `Media/${media1.id}` },
170
+ },
171
+ {
172
+ type: {
173
+ coding: [
174
+ {
175
+ system: 'http://your-app-url.com/task-input-types',
176
+ code: 'attachment',
177
+ display: 'File Attachment',
178
+ },
179
+ ],
180
+ },
181
+ valueReference: { reference: `Media/${media2.id}` },
182
+ },
183
+ ],
184
+ });
185
+
186
+ const { getNotificationService } = await import('../../../lib/notification-service');
187
+ const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
188
+ (getNotificationService as any).mockReturnValue({
189
+ createNotification: mockCreateNotification,
190
+ });
191
+
192
+ await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
193
+
194
+ expect(mockCreateNotification).toHaveBeenCalledTimes(1);
195
+ const callArgs = mockCreateNotification.mock.calls[0][0];
196
+
197
+ expect(callArgs.contextParameters.attachmentCount).toBe(2);
198
+ expect(callArgs.attachments).toBeDefined();
199
+ expect(callArgs.attachments.length).toBe(2);
200
+ expect(callArgs.attachments[0].fileId).toBe(media1.id);
201
+ expect(callArgs.attachments[0].description).toBe('document.pdf');
202
+ expect(callArgs.attachments[1].fileId).toBe(media2.id);
203
+ expect(callArgs.attachments[1].description).toBe('image.jpg');
204
+ });
205
+
206
+ it('should send email without attachments for task with no files', async () => {
207
+ const practitioner = await medplum.createResource<Practitioner>({
208
+ resourceType: 'Practitioner',
209
+ name: [{ given: ['Bob'], family: 'Johnson' }],
210
+ });
211
+
212
+ const task = await medplum.createResource<Task>({
213
+ resourceType: 'Task',
214
+ status: 'requested',
215
+ intent: 'order',
216
+ description: 'Task without attachments',
217
+ code: { text: 'Simple Task' },
218
+ owner: { reference: `Practitioner/${practitioner.id}` },
219
+ });
220
+
221
+ const { getNotificationService } = await import('../../../lib/notification-service');
222
+ const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
223
+ (getNotificationService as any).mockReturnValue({
224
+ createNotification: mockCreateNotification,
225
+ });
226
+
227
+ await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
228
+
229
+ expect(mockCreateNotification).toHaveBeenCalledTimes(1);
230
+ const callArgs = mockCreateNotification.mock.calls[0][0];
231
+
232
+ expect(callArgs.contextParameters.attachmentCount).toBe(0);
233
+ expect(callArgs.attachments).toBeDefined();
234
+ expect(callArgs.attachments.length).toBe(0);
235
+ });
236
+
237
+ it('should handle attachment conversion failures gracefully', async () => {
238
+ const practitioner = await medplum.createResource<Practitioner>({
239
+ resourceType: 'Practitioner',
240
+ name: [{ given: ['Alice'], family: 'Williams' }],
241
+ });
242
+
243
+ // Create media resource with invalid binary reference
244
+ const media = await medplum.createResource<Media>({
245
+ resourceType: 'Media',
246
+ status: 'completed',
247
+ content: {
248
+ contentType: 'application/pdf',
249
+ url: 'Binary/nonexistent-binary-id',
250
+ title: 'invalid.pdf',
251
+ },
252
+ });
253
+
254
+ const task = await medplum.createResource<Task>({
255
+ resourceType: 'Task',
256
+ status: 'requested',
257
+ intent: 'order',
258
+ description: 'Task with invalid attachment',
259
+ code: { text: 'Test Task' },
260
+ owner: { reference: `Practitioner/${practitioner.id}` },
261
+ input: [
262
+ {
263
+ type: {
264
+ coding: [
265
+ {
266
+ system: 'http://your-app-url.com/task-input-types',
267
+ code: 'attachment',
268
+ display: 'File Attachment',
269
+ },
270
+ ],
271
+ },
272
+ valueReference: { reference: `Media/${media.id}` },
273
+ },
274
+ ],
275
+ });
276
+
277
+ const { getNotificationService } = await import('../../../lib/notification-service');
278
+ const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
279
+ (getNotificationService as any).mockReturnValue({
280
+ createNotification: mockCreateNotification,
281
+ });
282
+
283
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
284
+
285
+ await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
286
+
287
+ // Email should still be sent with the attachment reference
288
+ // The actual file retrieval happens later when VintaSend processes the notification
289
+ expect(mockCreateNotification).toHaveBeenCalledTimes(1);
290
+ const callArgs = mockCreateNotification.mock.calls[0][0];
291
+
292
+ expect(callArgs.contextParameters.attachmentCount).toBe(1);
293
+ expect(callArgs.attachments.length).toBe(1);
294
+ expect(callArgs.attachments[0].fileId).toBe(media.id);
295
+
296
+ consoleErrorSpy.mockRestore();
297
+ });
298
+
299
+ it('should skip email for tasks assigned to groups', async () => {
300
+ const task = await medplum.createResource<Task>({
301
+ resourceType: 'Task',
302
+ status: 'requested',
303
+ intent: 'order',
304
+ description: 'Task assigned to group',
305
+ code: { text: 'Group Task' },
306
+ owner: { reference: 'Group/test-group-123' },
307
+ });
308
+
309
+ const { getNotificationService } = await import('../../../lib/notification-service');
310
+ const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
311
+ (getNotificationService as any).mockReturnValue({
312
+ createNotification: mockCreateNotification,
313
+ });
314
+
315
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
316
+
317
+ await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
318
+
319
+ expect(mockCreateNotification).not.toHaveBeenCalled();
320
+ expect(consoleLogSpy).toHaveBeenCalledWith(
321
+ '[sendTaskAssignmentEmail] Task assigned to Group, skipping email notification'
322
+ );
323
+
324
+ consoleLogSpy.mockRestore();
325
+ });
326
+
327
+ it('should handle urgent tasks with attachments', async () => {
328
+ const practitioner = await medplum.createResource<Practitioner>({
329
+ resourceType: 'Practitioner',
330
+ name: [{ given: ['Charlie'], family: 'Brown' }],
331
+ });
332
+
333
+ const binary = await medplum.createResource<Binary>({
334
+ resourceType: 'Binary',
335
+ contentType: 'application/pdf',
336
+ data: Buffer.from('urgent document').toString('base64'),
337
+ });
338
+
339
+ const media = await medplum.createResource<Media>({
340
+ resourceType: 'Media',
341
+ status: 'completed',
342
+ content: {
343
+ contentType: 'application/pdf',
344
+ url: `Binary/${binary.id}`,
345
+ title: 'urgent.pdf',
346
+ },
347
+ });
348
+
349
+ const task = await medplum.createResource<Task>({
350
+ resourceType: 'Task',
351
+ status: 'requested',
352
+ intent: 'order',
353
+ description: 'Urgent task with attachment',
354
+ code: { text: 'Urgent Review' },
355
+ owner: { reference: `Practitioner/${practitioner.id}` },
356
+ priority: 'urgent',
357
+ input: [
358
+ {
359
+ type: {
360
+ coding: [
361
+ {
362
+ system: 'http://your-app-url.com/task-input-types',
363
+ code: 'attachment',
364
+ display: 'File Attachment',
365
+ },
366
+ ],
367
+ },
368
+ valueReference: { reference: `Media/${media.id}` },
369
+ },
370
+ ],
371
+ });
372
+
373
+ const { getNotificationService } = await import('../../../lib/notification-service');
374
+ const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
375
+ (getNotificationService as any).mockReturnValue({
376
+ createNotification: mockCreateNotification,
377
+ });
378
+
379
+ await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
380
+
381
+ expect(mockCreateNotification).toHaveBeenCalledTimes(1);
382
+ const callArgs = mockCreateNotification.mock.calls[0][0];
383
+
384
+ expect(callArgs.contextParameters.taskIsUrgent).toBe(true);
385
+ expect(callArgs.contextParameters.attachmentCount).toBe(1);
386
+ expect(callArgs.attachments.length).toBe(1);
387
+ });
388
+ });