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,1016 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { MantineProvider } from '@mantine/core';
4
+ import { Notifications } from '@mantine/notifications';
5
+ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
6
+ import userEvent from '@testing-library/user-event';
7
+ import { MedplumProvider } from '@medplum/react';
8
+ import type {
9
+ ChargeItem,
10
+ ChargeItemDefinition,
11
+ Claim,
12
+ CodeableConcept,
13
+ Coverage,
14
+ Encounter,
15
+ Patient,
16
+ Practitioner,
17
+ } from '@medplum/fhirtypes';
18
+ import { HomerSimpson, MockClient } from '@medplum/mock';
19
+ import { MemoryRouter } from 'react-router';
20
+ import { describe, expect, test, beforeEach, vi } from 'vitest';
21
+ import { BillingTab } from './BillingTab';
22
+ import * as useDebouncedUpdateResourceModule from '../../hooks/useDebouncedUpdateResource';
23
+ import * as claimsUtils from '../../utils/claims';
24
+ import * as chargeItemsUtils from '../../utils/chargeitems';
25
+ import { SAVE_TIMEOUT_MS } from '../../config/constants';
26
+
27
+ vi.mock('@mantine/notifications', async () => {
28
+ const actual = await vi.importActual('@mantine/notifications');
29
+ return {
30
+ ...actual,
31
+ showNotification: vi.fn(),
32
+ };
33
+ });
34
+
35
+ const { showNotification } = await import('@mantine/notifications');
36
+
37
+ const mockPatient: Patient = {
38
+ resourceType: 'Patient',
39
+ id: 'patient-123',
40
+ name: [{ given: ['John'], family: 'Doe' }],
41
+ };
42
+
43
+ const mockEncounter: Encounter = {
44
+ resourceType: 'Encounter',
45
+ id: 'encounter-123',
46
+ status: 'finished',
47
+ class: { code: 'AMB', system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode' },
48
+ subject: { reference: `Patient/${HomerSimpson.id}` },
49
+ participant: [
50
+ {
51
+ individual: { reference: 'Practitioner/practitioner-123' },
52
+ },
53
+ ],
54
+ };
55
+
56
+ const mockCoverage: Coverage = {
57
+ resourceType: 'Coverage',
58
+ id: 'coverage-123',
59
+ status: 'active',
60
+ beneficiary: { reference: 'Patient/patient-123' },
61
+ payor: [{ reference: 'Organization/organization-123' }],
62
+ };
63
+
64
+ const mockPractitioner: Practitioner = {
65
+ resourceType: 'Practitioner',
66
+ id: 'practitioner-123',
67
+ name: [{ given: ['Dr.'], family: 'Test' }],
68
+ };
69
+
70
+ const mockChargeItem: ChargeItem = {
71
+ resourceType: 'ChargeItem',
72
+ id: 'charge-123',
73
+ status: 'billable',
74
+ subject: { reference: 'Patient/patient-123' },
75
+ code: {
76
+ coding: [
77
+ {
78
+ system: 'http://www.ama-assn.org/go/cpt',
79
+ code: '99214',
80
+ display: 'Office Visit Level 4',
81
+ },
82
+ ],
83
+ text: 'Test Charge',
84
+ },
85
+ priceOverride: {
86
+ value: 100,
87
+ currency: 'USD',
88
+ },
89
+ };
90
+
91
+ const mockClaim: Claim = {
92
+ resourceType: 'Claim',
93
+ id: 'claim-123',
94
+ status: 'active',
95
+ type: { coding: [{ code: 'professional' }] },
96
+ use: 'claim',
97
+ created: new Date().toISOString(),
98
+ priority: { coding: [{ code: 'normal' }] },
99
+ insurance: [],
100
+ patient: { reference: 'Patient/patient-123' },
101
+ provider: { reference: 'Practitioner/practitioner-123' },
102
+ };
103
+
104
+ describe('BillingTab', () => {
105
+ let medplum: MockClient;
106
+
107
+ beforeEach(async () => {
108
+ medplum = new MockClient();
109
+ vi.clearAllMocks();
110
+ // Mock useDebouncedUpdateResource to return a function that resolves immediately
111
+ vi.spyOn(useDebouncedUpdateResourceModule, 'useDebouncedUpdateResource').mockReturnValue(
112
+ vi.fn().mockResolvedValue(undefined)
113
+ );
114
+ });
115
+
116
+ const setup = async (props: Partial<Parameters<typeof BillingTab>[0]> = {}): Promise<void> => {
117
+ return act(async () => {
118
+ render(
119
+ <MemoryRouter>
120
+ <MedplumProvider medplum={medplum}>
121
+ <MantineProvider>
122
+ <Notifications />
123
+ <BillingTab
124
+ patient={mockPatient}
125
+ encounter={mockEncounter}
126
+ setEncounter={vi.fn()}
127
+ practitioner={mockPractitioner}
128
+ setPractitioner={vi.fn()}
129
+ chargeItems={[]}
130
+ setChargeItems={vi.fn()}
131
+ claim={undefined}
132
+ setClaim={vi.fn()}
133
+ {...props}
134
+ />
135
+ </MantineProvider>
136
+ </MedplumProvider>
137
+ </MemoryRouter>
138
+ );
139
+ });
140
+ };
141
+
142
+ test('renders visit details panel', async () => {
143
+ await setup();
144
+ expect(screen.getByText('Visit Details')).toBeInTheDocument();
145
+ expect(screen.getByText(/Dr\. Test/i)).toBeInTheDocument();
146
+ });
147
+
148
+ test('renders condition list when there are conditions', async () => {
149
+ const mockCondition = {
150
+ resourceType: 'Condition' as const,
151
+ id: 'condition-1',
152
+ code: {
153
+ coding: [{ code: 'R51', display: 'Headache' }],
154
+ text: 'Headache',
155
+ },
156
+ };
157
+
158
+ vi.spyOn(medplum, 'readReference').mockResolvedValue(mockCondition as any);
159
+
160
+ await setup({
161
+ encounter: {
162
+ ...mockEncounter,
163
+ diagnosis: [
164
+ {
165
+ condition: { reference: 'Condition/condition-1' },
166
+ },
167
+ ],
168
+ },
169
+ });
170
+
171
+ expect(screen.getByText('Diagnosis')).toBeInTheDocument();
172
+ expect(screen.getByText('Add Diagnosis')).toBeInTheDocument();
173
+
174
+ await waitFor(() => {
175
+ expect(screen.getByText('Headache')).toBeInTheDocument();
176
+ });
177
+ });
178
+
179
+ test('renders condition list when there are NO conditions', async () => {
180
+ await setup({
181
+ encounter: {
182
+ ...mockEncounter,
183
+ diagnosis: [],
184
+ },
185
+ });
186
+
187
+ expect(screen.getByText('Diagnosis')).toBeInTheDocument();
188
+ expect(screen.getByText('Add Diagnosis')).toBeInTheDocument();
189
+ expect(screen.queryByText('Headache')).not.toBeInTheDocument();
190
+ });
191
+
192
+ test('renders charge item list when charge items are provided', async () => {
193
+ await setup({ chargeItems: [mockChargeItem] });
194
+
195
+ expect(screen.getByText('Charge Items')).toBeInTheDocument();
196
+ expect(screen.getByText('Add Charge Item')).toBeInTheDocument();
197
+ });
198
+
199
+ test('does not render export claim button when no claim', async () => {
200
+ await setup({ claim: undefined });
201
+
202
+ expect(screen.queryByText('Export Claim')).not.toBeInTheDocument();
203
+ });
204
+
205
+ test('renders export claim button when claim exists', async () => {
206
+ await setup({ claim: mockClaim });
207
+
208
+ expect(screen.getByText('Export Claim')).toBeInTheDocument();
209
+ });
210
+
211
+ test('shows export menu options when export button is clicked', async () => {
212
+ const user = userEvent.setup();
213
+ await setup({ claim: mockClaim });
214
+
215
+ const exportButton = screen.getByText('Export Claim');
216
+ await user.click(exportButton);
217
+
218
+ await waitFor(() => {
219
+ expect(screen.getByText('CMS 1500 Form')).toBeInTheDocument();
220
+ expect(screen.getByText('EDI X12')).toBeInTheDocument();
221
+ expect(screen.getByText('NUCC Crosswalk CSV')).toBeInTheDocument();
222
+ });
223
+ });
224
+
225
+ test('exports claim as CMS 1500 when option is selected', async () => {
226
+ const user = userEvent.setup();
227
+
228
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue([mockCoverage] as any);
229
+ vi.spyOn(medplum, 'post').mockResolvedValue({
230
+ resourceType: 'Media',
231
+ content: { url: 'https://example.com/claim.pdf' },
232
+ } as any);
233
+
234
+ const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
235
+
236
+ await setup({ claim: mockClaim });
237
+
238
+ const exportButton = screen.getByText('Export Claim');
239
+ await user.click(exportButton);
240
+
241
+ await waitFor(() => {
242
+ expect(screen.getByText('CMS 1500 Form')).toBeInTheDocument();
243
+ });
244
+
245
+ await user.click(screen.getByText('CMS 1500 Form'));
246
+
247
+ await waitFor(() => {
248
+ expect(medplum.post).toHaveBeenCalled();
249
+ expect(windowOpenSpy).toHaveBeenCalledWith('https://example.com/claim.pdf', '_blank');
250
+ });
251
+
252
+ windowOpenSpy.mockRestore();
253
+ });
254
+
255
+ test('renders request billing service button', async () => {
256
+ await setup({ claim: mockClaim });
257
+ expect(screen.getByText('Request to connect a billing service')).toBeInTheDocument();
258
+ });
259
+
260
+ test('fetches coverage on mount', async () => {
261
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue([mockCoverage] as any);
262
+ await act(async () => {
263
+ await setup();
264
+ });
265
+
266
+ await waitFor(() => {
267
+ expect(medplum.searchResources).toHaveBeenCalledWith(
268
+ 'Coverage',
269
+ expect.stringContaining('patient=Patient/patient-123')
270
+ );
271
+ });
272
+ });
273
+
274
+ test('shows notification when EDI X12 menu item is clicked', async () => {
275
+ const user = userEvent.setup();
276
+ await setup({ claim: mockClaim });
277
+
278
+ const exportButton = screen.getByText('Export Claim');
279
+ await user.click(exportButton);
280
+
281
+ await waitFor(() => {
282
+ expect(screen.getByText('EDI X12')).toBeInTheDocument();
283
+ });
284
+
285
+ await user.click(screen.getByText('EDI X12'));
286
+
287
+ await waitFor(() => {
288
+ expect(vi.mocked(showNotification)).toHaveBeenCalledWith({
289
+ title: 'EDI X12',
290
+ message: 'Please contact sales to enable EDI X12 export',
291
+ color: 'blue',
292
+ });
293
+ });
294
+ });
295
+
296
+ test('shows notification when NUCC Crosswalk CSV menu item is clicked', async () => {
297
+ const user = userEvent.setup();
298
+ await setup({ claim: mockClaim });
299
+
300
+ const exportButton = screen.getByText('Export Claim');
301
+ await user.click(exportButton);
302
+
303
+ await waitFor(() => {
304
+ expect(screen.getByText('NUCC Crosswalk CSV')).toBeInTheDocument();
305
+ });
306
+
307
+ await user.click(screen.getByText('NUCC Crosswalk CSV'));
308
+
309
+ await waitFor(() => {
310
+ expect(vi.mocked(showNotification)).toHaveBeenCalledWith({
311
+ title: 'NUCC Crosswalk',
312
+ message: 'Please contact sales to enable NUCC Crosswalk export',
313
+ color: 'blue',
314
+ });
315
+ });
316
+ });
317
+
318
+ test('updates claim when charge item is added', async () => {
319
+ const user = userEvent.setup();
320
+ const setChargeItems = vi.fn();
321
+ const setClaim = vi.fn();
322
+ const debouncedUpdateResource = vi.fn().mockResolvedValue(undefined);
323
+
324
+ vi.spyOn(useDebouncedUpdateResourceModule, 'useDebouncedUpdateResource').mockReturnValue(debouncedUpdateResource);
325
+
326
+ const mockCptCode: CodeableConcept = {
327
+ coding: [
328
+ {
329
+ system: 'http://www.ama-assn.org/go/cpt',
330
+ code: '99214',
331
+ display: 'Office Visit Level 4',
332
+ },
333
+ ],
334
+ };
335
+ const mockDefinition: ChargeItemDefinition & { id: string } = {
336
+ resourceType: 'ChargeItemDefinition',
337
+ id: 'def-123',
338
+ status: 'active',
339
+ url: 'http://example.com/charge-item-def',
340
+ title: 'Test Definition',
341
+ };
342
+ const newChargeItem: ChargeItem & { id: string } = {
343
+ resourceType: 'ChargeItem',
344
+ id: 'charge-new',
345
+ status: 'planned',
346
+ subject: { reference: 'Patient/patient-123' },
347
+ code: mockCptCode,
348
+ };
349
+ const appliedChargeItem: ChargeItem & { id: string } = {
350
+ ...newChargeItem,
351
+ priceOverride: { value: 200, currency: 'USD' },
352
+ };
353
+
354
+ const mockClaimItems = [
355
+ {
356
+ sequence: 1,
357
+ productOrService: {
358
+ coding: [{ system: 'http://www.ama-assn.org/go/cpt', code: '99214' }],
359
+ },
360
+ },
361
+ ];
362
+
363
+ // Mock valueSetExpand for CPT code search
364
+ medplum.valueSetExpand = vi.fn().mockResolvedValue({
365
+ resourceType: 'ValueSet',
366
+ expansion: {
367
+ contains: [
368
+ {
369
+ system: 'http://www.ama-assn.org/go/cpt',
370
+ code: '99214',
371
+ display: 'Office Visit Level 4',
372
+ },
373
+ ],
374
+ },
375
+ });
376
+
377
+ // Mock searchResources for ChargeItemDefinition search
378
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue([mockDefinition] as any);
379
+ vi.spyOn(medplum, 'createResource').mockResolvedValue(newChargeItem);
380
+ vi.spyOn(chargeItemsUtils, 'applyChargeItemDefinition').mockResolvedValue(appliedChargeItem);
381
+ vi.spyOn(claimsUtils, 'getCptChargeItems').mockReturnValue(mockClaimItems);
382
+ vi.spyOn(chargeItemsUtils, 'calculateTotalPrice').mockReturnValue(200);
383
+
384
+ await setup({
385
+ claim: mockClaim,
386
+ chargeItems: [],
387
+ setChargeItems,
388
+ setClaim,
389
+ encounter: mockEncounter,
390
+ });
391
+
392
+ // Open modal
393
+ await user.click(screen.getByText('Add Charge Item'));
394
+
395
+ await waitFor(() => {
396
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
397
+ });
398
+
399
+ // Find CPT Code input
400
+ const cptInputs = screen.getAllByRole('searchbox');
401
+ const cptInput = cptInputs.find((input) => {
402
+ const label = input.closest('.mantine-InputWrapper-root')?.querySelector('label');
403
+ return label?.textContent?.includes('CPT Code');
404
+ }) as HTMLInputElement;
405
+
406
+ expect(cptInput).toBeDefined();
407
+
408
+ // Type in CPT code input
409
+ await act(async () => {
410
+ await user.type(cptInput, '99214');
411
+ });
412
+
413
+ // Wait for valueSetExpand to be called
414
+ await waitFor(
415
+ () => {
416
+ expect(medplum.valueSetExpand).toHaveBeenCalled();
417
+ },
418
+ { timeout: 3000 }
419
+ );
420
+
421
+ // Select the CPT code option
422
+ await act(async () => {
423
+ fireEvent.keyDown(cptInput, { key: 'ArrowDown', code: 'ArrowDown' });
424
+ fireEvent.keyDown(cptInput, { key: 'Enter', code: 'Enter' });
425
+ });
426
+
427
+ // Wait for CPT code to be selected
428
+ await waitFor(
429
+ () => {
430
+ const selectedItems = document.querySelector('[data-testid="selected-items"]');
431
+ const hasCode =
432
+ selectedItems?.textContent?.includes('99214') || selectedItems?.textContent?.includes('Office Visit Level 4');
433
+ return hasCode === true;
434
+ },
435
+ { timeout: 3000 }
436
+ );
437
+
438
+ // Find Charge Item Definition input
439
+ const definitionInputs = screen.getAllByRole('searchbox');
440
+ const definitionInput = definitionInputs.find((input) => {
441
+ const placeholder = (input as HTMLInputElement).placeholder;
442
+ return placeholder?.toLowerCase().includes('charge item definition');
443
+ }) as HTMLInputElement;
444
+
445
+ expect(definitionInput).toBeDefined();
446
+
447
+ // Type in definition input
448
+ await act(async () => {
449
+ await user.type(definitionInput, 'Test');
450
+ });
451
+
452
+ // Wait for searchResources to be called
453
+ await waitFor(
454
+ () => {
455
+ expect(medplum.searchResources).toHaveBeenCalledWith(
456
+ 'ChargeItemDefinition',
457
+ expect.any(URLSearchParams),
458
+ expect.any(Object)
459
+ );
460
+ },
461
+ { timeout: 3000 }
462
+ );
463
+
464
+ // Select the definition option
465
+ await act(async () => {
466
+ fireEvent.keyDown(definitionInput, { key: 'ArrowDown', code: 'ArrowDown' });
467
+ fireEvent.keyDown(definitionInput, { key: 'Enter', code: 'Enter' });
468
+ });
469
+
470
+ // Wait for definition to be selected and button to be enabled
471
+ await waitFor(
472
+ () => {
473
+ const addButtons = screen.getAllByRole('button', { name: 'Add Charge Item' });
474
+ const submitButton = addButtons.find((btn) => {
475
+ const htmlBtn = btn as HTMLButtonElement;
476
+ return htmlBtn.type === 'button' && !htmlBtn.disabled;
477
+ });
478
+ return submitButton !== undefined;
479
+ },
480
+ { timeout: 5000 }
481
+ );
482
+
483
+ // Submit the form
484
+ const addButtons = screen.getAllByRole('button', { name: 'Add Charge Item' });
485
+ const submitButton = addButtons.find((btn) => {
486
+ const htmlBtn = btn as HTMLButtonElement;
487
+ return htmlBtn.type === 'button' && !htmlBtn.disabled;
488
+ });
489
+
490
+ expect(submitButton).toBeDefined();
491
+ if (submitButton) {
492
+ await user.click(submitButton);
493
+
494
+ // Verify charge item was created and updateChargeItems was called
495
+ await waitFor(() => {
496
+ expect(medplum.createResource).toHaveBeenCalledWith(
497
+ expect.objectContaining({
498
+ resourceType: 'ChargeItem',
499
+ status: 'planned',
500
+ code: mockCptCode,
501
+ definitionCanonical: ['http://example.com/charge-item-def'],
502
+ })
503
+ );
504
+ expect(chargeItemsUtils.applyChargeItemDefinition).toHaveBeenCalled();
505
+ expect(setChargeItems).toHaveBeenCalledWith([appliedChargeItem]);
506
+ });
507
+
508
+ // Verify claim was updated
509
+ await waitFor(
510
+ () => {
511
+ expect(claimsUtils.getCptChargeItems).toHaveBeenCalledWith([appliedChargeItem], {
512
+ reference: 'Encounter/encounter-123',
513
+ });
514
+ expect(chargeItemsUtils.calculateTotalPrice).toHaveBeenCalledWith([appliedChargeItem]);
515
+ expect(setClaim).toHaveBeenCalledWith(
516
+ expect.objectContaining({
517
+ ...mockClaim,
518
+ item: mockClaimItems,
519
+ total: { value: 200 },
520
+ })
521
+ );
522
+ expect(debouncedUpdateResource).toHaveBeenCalledWith(
523
+ expect.objectContaining({
524
+ ...mockClaim,
525
+ item: mockClaimItems,
526
+ total: { value: 200 },
527
+ })
528
+ );
529
+ },
530
+ { timeout: 5000 }
531
+ );
532
+ }
533
+ });
534
+
535
+ test('creates claim when practitioner is changed and charge items exist', async () => {
536
+ const setEncounter = vi.fn();
537
+ const setClaim = vi.fn();
538
+ const debouncedUpdateResource = vi.fn().mockResolvedValue(undefined);
539
+
540
+ vi.spyOn(useDebouncedUpdateResourceModule, 'useDebouncedUpdateResource').mockReturnValue(debouncedUpdateResource);
541
+
542
+ const mockPractitioner1: Practitioner = {
543
+ resourceType: 'Practitioner',
544
+ id: 'practitioner-1',
545
+ name: [{ given: ['Dr.'], family: 'Test' }],
546
+ };
547
+
548
+ const mockPractitioner2: Practitioner = {
549
+ resourceType: 'Practitioner',
550
+ id: 'practitioner-2',
551
+ name: [{ given: ['Dr.'], family: 'Smith' }],
552
+ };
553
+
554
+ const appliedChargeItem: ChargeItem & { id: string } = {
555
+ resourceType: 'ChargeItem',
556
+ id: 'charge-new',
557
+ status: 'planned',
558
+ subject: { reference: 'Patient/patient-123' },
559
+ code: {
560
+ coding: [
561
+ {
562
+ system: 'http://www.ama-assn.org/go/cpt',
563
+ code: '99214',
564
+ display: 'Office Visit Level 4',
565
+ },
566
+ ],
567
+ },
568
+ priceOverride: { value: 200, currency: 'USD' },
569
+ };
570
+
571
+ const newClaim: Claim & { id: string } = {
572
+ resourceType: 'Claim',
573
+ id: 'claim-new',
574
+ status: 'active',
575
+ type: { coding: [{ code: 'professional' }] },
576
+ use: 'claim',
577
+ created: new Date().toISOString(),
578
+ priority: { coding: [{ code: 'normal' }] },
579
+ insurance: [],
580
+ patient: { reference: 'Patient/patient-123' },
581
+ provider: { reference: 'Practitioner/practitioner-2' },
582
+ };
583
+
584
+ const updatedEncounter: Encounter = {
585
+ ...mockEncounter,
586
+ participant: [
587
+ {
588
+ individual: { reference: 'Practitioner/practitioner-2' },
589
+ },
590
+ ],
591
+ };
592
+
593
+ await medplum.createResource(mockPractitioner1);
594
+ await medplum.createResource(mockPractitioner2);
595
+
596
+ // Mock searchResources to return different results based on resource type
597
+ // Use mockResolvedValue for Coverage (called on mount) and mockImplementation for Practitioner
598
+ vi.spyOn(medplum, 'searchResources')
599
+ .mockResolvedValueOnce([mockCoverage] as any) // Coverage search on mount
600
+ .mockResolvedValue([mockPractitioner1, mockPractitioner2] as any); // Practitioner searches
601
+ vi.spyOn(claimsUtils, 'createClaimFromEncounter').mockResolvedValue(newClaim);
602
+ vi.spyOn(medplum, 'updateResource').mockResolvedValue(updatedEncounter as any);
603
+ vi.spyOn(medplum, 'readReference').mockResolvedValue(mockPractitioner2 as any);
604
+
605
+ // Setup with charge items but no claim initially
606
+ await setup({
607
+ claim: undefined, // No claim initially
608
+ chargeItems: [appliedChargeItem], // Charge items already present
609
+ setChargeItems: vi.fn(),
610
+ setClaim,
611
+ setEncounter,
612
+ encounter: {
613
+ resourceType: 'Encounter',
614
+ id: 'encounter-123',
615
+ status: 'finished',
616
+ class: { code: 'AMB', system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode' },
617
+ subject: { reference: `Patient/${HomerSimpson.id}` },
618
+ },
619
+ practitioner: undefined,
620
+ });
621
+
622
+ await waitFor(() => {
623
+ expect(screen.getByText('Practitioner')).toBeInTheDocument();
624
+ });
625
+
626
+ const practitionerInput = screen.getByPlaceholderText('Search for practitioner');
627
+ expect(practitionerInput).toBeDefined();
628
+
629
+ await act(async () => {
630
+ fireEvent.change(practitionerInput, { target: { value: 'Smith' } });
631
+ });
632
+
633
+ await waitFor(
634
+ () => {
635
+ const smithOption = screen.queryByText(/Smith/i);
636
+ expect(smithOption).toBeInTheDocument();
637
+ },
638
+ { timeout: 3000 }
639
+ );
640
+
641
+ await act(async () => {
642
+ const smithOption = screen.getByText(/Smith/i);
643
+ fireEvent.click(smithOption);
644
+ });
645
+
646
+ await waitFor(
647
+ () => {
648
+ expect(medplum.updateResource).toHaveBeenCalled();
649
+ },
650
+ { timeout: SAVE_TIMEOUT_MS + 2000 }
651
+ );
652
+
653
+ await waitFor(
654
+ () => {
655
+ expect(medplum.readReference).toHaveBeenCalledWith({ reference: 'Practitioner/practitioner-2' });
656
+ },
657
+ { timeout: 1000 }
658
+ );
659
+
660
+ await waitFor(
661
+ () => {
662
+ expect(claimsUtils.createClaimFromEncounter).toHaveBeenCalledWith(
663
+ medplum,
664
+ mockPatient.id,
665
+ mockEncounter.id,
666
+ mockPractitioner2.id,
667
+ [appliedChargeItem]
668
+ );
669
+ expect(setClaim).toHaveBeenCalledWith(newClaim);
670
+ },
671
+ { timeout: 5000 }
672
+ );
673
+ }, 15000);
674
+
675
+ test('updates claim when practitioner is changed and claim already exists', async () => {
676
+ const setEncounter = vi.fn();
677
+ const setClaim = vi.fn();
678
+ const debouncedUpdateResource = vi.fn().mockResolvedValue(undefined);
679
+
680
+ vi.spyOn(useDebouncedUpdateResourceModule, 'useDebouncedUpdateResource').mockReturnValue(debouncedUpdateResource);
681
+
682
+ const mockPractitioner1: Practitioner = {
683
+ resourceType: 'Practitioner',
684
+ id: 'practitioner-1',
685
+ name: [{ given: ['Dr.'], family: 'Test' }],
686
+ };
687
+
688
+ const mockPractitioner2: Practitioner = {
689
+ resourceType: 'Practitioner',
690
+ id: 'practitioner-2',
691
+ name: [{ given: ['Dr.'], family: 'Smith' }],
692
+ };
693
+
694
+ const appliedChargeItem: ChargeItem & { id: string } = {
695
+ resourceType: 'ChargeItem',
696
+ id: 'charge-new',
697
+ status: 'planned',
698
+ subject: { reference: 'Patient/patient-123' },
699
+ code: {
700
+ coding: [
701
+ {
702
+ system: 'http://www.ama-assn.org/go/cpt',
703
+ code: '99214',
704
+ display: 'Office Visit Level 4',
705
+ },
706
+ ],
707
+ },
708
+ priceOverride: { value: 200, currency: 'USD' },
709
+ };
710
+
711
+ const existingClaim: Claim & { id: string } = {
712
+ resourceType: 'Claim',
713
+ id: 'claim-existing',
714
+ status: 'active',
715
+ type: { coding: [{ code: 'professional' }] },
716
+ use: 'claim',
717
+ created: new Date().toISOString(),
718
+ priority: { coding: [{ code: 'normal' }] },
719
+ insurance: [],
720
+ patient: { reference: 'Patient/patient-123' },
721
+ provider: { reference: 'Practitioner/practitioner-1' }, // Original practitioner
722
+ };
723
+
724
+ const updatedClaim: Claim & { id: string } = {
725
+ ...existingClaim,
726
+ provider: { reference: 'Practitioner/practitioner-2' }, // Updated practitioner
727
+ };
728
+
729
+ const updatedEncounter: Encounter = {
730
+ ...mockEncounter,
731
+ participant: [
732
+ {
733
+ individual: { reference: 'Practitioner/practitioner-2' },
734
+ },
735
+ ],
736
+ };
737
+
738
+ await medplum.createResource(mockPractitioner1);
739
+ await medplum.createResource(mockPractitioner2);
740
+
741
+ // Mock searchResources to return different results based on resource type
742
+ vi.spyOn(medplum, 'searchResources')
743
+ .mockResolvedValueOnce([mockCoverage] as any) // Coverage search on mount
744
+ .mockResolvedValue([mockPractitioner1, mockPractitioner2] as any); // Practitioner searches
745
+ vi.spyOn(medplum, 'updateResource').mockImplementation(async (resource: any) => {
746
+ // Return updated encounter when updating encounter, updated claim when updating claim
747
+ if (resource.resourceType === 'Encounter') {
748
+ return updatedEncounter as any;
749
+ }
750
+ if (resource.resourceType === 'Claim') {
751
+ return updatedClaim as any;
752
+ }
753
+ return resource;
754
+ });
755
+ vi.spyOn(medplum, 'readReference').mockResolvedValue(mockPractitioner2 as any);
756
+
757
+ await setup({
758
+ claim: existingClaim,
759
+ chargeItems: [appliedChargeItem],
760
+ setChargeItems: vi.fn(),
761
+ setClaim,
762
+ setEncounter,
763
+ encounter: {
764
+ resourceType: 'Encounter',
765
+ id: 'encounter-123',
766
+ status: 'finished',
767
+ class: { code: 'AMB', system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode' },
768
+ subject: { reference: `Patient/${HomerSimpson.id}` },
769
+ participant: [
770
+ {
771
+ individual: { reference: 'Practitioner/practitioner-1' }, // Original practitioner
772
+ },
773
+ ],
774
+ },
775
+ practitioner: mockPractitioner1, // Start with practitioner-1
776
+ });
777
+
778
+ await waitFor(() => {
779
+ expect(screen.getByText('Practitioner')).toBeInTheDocument();
780
+ expect(screen.getByText(/Dr\. Test/i)).toBeInTheDocument();
781
+ });
782
+
783
+ const practitionerText = screen.getByText(/Dr\. Test/i);
784
+ const practitionerContainer = practitionerText.closest('[data-testid]') || practitionerText.closest('div');
785
+ const clickableElement =
786
+ practitionerContainer?.querySelector('button') || practitionerText.closest('button') || practitionerText;
787
+
788
+ // Click to open the dropdown
789
+ await act(async () => {
790
+ fireEvent.click(clickableElement);
791
+ });
792
+
793
+ await waitFor(
794
+ () => {
795
+ const practitionerInput = screen.queryByPlaceholderText('Search for practitioner') as HTMLInputElement;
796
+ expect(practitionerInput).toBeInTheDocument();
797
+ },
798
+ { timeout: 3000 }
799
+ );
800
+
801
+ const practitionerInput = screen.getByPlaceholderText('Search for practitioner');
802
+
803
+ await act(async () => {
804
+ fireEvent.change(practitionerInput, { target: { value: 'Smith' } });
805
+ });
806
+
807
+ await waitFor(
808
+ () => {
809
+ const smithOption = screen.queryByText(/Smith/i);
810
+ expect(smithOption).toBeInTheDocument();
811
+ },
812
+ { timeout: 3000 }
813
+ );
814
+
815
+ await act(async () => {
816
+ const smithOption = screen.getByText(/Smith/i);
817
+ fireEvent.click(smithOption);
818
+ });
819
+
820
+ await waitFor(
821
+ () => {
822
+ expect(medplum.updateResource).toHaveBeenCalled();
823
+ },
824
+ { timeout: SAVE_TIMEOUT_MS + 2000 }
825
+ );
826
+
827
+ await waitFor(
828
+ () => {
829
+ expect(medplum.readReference).toHaveBeenCalledWith({ reference: 'Practitioner/practitioner-2' });
830
+ },
831
+ { timeout: 1000 }
832
+ );
833
+
834
+ await waitFor(
835
+ () => {
836
+ const updateCalls = vi.mocked(medplum.updateResource).mock.calls;
837
+ const claimUpdateCall = updateCalls.find((call) => {
838
+ const resource = call[0] as Claim;
839
+ return resource.resourceType === 'Claim' && resource.provider?.reference === 'Practitioner/practitioner-2';
840
+ });
841
+ expect(claimUpdateCall).toBeDefined();
842
+ expect(setClaim).toHaveBeenCalledWith(updatedClaim);
843
+ },
844
+ { timeout: 5000 }
845
+ );
846
+ }, 15000);
847
+
848
+ test('handles export when claim id is missing', async () => {
849
+ const user = userEvent.setup();
850
+
851
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue([mockCoverage] as any);
852
+ vi.spyOn(medplum, 'post').mockResolvedValue({
853
+ resourceType: 'Media',
854
+ content: { url: 'https://example.com/claim.pdf' },
855
+ } as any);
856
+
857
+ // Setup with a claim but with undefined id
858
+ await setup({ claim: { ...mockClaim, id: undefined } });
859
+
860
+ // Export button is visible but clicking it should be a no-op
861
+ const exportButton = screen.getByText('Export Claim');
862
+ await user.click(exportButton);
863
+
864
+ await waitFor(() => {
865
+ expect(screen.getByText('CMS 1500 Form')).toBeInTheDocument();
866
+ });
867
+
868
+ await user.click(screen.getByText('CMS 1500 Form'));
869
+
870
+ // Post should NOT be called due to early return
871
+ await waitFor(() => {
872
+ expect(medplum.post).not.toHaveBeenCalled();
873
+ });
874
+ });
875
+
876
+ test('shows error when export fails to return Media', async () => {
877
+ const user = userEvent.setup();
878
+
879
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue([mockCoverage] as any);
880
+ vi.spyOn(medplum, 'post').mockResolvedValue({
881
+ resourceType: 'OperationOutcome',
882
+ issue: [{ severity: 'error', code: 'invalid' }],
883
+ } as any);
884
+
885
+ await setup({ claim: mockClaim });
886
+
887
+ const exportButton = screen.getByText('Export Claim');
888
+ await user.click(exportButton);
889
+
890
+ await waitFor(() => {
891
+ expect(screen.getByText('CMS 1500 Form')).toBeInTheDocument();
892
+ });
893
+
894
+ await user.click(screen.getByText('CMS 1500 Form'));
895
+
896
+ await waitFor(() => {
897
+ expect(screen.getByText('Failed to download PDF')).toBeInTheDocument();
898
+ });
899
+ });
900
+
901
+ test('creates self-pay coverage when no coverage exists', async () => {
902
+ const user = userEvent.setup();
903
+
904
+ // Return empty coverage array first, then return self-pay coverage for the claim
905
+ const selfPayCoverage: Coverage = {
906
+ resourceType: 'Coverage',
907
+ id: 'self-pay-123',
908
+ status: 'active',
909
+ beneficiary: { reference: 'Patient/patient-123' },
910
+ payor: [{ reference: 'Patient/patient-123' }],
911
+ };
912
+
913
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue([] as any);
914
+ vi.spyOn(medplum, 'createResource').mockResolvedValue(selfPayCoverage as any);
915
+ vi.spyOn(medplum, 'post').mockResolvedValue({
916
+ resourceType: 'Media',
917
+ content: { url: 'https://example.com/claim.pdf' },
918
+ } as any);
919
+
920
+ const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
921
+
922
+ await setup({ claim: mockClaim });
923
+
924
+ const exportButton = screen.getByText('Export Claim');
925
+ await user.click(exportButton);
926
+
927
+ await waitFor(() => {
928
+ expect(screen.getByText('CMS 1500 Form')).toBeInTheDocument();
929
+ });
930
+
931
+ await user.click(screen.getByText('CMS 1500 Form'));
932
+
933
+ await waitFor(() => {
934
+ expect(medplum.post).toHaveBeenCalled();
935
+ });
936
+
937
+ windowOpenSpy.mockRestore();
938
+ });
939
+
940
+ test('handles error in encounter change', async () => {
941
+ const setEncounter = vi.fn();
942
+ const debouncedUpdateResource = vi.fn().mockResolvedValue(undefined);
943
+
944
+ vi.spyOn(useDebouncedUpdateResourceModule, 'useDebouncedUpdateResource').mockReturnValue(debouncedUpdateResource);
945
+ vi.spyOn(medplum, 'updateResource').mockRejectedValue(new Error('Update failed'));
946
+
947
+ await setup({
948
+ setEncounter,
949
+ practitioner: undefined,
950
+ encounter: mockEncounter,
951
+ });
952
+
953
+ await waitFor(() => {
954
+ expect(screen.getByText('Visit Details')).toBeInTheDocument();
955
+ });
956
+ });
957
+
958
+ test('exports claim with conditions that have ICD-10 coding', async () => {
959
+ const user = userEvent.setup();
960
+
961
+ const mockCondition = {
962
+ resourceType: 'Condition' as const,
963
+ id: 'condition-1',
964
+ code: {
965
+ coding: [
966
+ {
967
+ system: 'http://hl7.org/fhir/sid/icd-10-cm',
968
+ code: 'R51',
969
+ display: 'Headache',
970
+ },
971
+ ],
972
+ text: 'Headache',
973
+ },
974
+ };
975
+
976
+ vi.spyOn(medplum, 'readReference').mockResolvedValue(mockCondition as any);
977
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue([mockCoverage] as any);
978
+ vi.spyOn(medplum, 'post').mockResolvedValue({
979
+ resourceType: 'Media',
980
+ content: { url: 'https://example.com/claim.pdf' },
981
+ } as any);
982
+
983
+ const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
984
+
985
+ await setup({
986
+ claim: mockClaim,
987
+ encounter: {
988
+ ...mockEncounter,
989
+ diagnosis: [
990
+ {
991
+ condition: { reference: 'Condition/condition-1' },
992
+ },
993
+ ],
994
+ },
995
+ });
996
+
997
+ await waitFor(() => {
998
+ expect(screen.getByText('Headache')).toBeInTheDocument();
999
+ });
1000
+
1001
+ const exportButton = screen.getByText('Export Claim');
1002
+ await user.click(exportButton);
1003
+
1004
+ await waitFor(() => {
1005
+ expect(screen.getByText('CMS 1500 Form')).toBeInTheDocument();
1006
+ });
1007
+
1008
+ await user.click(screen.getByText('CMS 1500 Form'));
1009
+
1010
+ await waitFor(() => {
1011
+ expect(medplum.post).toHaveBeenCalled();
1012
+ });
1013
+
1014
+ windowOpenSpy.mockRestore();
1015
+ });
1016
+ });