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,1137 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import type {
4
+ Coding,
5
+ Patient,
6
+ Questionnaire,
7
+ QuestionnaireResponse,
8
+ QuestionnaireResponseItemAnswer,
9
+ Reference,
10
+ Organization,
11
+ } from '@medplum/fhirtypes';
12
+ import { MockClient } from '@medplum/mock';
13
+ import { describe, expect, test, beforeEach, vi } from 'vitest';
14
+ import {
15
+ addAllergy,
16
+ addCondition,
17
+ addConsent,
18
+ addCoverage,
19
+ addExtension,
20
+ addFamilyMemberHistory,
21
+ addImmunization,
22
+ addLanguage,
23
+ addMedication,
24
+ addPharmacy,
25
+ convertDateToDateTime,
26
+ findQuestionnaireItem,
27
+ getGroupRepeatedAnswers,
28
+ getHumanName,
29
+ getPatientAddress,
30
+ observationCategoryMapping,
31
+ observationCodeMapping,
32
+ upsertObservation,
33
+ } from './intake-utils';
34
+
35
+ describe('intake utils', () => {
36
+ let medplum: MockClient;
37
+ let patient: Patient;
38
+
39
+ beforeEach(() => {
40
+ medplum = new MockClient();
41
+ patient = { resourceType: 'Patient', id: 'patient-1' };
42
+ });
43
+
44
+ describe('addExtension', () => {
45
+ test('adds coded extension with sub extension text', () => {
46
+ const answer: QuestionnaireResponseItemAnswer = {
47
+ valueCoding: { system: 'http://example.com', code: 'code', display: 'Display' },
48
+ };
49
+
50
+ addExtension(patient, 'http://example.com/ext', 'valueCoding', answer, 'ombCategory');
51
+
52
+ expect(patient.extension).toEqual([
53
+ {
54
+ url: 'http://example.com/ext',
55
+ extension: [
56
+ {
57
+ url: 'ombCategory',
58
+ valueCoding: { system: 'http://example.com', code: 'code', display: 'Display' },
59
+ },
60
+ {
61
+ url: 'text',
62
+ valueString: 'Display',
63
+ },
64
+ ],
65
+ },
66
+ ]);
67
+ });
68
+
69
+ test('adds boolean extension and interprets undefined as false', () => {
70
+ addExtension(patient, 'http://example.com/bool', 'valueBoolean', {});
71
+
72
+ expect(patient.extension).toEqual([
73
+ expect.objectContaining({
74
+ url: 'http://example.com/bool',
75
+ valueBoolean: false,
76
+ }),
77
+ ]);
78
+ });
79
+
80
+ test('adds extension without sub extension', () => {
81
+ const answer: QuestionnaireResponseItemAnswer = {
82
+ valueCoding: { system: 'http://example.com', code: 'code' },
83
+ };
84
+
85
+ addExtension(patient, 'http://example.com/ext', 'valueCoding', answer);
86
+
87
+ expect(patient.extension).toEqual([
88
+ {
89
+ url: 'http://example.com/ext',
90
+ valueCoding: { system: 'http://example.com', code: 'code' },
91
+ },
92
+ ]);
93
+ });
94
+
95
+ test('adds extension with sub extension but no display text when display is missing', () => {
96
+ const answer: QuestionnaireResponseItemAnswer = {
97
+ valueCoding: { system: 'http://example.com', code: 'code' },
98
+ };
99
+
100
+ addExtension(patient, 'http://example.com/ext', 'valueCoding', answer, 'ombCategory');
101
+
102
+ expect(patient.extension).toEqual([
103
+ {
104
+ url: 'http://example.com/ext',
105
+ extension: [
106
+ {
107
+ url: 'ombCategory',
108
+ valueCoding: { system: 'http://example.com', code: 'code' },
109
+ },
110
+ ],
111
+ },
112
+ ]);
113
+ });
114
+
115
+ test('returns early when value is undefined', () => {
116
+ const initialExtensions = patient.extension;
117
+ addExtension(patient, 'http://example.com/ext', 'valueCoding', undefined);
118
+ expect(patient.extension).toBe(initialExtensions);
119
+ });
120
+
121
+ test('adds boolean extension with true value', () => {
122
+ addExtension(patient, 'http://example.com/bool', 'valueBoolean', { valueBoolean: true });
123
+ expect(patient.extension).toEqual([
124
+ expect.objectContaining({
125
+ url: 'http://example.com/bool',
126
+ valueBoolean: true,
127
+ }),
128
+ ]);
129
+ });
130
+ });
131
+
132
+ describe('addLanguage', () => {
133
+ test('adds and updates preferred language', () => {
134
+ const coding: Coding = { system: 'urn:ietf:bcp:47', code: 'en', display: 'English' };
135
+
136
+ addLanguage(patient, coding);
137
+ addLanguage(patient, coding, true);
138
+
139
+ expect(patient.communication).toHaveLength(1);
140
+ expect(patient.communication?.[0].preferred).toBe(true);
141
+ });
142
+
143
+ test('returns early when coding is undefined', () => {
144
+ const initialCommunication = patient.communication;
145
+ addLanguage(patient, undefined);
146
+ expect(patient.communication).toBe(initialCommunication);
147
+ });
148
+
149
+ test('adds new language when not present', () => {
150
+ const coding: Coding = { system: 'urn:ietf:bcp:47', code: 'es', display: 'Spanish' };
151
+ addLanguage(patient, coding);
152
+ expect(patient.communication).toHaveLength(1);
153
+ expect(patient.communication?.[0].language.coding?.[0].code).toBe('es');
154
+ });
155
+
156
+ test('sets preferred flag on existing language', () => {
157
+ const coding: Coding = { system: 'urn:ietf:bcp:47', code: 'en', display: 'English' };
158
+ patient.communication = [
159
+ {
160
+ language: {
161
+ coding: [{ system: 'urn:ietf:bcp:47', code: 'en', display: 'English' }],
162
+ },
163
+ },
164
+ ];
165
+ addLanguage(patient, coding, true);
166
+ expect(patient.communication?.[0].preferred).toBe(true);
167
+ });
168
+ });
169
+
170
+ describe('questionnaire helpers', () => {
171
+ test('getHumanName builds full name', () => {
172
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
173
+ 'first-name': { valueString: 'Ada' },
174
+ 'middle-name': { valueString: 'M.' },
175
+ 'last-name': { valueString: 'Lovelace' },
176
+ };
177
+ expect(getHumanName(answers)).toEqual({ given: ['Ada', 'M.'], family: 'Lovelace' });
178
+ });
179
+
180
+ test('getHumanName builds name with prefix', () => {
181
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
182
+ 'related-person-first-name': { valueString: 'John' },
183
+ 'related-person-last-name': { valueString: 'Doe' },
184
+ };
185
+ expect(getHumanName(answers, 'related-person-')).toEqual({ given: ['John'], family: 'Doe' });
186
+ });
187
+
188
+ test('getHumanName returns undefined when no name fields present', () => {
189
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {};
190
+ expect(getHumanName(answers)).toBeUndefined();
191
+ });
192
+
193
+ test('getHumanName builds name with only first name', () => {
194
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
195
+ 'first-name': { valueString: 'Ada' },
196
+ };
197
+ expect(getHumanName(answers)).toEqual({ given: ['Ada'] });
198
+ });
199
+
200
+ test('getHumanName builds name with only last name', () => {
201
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
202
+ 'last-name': { valueString: 'Lovelace' },
203
+ };
204
+ expect(getHumanName(answers)).toEqual({ family: 'Lovelace' });
205
+ });
206
+
207
+ test('getPatientAddress builds address', () => {
208
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
209
+ street: { valueString: '1 Main St' },
210
+ city: { valueString: 'Springfield' },
211
+ state: { valueCoding: { code: 'CA' } },
212
+ zip: { valueString: '12345' },
213
+ };
214
+ expect(getPatientAddress(answers)).toEqual(
215
+ expect.objectContaining({ city: 'Springfield', state: 'CA', postalCode: '12345', use: 'home' })
216
+ );
217
+ });
218
+
219
+ test('getPatientAddress returns undefined when no address fields present', () => {
220
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {};
221
+ expect(getPatientAddress(answers)).toBeUndefined();
222
+ });
223
+
224
+ test('getPatientAddress builds partial address', () => {
225
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
226
+ city: { valueString: 'Springfield' },
227
+ };
228
+ expect(getPatientAddress(answers)).toEqual(
229
+ expect.objectContaining({ city: 'Springfield', use: 'home', type: 'physical' })
230
+ );
231
+ });
232
+
233
+ test('findQuestionnaireItem finds nested item', () => {
234
+ const questionnaire: Questionnaire = {
235
+ resourceType: 'Questionnaire',
236
+ item: [
237
+ {
238
+ linkId: 'group',
239
+ type: 'group',
240
+ item: [{ linkId: 'nested', type: 'string' }],
241
+ },
242
+ ],
243
+ status: 'active',
244
+ };
245
+ const result = findQuestionnaireItem(questionnaire.item, 'nested');
246
+ expect(result?.linkId).toBe('nested');
247
+ });
248
+
249
+ test('getGroupRepeatedAnswers flattens repeating groups', () => {
250
+ const questionnaire: Questionnaire = {
251
+ status: 'active',
252
+ resourceType: 'Questionnaire',
253
+ item: [{ linkId: 'allergies', type: 'group', item: [{ linkId: 'allergy-substance', type: 'string' }] }],
254
+ };
255
+ const response: QuestionnaireResponse = {
256
+ status: 'completed',
257
+ resourceType: 'QuestionnaireResponse',
258
+ item: [
259
+ {
260
+ linkId: 'allergies',
261
+ item: [{ linkId: 'allergy-substance', answer: [{ valueString: 'Peanuts' }] }],
262
+ },
263
+ {
264
+ linkId: 'allergies',
265
+ item: [{ linkId: 'allergy-substance', answer: [{ valueString: 'Shellfish' }] }],
266
+ },
267
+ ],
268
+ };
269
+
270
+ const answers = getGroupRepeatedAnswers(questionnaire, response, 'allergies');
271
+ expect(answers).toEqual([
272
+ { 'allergy-substance': { valueString: 'Peanuts' } },
273
+ { 'allergy-substance': { valueString: 'Shellfish' } },
274
+ ]);
275
+ });
276
+
277
+ test('getGroupRepeatedAnswers returns empty array when no response groups found', () => {
278
+ const questionnaire: Questionnaire = {
279
+ status: 'active',
280
+ resourceType: 'Questionnaire',
281
+ item: [{ linkId: 'allergies', type: 'group', item: [{ linkId: 'allergy-substance', type: 'string' }] }],
282
+ };
283
+ const response: QuestionnaireResponse = {
284
+ status: 'completed',
285
+ resourceType: 'QuestionnaireResponse',
286
+ item: [],
287
+ };
288
+
289
+ // When no response items match the groupLinkId, returns empty array
290
+ const answers = getGroupRepeatedAnswers(questionnaire, response, 'allergies');
291
+ expect(answers).toEqual([]);
292
+ });
293
+
294
+ test('getGroupRepeatedAnswers returns empty array when questionnaire item is not a group', () => {
295
+ const questionnaire: Questionnaire = {
296
+ status: 'active',
297
+ resourceType: 'Questionnaire',
298
+ item: [{ linkId: 'allergies', type: 'string' }],
299
+ };
300
+ const response: QuestionnaireResponse = {
301
+ status: 'completed',
302
+ resourceType: 'QuestionnaireResponse',
303
+ item: [{ linkId: 'allergies' }],
304
+ };
305
+
306
+ const answers = getGroupRepeatedAnswers(questionnaire, response, 'allergies');
307
+ expect(answers).toEqual([]);
308
+ });
309
+
310
+ test('getGroupRepeatedAnswers handles nested subgroups', () => {
311
+ const questionnaire: Questionnaire = {
312
+ status: 'active',
313
+ resourceType: 'Questionnaire',
314
+ item: [
315
+ {
316
+ linkId: 'group',
317
+ type: 'group',
318
+ item: [
319
+ { linkId: 'field1', type: 'string' },
320
+ {
321
+ linkId: 'subgroup',
322
+ type: 'group',
323
+ item: [{ linkId: 'subfield1', type: 'string' }],
324
+ },
325
+ ],
326
+ },
327
+ ],
328
+ };
329
+ const response: QuestionnaireResponse = {
330
+ status: 'completed',
331
+ resourceType: 'QuestionnaireResponse',
332
+ item: [
333
+ {
334
+ linkId: 'group',
335
+ item: [
336
+ { linkId: 'field1', answer: [{ valueString: 'value1' }] },
337
+ {
338
+ linkId: 'subgroup',
339
+ item: [{ linkId: 'subfield1', answer: [{ valueString: 'subvalue1' }] }],
340
+ },
341
+ ],
342
+ },
343
+ ],
344
+ };
345
+
346
+ const answers = getGroupRepeatedAnswers(questionnaire, response, 'group');
347
+ expect(answers).toEqual([
348
+ {
349
+ field1: { valueString: 'value1' },
350
+ subgroup: { subfield1: { valueString: 'subvalue1' } },
351
+ },
352
+ ]);
353
+ });
354
+
355
+ test('findQuestionnaireItem returns undefined when item not found', () => {
356
+ const questionnaire: Questionnaire = {
357
+ resourceType: 'Questionnaire',
358
+ item: [{ linkId: 'other', type: 'string' }],
359
+ status: 'active',
360
+ };
361
+ const result = findQuestionnaireItem(questionnaire.item, 'not-found');
362
+ expect(result).toBeUndefined();
363
+ });
364
+
365
+ test('findQuestionnaireItem returns undefined when items is undefined', () => {
366
+ const result = findQuestionnaireItem(undefined, 'any');
367
+ expect(result).toBeUndefined();
368
+ });
369
+
370
+ test('findQuestionnaireItem handles undefined currentItem in reduce', () => {
371
+ const questionnaire: Questionnaire = {
372
+ resourceType: 'Questionnaire',
373
+ item: [undefined as any, { linkId: 'found', type: 'string' }],
374
+ status: 'active',
375
+ };
376
+ const result = findQuestionnaireItem(questionnaire.item, 'found');
377
+ expect(result?.linkId).toBe('found');
378
+ });
379
+ });
380
+
381
+ describe('convertDateToDateTime', () => {
382
+ test('converts date to ISO string', () => {
383
+ expect(convertDateToDateTime('2020-01-01')).toContain('2020-01-01T00:00:00');
384
+ expect(convertDateToDateTime(undefined)).toBeUndefined();
385
+ });
386
+ });
387
+
388
+ describe('upsertObservation', () => {
389
+ test('upserts codeable concept observation', async () => {
390
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
391
+ await upsertObservation(
392
+ medplum as any,
393
+ patient,
394
+ observationCodeMapping.smokingStatus,
395
+ observationCategoryMapping.socialHistory,
396
+ 'valueCodeableConcept',
397
+ { valueCoding: { system: 'http://example.com', code: 'never' } }
398
+ );
399
+ expect(upsertSpy).toHaveBeenCalled();
400
+ });
401
+
402
+ test('skips when no value provided', async () => {
403
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource');
404
+ await upsertObservation(
405
+ medplum as any,
406
+ patient,
407
+ observationCodeMapping.smokingStatus,
408
+ observationCategoryMapping.socialHistory,
409
+ 'valueCodeableConcept',
410
+ undefined
411
+ );
412
+ expect(upsertSpy).not.toHaveBeenCalled();
413
+ });
414
+
415
+ test('skips when no code provided', async () => {
416
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource');
417
+ // The function checks for !value || !code early, but if code is provided without coding,
418
+ // it will try to access coding[0] which causes an error
419
+ // So we test with undefined code to trigger the early return
420
+ await upsertObservation(
421
+ medplum as any,
422
+ patient,
423
+ undefined as any,
424
+ observationCategoryMapping.socialHistory,
425
+ 'valueCodeableConcept',
426
+ { valueCoding: { system: 'http://example.com', code: 'never' } }
427
+ );
428
+ expect(upsertSpy).not.toHaveBeenCalled();
429
+ });
430
+
431
+ test('upserts dateTime observation', async () => {
432
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
433
+ await upsertObservation(
434
+ medplum as any,
435
+ patient,
436
+ observationCodeMapping.estimatedDeliveryDate,
437
+ observationCategoryMapping.socialHistory,
438
+ 'valueDateTime',
439
+ { valueDateTime: '2024-12-31T00:00:00Z' }
440
+ );
441
+ expect(upsertSpy).toHaveBeenCalledWith(
442
+ expect.objectContaining({
443
+ valueDateTime: '2024-12-31T00:00:00Z',
444
+ }),
445
+ expect.any(Object)
446
+ );
447
+ });
448
+
449
+ test('adds profile URL when provided', async () => {
450
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
451
+ await upsertObservation(
452
+ medplum as any,
453
+ patient,
454
+ observationCodeMapping.smokingStatus,
455
+ observationCategoryMapping.socialHistory,
456
+ 'valueCodeableConcept',
457
+ { valueCoding: { system: 'http://example.com', code: 'never' } },
458
+ 'http://example.com/profile'
459
+ );
460
+ expect(upsertSpy).toHaveBeenCalledWith(
461
+ expect.objectContaining({
462
+ meta: expect.objectContaining({
463
+ profile: ['http://example.com/profile'],
464
+ }),
465
+ }),
466
+ expect.any(Object)
467
+ );
468
+ });
469
+ });
470
+
471
+ describe('resource helpers', () => {
472
+ test('addAllergy returns early without substance', async () => {
473
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource');
474
+ await addAllergy(medplum as any, patient, {});
475
+ expect(upsertSpy).not.toHaveBeenCalled();
476
+ });
477
+
478
+ test('addAllergy upserts when code present', async () => {
479
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
480
+ await addAllergy(medplum as any, patient, {
481
+ 'allergy-substance': { valueCoding: { system: 'http://example.com', code: 'peanut' } },
482
+ });
483
+ expect(upsertSpy).toHaveBeenCalledWith(
484
+ expect.objectContaining({
485
+ resourceType: 'AllergyIntolerance',
486
+ }),
487
+ expect.objectContaining({
488
+ patient: `Patient/${patient.id}`,
489
+ })
490
+ );
491
+ });
492
+
493
+ test('addAllergy includes reaction when provided', async () => {
494
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
495
+ await addAllergy(medplum as any, patient, {
496
+ 'allergy-substance': { valueCoding: { system: 'http://example.com', code: 'peanut' } },
497
+ 'allergy-reaction': { valueString: 'Hives' },
498
+ });
499
+ expect(upsertSpy).toHaveBeenCalledWith(
500
+ expect.objectContaining({
501
+ reaction: [{ manifestation: [{ text: 'Hives' }] }],
502
+ }),
503
+ expect.any(Object)
504
+ );
505
+ });
506
+
507
+ test('addAllergy includes onsetDateTime when provided', async () => {
508
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
509
+ await addAllergy(medplum as any, patient, {
510
+ 'allergy-substance': { valueCoding: { system: 'http://example.com', code: 'peanut' } },
511
+ 'allergy-onset': { valueDateTime: '2020-01-01T00:00:00Z' },
512
+ });
513
+ expect(upsertSpy).toHaveBeenCalledWith(
514
+ expect.objectContaining({
515
+ onsetDateTime: '2020-01-01T00:00:00Z',
516
+ }),
517
+ expect.any(Object)
518
+ );
519
+ });
520
+
521
+ test('addCoverage upserts coverage resource', async () => {
522
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
523
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
524
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
525
+ 'subscriber-id': { valueString: 'sub-1' },
526
+ 'relationship-to-subscriber': { valueCoding: { code: 'self' } },
527
+ };
528
+
529
+ await addCoverage(medplum as any, patient, answers);
530
+
531
+ expect(upsertSpy).toHaveBeenCalledWith(
532
+ expect.objectContaining({
533
+ resourceType: 'Coverage',
534
+ beneficiary: expect.objectContaining({ reference: `Patient/${patient.id}` }),
535
+ }),
536
+ expect.objectContaining({
537
+ beneficiary: `Patient/${patient.id}`,
538
+ })
539
+ );
540
+ });
541
+
542
+ test('addConsent creates resource with provided scope', async () => {
543
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({} as any);
544
+ await addConsent(
545
+ medplum as any,
546
+ patient,
547
+ true,
548
+ observationCategoryMapping.socialHistory,
549
+ observationCategoryMapping.sdoh,
550
+ undefined,
551
+ '2020-01-01'
552
+ );
553
+ expect(createSpy).toHaveBeenCalledWith(
554
+ expect.objectContaining({
555
+ resourceType: 'Consent',
556
+ patient: expect.objectContaining({ reference: `Patient/${patient.id}` }),
557
+ status: 'active',
558
+ dateTime: '2020-01-01',
559
+ })
560
+ );
561
+ });
562
+
563
+ test('addConsent creates resource with rejected status when consentGiven is false', async () => {
564
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({} as any);
565
+ await addConsent(
566
+ medplum as any,
567
+ patient,
568
+ false,
569
+ observationCategoryMapping.socialHistory,
570
+ observationCategoryMapping.sdoh,
571
+ undefined,
572
+ '2020-01-01'
573
+ );
574
+ expect(createSpy).toHaveBeenCalledWith(
575
+ expect.objectContaining({
576
+ status: 'rejected',
577
+ })
578
+ );
579
+ });
580
+
581
+ test('addConsent includes policyRule when provided', async () => {
582
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({} as any);
583
+ const policyRule = { coding: [{ code: 'hipaa-npp' }] };
584
+ await addConsent(
585
+ medplum as any,
586
+ patient,
587
+ true,
588
+ observationCategoryMapping.socialHistory,
589
+ observationCategoryMapping.sdoh,
590
+ policyRule,
591
+ '2020-01-01'
592
+ );
593
+ expect(createSpy).toHaveBeenCalledWith(
594
+ expect.objectContaining({
595
+ policyRule: policyRule,
596
+ })
597
+ );
598
+ });
599
+
600
+ test('addMedication returns early without code', async () => {
601
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource');
602
+ await addMedication(medplum as any, patient, {});
603
+ expect(upsertSpy).not.toHaveBeenCalled();
604
+ });
605
+
606
+ test('addMedication upserts when code present', async () => {
607
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
608
+ await addMedication(medplum as any, patient, {
609
+ 'medication-code': { valueCoding: { system: 'http://example.com', code: 'aspirin' } },
610
+ });
611
+ expect(upsertSpy).toHaveBeenCalledWith(
612
+ expect.objectContaining({
613
+ resourceType: 'MedicationRequest',
614
+ status: 'active',
615
+ intent: 'order',
616
+ }),
617
+ expect.any(Object)
618
+ );
619
+ });
620
+
621
+ test('addMedication includes note when provided', async () => {
622
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
623
+ await addMedication(medplum as any, patient, {
624
+ 'medication-code': { valueCoding: { system: 'http://example.com', code: 'aspirin' } },
625
+ 'medication-note': { valueString: 'Take with food' },
626
+ });
627
+ expect(upsertSpy).toHaveBeenCalledWith(
628
+ expect.objectContaining({
629
+ note: [{ text: 'Take with food' }],
630
+ }),
631
+ expect.any(Object)
632
+ );
633
+ });
634
+
635
+ test('addCondition returns early without code', async () => {
636
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource');
637
+ await addCondition(medplum as any, patient, {});
638
+ expect(upsertSpy).not.toHaveBeenCalled();
639
+ });
640
+
641
+ test('addCondition upserts when code present', async () => {
642
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
643
+ await addCondition(medplum as any, patient, {
644
+ 'medical-history-problem': { valueCoding: { system: 'http://example.com', code: 'diabetes' } },
645
+ });
646
+ expect(upsertSpy).toHaveBeenCalledWith(
647
+ expect.objectContaining({
648
+ resourceType: 'Condition',
649
+ }),
650
+ expect.any(Object)
651
+ );
652
+ });
653
+
654
+ test('addCondition includes clinicalStatus when provided', async () => {
655
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
656
+ await addCondition(medplum as any, patient, {
657
+ 'medical-history-problem': { valueCoding: { system: 'http://example.com', code: 'diabetes' } },
658
+ 'medical-history-clinical-status': { valueCoding: { system: 'http://example.com', code: 'active' } },
659
+ });
660
+ expect(upsertSpy).toHaveBeenCalledWith(
661
+ expect.objectContaining({
662
+ clinicalStatus: { coding: [{ system: 'http://example.com', code: 'active' }] },
663
+ }),
664
+ expect.any(Object)
665
+ );
666
+ });
667
+
668
+ test('addCondition includes onsetDateTime when provided', async () => {
669
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
670
+ await addCondition(medplum as any, patient, {
671
+ 'medical-history-problem': { valueCoding: { system: 'http://example.com', code: 'diabetes' } },
672
+ 'medical-history-onset': { valueDateTime: '2020-01-01T00:00:00Z' },
673
+ });
674
+ expect(upsertSpy).toHaveBeenCalledWith(
675
+ expect.objectContaining({
676
+ onsetDateTime: '2020-01-01T00:00:00Z',
677
+ }),
678
+ expect.any(Object)
679
+ );
680
+ });
681
+
682
+ test('addFamilyMemberHistory returns early without condition or relationship', async () => {
683
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource');
684
+ await addFamilyMemberHistory(medplum as any, patient, {
685
+ 'family-member-history-problem': { valueCoding: { system: 'http://example.com', code: 'diabetes' } },
686
+ });
687
+ expect(upsertSpy).not.toHaveBeenCalled();
688
+ });
689
+
690
+ test('addFamilyMemberHistory upserts when condition and relationship present', async () => {
691
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
692
+ await addFamilyMemberHistory(medplum as any, patient, {
693
+ 'family-member-history-problem': { valueCoding: { system: 'http://example.com', code: 'diabetes' } },
694
+ 'family-member-history-relationship': { valueCoding: { system: 'http://example.com', code: 'mother' } },
695
+ });
696
+ expect(upsertSpy).toHaveBeenCalledWith(
697
+ expect.objectContaining({
698
+ resourceType: 'FamilyMemberHistory',
699
+ status: 'completed',
700
+ }),
701
+ expect.any(Object)
702
+ );
703
+ });
704
+
705
+ test('addFamilyMemberHistory includes deceasedBoolean when provided', async () => {
706
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
707
+ await addFamilyMemberHistory(medplum as any, patient, {
708
+ 'family-member-history-problem': { valueCoding: { system: 'http://example.com', code: 'diabetes' } },
709
+ 'family-member-history-relationship': { valueCoding: { system: 'http://example.com', code: 'mother' } },
710
+ 'family-member-history-deceased': { valueBoolean: true },
711
+ });
712
+ expect(upsertSpy).toHaveBeenCalledWith(
713
+ expect.objectContaining({
714
+ deceasedBoolean: true,
715
+ }),
716
+ expect.any(Object)
717
+ );
718
+ });
719
+
720
+ test('addImmunization returns early without code or date', async () => {
721
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource');
722
+ await addImmunization(medplum as any, patient, {
723
+ 'immunization-vaccine': { valueCoding: { system: 'http://example.com', code: 'flu' } },
724
+ });
725
+ expect(upsertSpy).not.toHaveBeenCalled();
726
+ });
727
+
728
+ test('addImmunization upserts when code and date present', async () => {
729
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
730
+ await addImmunization(medplum as any, patient, {
731
+ 'immunization-vaccine': { valueCoding: { system: 'http://example.com', code: 'flu' } },
732
+ 'immunization-date': { valueDateTime: '2024-01-01T00:00:00Z' },
733
+ });
734
+ expect(upsertSpy).toHaveBeenCalledWith(
735
+ expect.objectContaining({
736
+ resourceType: 'Immunization',
737
+ status: 'completed',
738
+ }),
739
+ expect.any(Object)
740
+ );
741
+ });
742
+
743
+ test('addPharmacy upserts CareTeam with pharmacy', async () => {
744
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
745
+ const pharmacy: Reference<Organization> = { reference: 'Organization/pharmacy-1' };
746
+ await addPharmacy(medplum as any, patient, pharmacy);
747
+ expect(upsertSpy).toHaveBeenCalledWith(
748
+ expect.objectContaining({
749
+ resourceType: 'CareTeam',
750
+ status: 'proposed',
751
+ name: 'Patient Preferred Pharmacy',
752
+ }),
753
+ expect.objectContaining({
754
+ name: 'Patient Preferred Pharmacy',
755
+ subject: `Patient/${patient.id}`,
756
+ })
757
+ );
758
+ });
759
+
760
+ test('addCoverage creates RelatedPerson when relationship requires it', async () => {
761
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({} as any);
762
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
763
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
764
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
765
+ 'subscriber-id': { valueString: 'sub-1' },
766
+ 'relationship-to-subscriber': { valueCoding: { code: 'child', system: 'http://example.com' } },
767
+ 'related-person': {
768
+ 'related-person-first-name': { valueString: 'John' },
769
+ 'related-person-last-name': { valueString: 'Doe' },
770
+ } as any,
771
+ };
772
+
773
+ await addCoverage(medplum as any, patient, answers);
774
+
775
+ expect(createSpy).toHaveBeenCalledWith(
776
+ expect.objectContaining({
777
+ resourceType: 'RelatedPerson',
778
+ })
779
+ );
780
+ expect(upsertSpy).toHaveBeenCalled();
781
+ });
782
+
783
+ test('addCoverage does not create RelatedPerson for self relationship', async () => {
784
+ const createSpy = vi.spyOn(medplum, 'createResource');
785
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
786
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
787
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
788
+ 'subscriber-id': { valueString: 'sub-1' },
789
+ 'relationship-to-subscriber': { valueCoding: { code: 'self', system: 'http://example.com' } },
790
+ };
791
+
792
+ await addCoverage(medplum as any, patient, answers);
793
+
794
+ expect(createSpy).not.toHaveBeenCalled();
795
+ expect(upsertSpy).toHaveBeenCalled();
796
+ });
797
+
798
+ test('addCoverage does not create RelatedPerson for other relationship', async () => {
799
+ const createSpy = vi.spyOn(medplum, 'createResource');
800
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
801
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
802
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
803
+ 'subscriber-id': { valueString: 'sub-1' },
804
+ 'relationship-to-subscriber': { valueCoding: { code: 'other', system: 'http://example.com' } },
805
+ };
806
+
807
+ await addCoverage(medplum as any, patient, answers);
808
+
809
+ expect(createSpy).not.toHaveBeenCalled();
810
+ expect(upsertSpy).toHaveBeenCalled();
811
+ });
812
+
813
+ test('addCoverage does not create RelatedPerson for injured relationship', async () => {
814
+ const createSpy = vi.spyOn(medplum, 'createResource');
815
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
816
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
817
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
818
+ 'subscriber-id': { valueString: 'sub-1' },
819
+ 'relationship-to-subscriber': { valueCoding: { code: 'injured', system: 'http://example.com' } },
820
+ };
821
+
822
+ await addCoverage(medplum as any, patient, answers);
823
+
824
+ expect(createSpy).not.toHaveBeenCalled();
825
+ expect(upsertSpy).toHaveBeenCalled();
826
+ });
827
+
828
+ test('addCoverage does not create RelatedPerson when relationship code is missing', async () => {
829
+ const createSpy = vi.spyOn(medplum, 'createResource');
830
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
831
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
832
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
833
+ 'subscriber-id': { valueString: 'sub-1' },
834
+ 'relationship-to-subscriber': { valueCoding: { system: 'http://example.com' } },
835
+ };
836
+
837
+ await addCoverage(medplum as any, patient, answers);
838
+
839
+ expect(createSpy).not.toHaveBeenCalled();
840
+ expect(upsertSpy).toHaveBeenCalled();
841
+ });
842
+
843
+ test('addCoverage does not create RelatedPerson when relatedPersonAnswers is missing', async () => {
844
+ const createSpy = vi.spyOn(medplum, 'createResource');
845
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
846
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
847
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
848
+ 'subscriber-id': { valueString: 'sub-1' },
849
+ 'relationship-to-subscriber': { valueCoding: { code: 'child', system: 'http://example.com' } },
850
+ };
851
+
852
+ await addCoverage(medplum as any, patient, answers);
853
+
854
+ expect(createSpy).not.toHaveBeenCalled();
855
+ expect(upsertSpy).toHaveBeenCalled();
856
+ });
857
+
858
+ test('addCoverage creates RelatedPerson with birthDate and gender', async () => {
859
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({} as any);
860
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
861
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
862
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
863
+ 'subscriber-id': { valueString: 'sub-1' },
864
+ 'relationship-to-subscriber': { valueCoding: { code: 'parent', system: 'http://example.com' } },
865
+ 'related-person': {
866
+ 'related-person-first-name': { valueString: 'John' },
867
+ 'related-person-last-name': { valueString: 'Doe' },
868
+ 'related-person-dob': { valueDate: '1980-01-01' },
869
+ 'related-person-gender-identity': { valueCoding: { code: 'male', system: 'http://example.com' } },
870
+ } as any,
871
+ };
872
+
873
+ await addCoverage(medplum as any, patient, answers);
874
+
875
+ expect(createSpy).toHaveBeenCalledWith(
876
+ expect.objectContaining({
877
+ resourceType: 'RelatedPerson',
878
+ birthDate: '1980-01-01',
879
+ gender: 'male',
880
+ })
881
+ );
882
+ expect(upsertSpy).toHaveBeenCalled();
883
+ });
884
+
885
+ test('addCoverage creates RelatedPerson with parent relationship', async () => {
886
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({} as any);
887
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
888
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
889
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
890
+ 'subscriber-id': { valueString: 'sub-1' },
891
+ 'relationship-to-subscriber': { valueCoding: { code: 'parent', system: 'http://example.com' } },
892
+ 'related-person': {
893
+ 'related-person-first-name': { valueString: 'John' },
894
+ 'related-person-last-name': { valueString: 'Doe' },
895
+ } as any,
896
+ };
897
+
898
+ await addCoverage(medplum as any, patient, answers);
899
+
900
+ expect(createSpy).toHaveBeenCalledWith(
901
+ expect.objectContaining({
902
+ resourceType: 'RelatedPerson',
903
+ relationship: [
904
+ {
905
+ coding: [
906
+ { system: 'http://terminology.hl7.org/CodeSystem/v3-RoleCode', code: 'CHILD', display: 'child' },
907
+ ],
908
+ },
909
+ ],
910
+ })
911
+ );
912
+ expect(upsertSpy).toHaveBeenCalled();
913
+ });
914
+
915
+ test('addCoverage creates RelatedPerson with spouse relationship', async () => {
916
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({} as any);
917
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
918
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
919
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
920
+ 'subscriber-id': { valueString: 'sub-1' },
921
+ 'relationship-to-subscriber': { valueCoding: { code: 'spouse', system: 'http://example.com' } },
922
+ 'related-person': {
923
+ 'related-person-first-name': { valueString: 'Jane' },
924
+ 'related-person-last-name': { valueString: 'Doe' },
925
+ } as any,
926
+ };
927
+
928
+ await addCoverage(medplum as any, patient, answers);
929
+
930
+ expect(createSpy).toHaveBeenCalledWith(
931
+ expect.objectContaining({
932
+ resourceType: 'RelatedPerson',
933
+ relationship: [
934
+ {
935
+ coding: [{ system: 'http://terminology.hl7.org/CodeSystem/v3-RoleCode', code: 'SPS', display: 'spouse' }],
936
+ },
937
+ ],
938
+ })
939
+ );
940
+ expect(upsertSpy).toHaveBeenCalled();
941
+ });
942
+
943
+ test('addCoverage creates RelatedPerson with common relationship (treated as spouse)', async () => {
944
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({} as any);
945
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
946
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
947
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
948
+ 'subscriber-id': { valueString: 'sub-1' },
949
+ 'relationship-to-subscriber': { valueCoding: { code: 'common', system: 'http://example.com' } },
950
+ 'related-person': {
951
+ 'related-person-first-name': { valueString: 'Jane' },
952
+ 'related-person-last-name': { valueString: 'Doe' },
953
+ } as any,
954
+ };
955
+
956
+ await addCoverage(medplum as any, patient, answers);
957
+
958
+ expect(createSpy).toHaveBeenCalledWith(
959
+ expect.objectContaining({
960
+ resourceType: 'RelatedPerson',
961
+ relationship: [
962
+ {
963
+ coding: [{ system: 'http://terminology.hl7.org/CodeSystem/v3-RoleCode', code: 'SPS', display: 'spouse' }],
964
+ },
965
+ ],
966
+ })
967
+ );
968
+ expect(upsertSpy).toHaveBeenCalled();
969
+ });
970
+
971
+ test('addCoverage creates RelatedPerson with undefined relationship when code not mapped', async () => {
972
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({} as any);
973
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
974
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
975
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
976
+ 'subscriber-id': { valueString: 'sub-1' },
977
+ 'relationship-to-subscriber': { valueCoding: { code: 'unknown-relationship', system: 'http://example.com' } },
978
+ 'related-person': {
979
+ 'related-person-first-name': { valueString: 'Jane' },
980
+ 'related-person-last-name': { valueString: 'Doe' },
981
+ } as any,
982
+ };
983
+
984
+ await addCoverage(medplum as any, patient, answers);
985
+
986
+ expect(createSpy).toHaveBeenCalledWith(
987
+ expect.objectContaining({
988
+ resourceType: 'RelatedPerson',
989
+ relationship: undefined,
990
+ })
991
+ );
992
+ expect(upsertSpy).toHaveBeenCalled();
993
+ });
994
+
995
+ test('addCoverage creates RelatedPerson without name when name is undefined', async () => {
996
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({} as any);
997
+ const upsertSpy = vi.spyOn(medplum, 'upsertResource').mockResolvedValue({} as any);
998
+ const answers: Record<string, QuestionnaireResponseItemAnswer> = {
999
+ 'insurance-provider': { valueReference: { reference: 'Organization/org-1' } as Reference<Organization> },
1000
+ 'subscriber-id': { valueString: 'sub-1' },
1001
+ 'relationship-to-subscriber': { valueCoding: { code: 'child', system: 'http://example.com' } },
1002
+ 'related-person': {} as any,
1003
+ };
1004
+
1005
+ await addCoverage(medplum as any, patient, answers);
1006
+
1007
+ expect(createSpy).toHaveBeenCalledWith(
1008
+ expect.objectContaining({
1009
+ resourceType: 'RelatedPerson',
1010
+ name: undefined,
1011
+ })
1012
+ );
1013
+ expect(upsertSpy).toHaveBeenCalled();
1014
+ });
1015
+ });
1016
+
1017
+ describe('getGroupRepeatedAnswers edge cases', () => {
1018
+ test('handles items with nested subgroups and answers', () => {
1019
+ const questionnaire: Questionnaire = {
1020
+ status: 'active',
1021
+ resourceType: 'Questionnaire',
1022
+ item: [
1023
+ {
1024
+ linkId: 'group',
1025
+ type: 'group',
1026
+ item: [
1027
+ { linkId: 'field1', type: 'string' },
1028
+ {
1029
+ linkId: 'subgroup',
1030
+ type: 'group',
1031
+ item: [{ linkId: 'subfield1', type: 'string' }],
1032
+ },
1033
+ ],
1034
+ },
1035
+ ],
1036
+ };
1037
+ const response: QuestionnaireResponse = {
1038
+ status: 'completed',
1039
+ resourceType: 'QuestionnaireResponse',
1040
+ item: [
1041
+ {
1042
+ linkId: 'group',
1043
+ item: [
1044
+ { linkId: 'field1', answer: [{ valueString: 'value1' }] },
1045
+ {
1046
+ linkId: 'subgroup',
1047
+ item: [{ linkId: 'subfield1', answer: [{ valueString: 'subvalue1' }] }],
1048
+ },
1049
+ ],
1050
+ },
1051
+ ],
1052
+ };
1053
+
1054
+ const answers = getGroupRepeatedAnswers(questionnaire, response, 'group');
1055
+ expect(answers).toEqual([
1056
+ {
1057
+ field1: { valueString: 'value1' },
1058
+ subgroup: { subfield1: { valueString: 'subvalue1' } },
1059
+ },
1060
+ ]);
1061
+ });
1062
+
1063
+ test('handles items without answers in nested subgroups', () => {
1064
+ const questionnaire: Questionnaire = {
1065
+ status: 'active',
1066
+ resourceType: 'Questionnaire',
1067
+ item: [
1068
+ {
1069
+ linkId: 'group',
1070
+ type: 'group',
1071
+ item: [
1072
+ {
1073
+ linkId: 'subgroup',
1074
+ type: 'group',
1075
+ item: [{ linkId: 'subfield1', type: 'string' }],
1076
+ },
1077
+ ],
1078
+ },
1079
+ ],
1080
+ };
1081
+ const response: QuestionnaireResponse = {
1082
+ status: 'completed',
1083
+ resourceType: 'QuestionnaireResponse',
1084
+ item: [
1085
+ {
1086
+ linkId: 'group',
1087
+ item: [
1088
+ {
1089
+ linkId: 'subgroup',
1090
+ item: [{ linkId: 'subfield1' }], // No answer
1091
+ },
1092
+ ],
1093
+ },
1094
+ ],
1095
+ };
1096
+
1097
+ const answers = getGroupRepeatedAnswers(questionnaire, response, 'group');
1098
+ // When there's no answer, it returns an empty object
1099
+ expect(answers).toEqual([
1100
+ {
1101
+ subgroup: {},
1102
+ },
1103
+ ]);
1104
+ });
1105
+
1106
+ test('handles items with empty answer arrays', () => {
1107
+ const questionnaire: Questionnaire = {
1108
+ status: 'active',
1109
+ resourceType: 'Questionnaire',
1110
+ item: [
1111
+ {
1112
+ linkId: 'group',
1113
+ type: 'group',
1114
+ item: [{ linkId: 'field1', type: 'string' }],
1115
+ },
1116
+ ],
1117
+ };
1118
+ const response: QuestionnaireResponse = {
1119
+ status: 'completed',
1120
+ resourceType: 'QuestionnaireResponse',
1121
+ item: [
1122
+ {
1123
+ linkId: 'group',
1124
+ item: [{ linkId: 'field1', answer: [] }],
1125
+ },
1126
+ ],
1127
+ };
1128
+
1129
+ const answers = getGroupRepeatedAnswers(questionnaire, response, 'group');
1130
+ expect(answers).toEqual([
1131
+ {
1132
+ field1: {},
1133
+ },
1134
+ ]);
1135
+ });
1136
+ });
1137
+ });