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.
- package/README.md +5 -0
- package/dist/examples/vintasend-medplum-example/.env.example +11 -0
- package/dist/examples/vintasend-medplum-example/IMPLEMENTATION_PLAN_FILE_ATTACHMENTS.md +597 -0
- package/dist/examples/vintasend-medplum-example/README.md +190 -0
- package/dist/examples/vintasend-medplum-example/TUTORIAL_EMAIL_NOTIFICATIONS.md +2596 -0
- package/dist/examples/vintasend-medplum-example/bots/handlers/send-pending-notifications-bot.ts +39 -0
- package/dist/examples/vintasend-medplum-example/bots/handlers/send-task-assignment-email.ts +41 -0
- package/dist/examples/vintasend-medplum-example/bots/handlers/task-due-soon-notification-bot.ts +86 -0
- package/dist/examples/vintasend-medplum-example/bots/index.ts +53 -0
- package/dist/examples/vintasend-medplum-example/bots/services/emails/schedule-task-due-soon-email.ts +84 -0
- package/dist/examples/vintasend-medplum-example/bots/services/emails/send-task-assignment-email.test.ts +388 -0
- package/dist/examples/vintasend-medplum-example/bots/services/emails/send-task-assignment-email.ts +113 -0
- package/dist/examples/vintasend-medplum-example/bots/shared/task-due-soon-helpers.ts +115 -0
- package/dist/examples/vintasend-medplum-example/bots/task-assignment-bot.ts +41 -0
- package/dist/examples/vintasend-medplum-example/compiled-notification-templates.json +6 -0
- package/dist/examples/vintasend-medplum-example/esbuild-script.mjs +71 -0
- package/dist/examples/vintasend-medplum-example/index.html +14 -0
- package/dist/examples/vintasend-medplum-example/lib/constants.ts +32 -0
- package/dist/examples/vintasend-medplum-example/lib/extensions.ts +1 -0
- package/dist/examples/vintasend-medplum-example/lib/file-upload.test.ts +389 -0
- package/dist/examples/vintasend-medplum-example/lib/file-upload.ts +222 -0
- package/dist/examples/vintasend-medplum-example/lib/medplum-singleton.ts +18 -0
- package/dist/examples/vintasend-medplum-example/lib/notification-service.test.ts +293 -0
- package/dist/examples/vintasend-medplum-example/lib/notification-service.ts +284 -0
- package/dist/examples/vintasend-medplum-example/lib/patients.ts +20 -0
- package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-assignment/body.html.pug +37 -0
- package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-assignment/subject.txt.pug +4 -0
- package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-due-soon/body.html.pug +34 -0
- package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-due-soon/subject.txt.pug +4 -0
- package/dist/examples/vintasend-medplum-example/package.json +75 -0
- package/dist/examples/vintasend-medplum-example/plugins/gql-plugin.mjs +31 -0
- package/dist/examples/vintasend-medplum-example/postcss.config.mjs +21 -0
- package/dist/examples/vintasend-medplum-example/public/favicon.ico +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/acuity.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/auth0.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/azure.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/calcom.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/candid.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/claude.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/datadog.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/deepseek.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/entra.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/epic.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/google.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/healthgorilla.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/healthie.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/labcorp.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/okta.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/openai.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/particle.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/quest.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/recaptcha.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/snowflake.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/stedi.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/stripe.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/sumo.png +0 -0
- package/dist/examples/vintasend-medplum-example/scripts/README.md +162 -0
- package/dist/examples/vintasend-medplum-example/scripts/client.ts +18 -0
- package/dist/examples/vintasend-medplum-example/scripts/deploy-bots.ts +171 -0
- package/dist/examples/vintasend-medplum-example/src/App.tsx +185 -0
- package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemList.test.tsx +350 -0
- package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemList.tsx +241 -0
- package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemPanel.test.tsx +616 -0
- package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemPanel.tsx +138 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionItem.test.tsx +92 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionItem.tsx +47 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionList.test.tsx +464 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionList.tsx +186 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionModal.test.tsx +80 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionModal.tsx +82 -0
- package/dist/examples/vintasend-medplum-example/src/components/DoseSpotIcon.test.tsx +100 -0
- package/dist/examples/vintasend-medplum-example/src/components/DoseSpotIcon.tsx +20 -0
- package/dist/examples/vintasend-medplum-example/src/components/IntegrationCard.module.css +3 -0
- package/dist/examples/vintasend-medplum-example/src/components/IntegrationCard.tsx +62 -0
- package/dist/examples/vintasend-medplum-example/src/components/MessageWithLinks.tsx +47 -0
- package/dist/examples/vintasend-medplum-example/src/components/PerformingLabInput.test.tsx +299 -0
- package/dist/examples/vintasend-medplum-example/src/components/PerformingLabInput.tsx +52 -0
- package/dist/examples/vintasend-medplum-example/src/components/ResourceFormWithRequiredProfile.tsx +82 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/BillingTab.test.tsx +1016 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/BillingTab.tsx +298 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterChart.test.tsx +732 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterChart.tsx +282 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterHeader.test.tsx +268 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterHeader.tsx +224 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/SignAddendum.test.tsx +255 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/SignAddendum.tsx +212 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/SignLockDialog.test.tsx +120 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/SignLockDialog.tsx +57 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/VisitDetailsPanel.test.tsx +224 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/VisitDetailsPanel.tsx +100 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/CoverageInput.test.tsx +431 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/CoverageInput.tsx +130 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.module.css +31 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.test.tsx +234 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.tsx +143 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.module.css +11 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.test.tsx +875 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.tsx +943 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabResultDetails.test.tsx +413 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabResultDetails.tsx +203 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabSelectEmpty.tsx +22 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/README.md +104 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/TestMetadataCardInput.test.tsx +318 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/TestMetadataCardInput.tsx +87 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ChatList.test.tsx +126 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ChatList.tsx +38 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.module.css +23 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.test.tsx +167 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.tsx +53 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/NewTopicDialog.test.tsx +94 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/NewTopicDialog.tsx +165 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.module.css +8 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.test.tsx +523 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.tsx +230 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.module.css +23 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.test.tsx +567 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.tsx +358 -0
- package/dist/examples/vintasend-medplum-example/src/components/plandefinition/AddPlanDefinition.module.css +40 -0
- package/dist/examples/vintasend-medplum-example/src/components/plandefinition/AddPlanDefinition.tsx +257 -0
- package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.module.css +7 -0
- package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.test.tsx +279 -0
- package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.tsx +156 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.module.css +45 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.test.tsx +90 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.tsx +84 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourceBox.module.css +26 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourceBox.tsx +90 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourcePanel.test.tsx +305 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourcePanel.tsx +46 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.module.css +262 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.test.tsx +622 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.tsx +286 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/NewTaskModal.tsx +275 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskAttachmentList.tsx +132 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.module.css +45 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.test.tsx +749 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.tsx +416 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailPanel.test.tsx +278 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailPanel.tsx +133 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.module.css +16 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.test.tsx +255 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.tsx +203 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFileUpload.tsx +129 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.test.tsx +156 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.tsx +142 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.utils.ts +28 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskInputNote.test.tsx +134 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskInputNote.tsx +250 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.module.css +23 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.test.tsx +149 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.tsx +53 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskNoteItem.test.tsx +68 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskNoteItem.tsx +46 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskProperties.test.tsx +555 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskProperties.tsx +170 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskSelectEmpty.test.tsx +32 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskSelectEmpty.tsx +34 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/SimpleTask.test.tsx +47 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/SimpleTask.tsx +29 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskPanel.test.tsx +285 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskPanel.tsx +129 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskQuestionnaireForm.test.tsx +455 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskQuestionnaireForm.tsx +167 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskServiceRequest.test.tsx +435 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskServiceRequest.tsx +116 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.module.css +38 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.test.tsx +200 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.tsx +84 -0
- package/dist/examples/vintasend-medplum-example/src/components/utils.test.ts +176 -0
- package/dist/examples/vintasend-medplum-example/src/components/utils.ts +17 -0
- package/dist/examples/vintasend-medplum-example/src/config/constants.ts +3 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useDebouncedUpdateResource.test.tsx +166 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useDebouncedUpdateResource.ts +28 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useEncounter.test.tsx +94 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useEncounter.ts +11 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useEncounterChart.test.tsx +477 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useEncounterChart.ts +191 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/usePatient.test.tsx +100 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/usePatient.ts +18 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useThreadInbox.test.tsx +379 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useThreadInbox.ts +194 -0
- package/dist/examples/vintasend-medplum-example/src/index.css +8 -0
- package/dist/examples/vintasend-medplum-example/src/main.tsx +57 -0
- package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.module.css +6 -0
- package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.test.tsx +295 -0
- package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.tsx +124 -0
- package/dist/examples/vintasend-medplum-example/src/pages/SignInPage.test.tsx +77 -0
- package/dist/examples/vintasend-medplum-example/src/pages/SignInPage.tsx +22 -0
- package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterChartPage.test.tsx +87 -0
- package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterChartPage.tsx +27 -0
- package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.module.css +16 -0
- package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.test.tsx +287 -0
- package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.tsx +151 -0
- package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.test.tsx +519 -0
- package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.tsx +179 -0
- package/dist/examples/vintasend-medplum-example/src/pages/integrations/FavoriteMedicationsTable.tsx +76 -0
- package/dist/examples/vintasend-medplum-example/src/pages/integrations/IntegrationsPage.test.tsx +234 -0
- package/dist/examples/vintasend-medplum-example/src/pages/integrations/IntegrationsPage.tsx +222 -0
- package/dist/examples/vintasend-medplum-example/src/pages/labs/OrderLabsPage.test.tsx +356 -0
- package/dist/examples/vintasend-medplum-example/src/pages/labs/OrderLabsPage.tsx +275 -0
- package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.module.css +8 -0
- package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.test.tsx +103 -0
- package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.tsx +78 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/CommunicationTab.test.tsx +84 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/CommunicationTab.tsx +82 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotAdvancedOptions.test.tsx +364 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotAdvancedOptions.tsx +149 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotTab.test.tsx +159 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotTab.tsx +37 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/EditTab.test.tsx +140 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/EditTab.tsx +72 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/ExportTab.test.tsx +57 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/ExportTab.tsx +14 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/IntakeFormPage.test.tsx +241 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/IntakeFormPage.tsx +710 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.module.css +37 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.test.tsx +428 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.tsx +334 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.module.css +24 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.test.tsx +154 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.tsx +115 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.utils.test.ts +223 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.utils.ts +89 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientSearchPage.test.tsx +147 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientSearchPage.tsx +79 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientTabsNavigation.tsx +35 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/TasksTab.test.tsx +185 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/TasksTab.tsx +115 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/TimelineTab.tsx +14 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceCreatePage.test.tsx +170 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceCreatePage.tsx +117 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceDetailPage.tsx +28 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceEditPage.test.tsx +131 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceEditPage.tsx +65 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceHistoryPage.test.tsx +108 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceHistoryPage.tsx +16 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.module.css +7 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.test.tsx +37 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.tsx +44 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/useResourceType.ts +44 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/utils.ts +9 -0
- package/dist/examples/vintasend-medplum-example/src/pages/schedule/SchedulePage.test.tsx +302 -0
- package/dist/examples/vintasend-medplum-example/src/pages/schedule/SchedulePage.tsx +416 -0
- package/dist/examples/vintasend-medplum-example/src/pages/spaces/ChatInput.tsx +91 -0
- package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.module.css +6 -0
- package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.test.tsx +102 -0
- package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.tsx +44 -0
- package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.module.css +7 -0
- package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.test.tsx +133 -0
- package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.tsx +91 -0
- package/dist/examples/vintasend-medplum-example/src/test-utils/render.tsx +20 -0
- package/dist/examples/vintasend-medplum-example/src/test.setup.ts +49 -0
- package/dist/examples/vintasend-medplum-example/src/types/encounter.ts +8 -0
- package/dist/examples/vintasend-medplum-example/src/types/spaces.ts +10 -0
- package/dist/examples/vintasend-medplum-example/src/utils/chargeitems.test.ts +141 -0
- package/dist/examples/vintasend-medplum-example/src/utils/chargeitems.ts +59 -0
- package/dist/examples/vintasend-medplum-example/src/utils/claims.test.ts +153 -0
- package/dist/examples/vintasend-medplum-example/src/utils/claims.ts +65 -0
- package/dist/examples/vintasend-medplum-example/src/utils/communication-search.ts +47 -0
- package/dist/examples/vintasend-medplum-example/src/utils/coverage.test.ts +48 -0
- package/dist/examples/vintasend-medplum-example/src/utils/coverage.ts +33 -0
- package/dist/examples/vintasend-medplum-example/src/utils/documentReference.test.ts +102 -0
- package/dist/examples/vintasend-medplum-example/src/utils/documentReference.ts +55 -0
- package/dist/examples/vintasend-medplum-example/src/utils/encounter.test.ts +169 -0
- package/dist/examples/vintasend-medplum-example/src/utils/encounter.ts +261 -0
- package/dist/examples/vintasend-medplum-example/src/utils/intake-form.test.ts +154 -0
- package/dist/examples/vintasend-medplum-example/src/utils/intake-form.ts +272 -0
- package/dist/examples/vintasend-medplum-example/src/utils/intake-utils.test.ts +1137 -0
- package/dist/examples/vintasend-medplum-example/src/utils/intake-utils.ts +827 -0
- package/dist/examples/vintasend-medplum-example/src/utils/notifications.test.ts +27 -0
- package/dist/examples/vintasend-medplum-example/src/utils/notifications.ts +15 -0
- package/dist/examples/vintasend-medplum-example/src/utils/spaceMessaging.ts +249 -0
- package/dist/examples/vintasend-medplum-example/src/utils/spacePersistence.test.ts +450 -0
- package/dist/examples/vintasend-medplum-example/src/utils/spacePersistence.ts +147 -0
- package/dist/examples/vintasend-medplum-example/src/utils/task-search.ts +63 -0
- package/dist/examples/vintasend-medplum-example/src/vite-env.d.ts +3 -0
- package/dist/examples/vintasend-medplum-example/tsconfig.bots.json +4 -0
- package/dist/examples/vintasend-medplum-example/tsconfig.json +19 -0
- package/dist/examples/vintasend-medplum-example/vercel.json +3 -0
- package/dist/examples/vintasend-medplum-example/vite.config.ts +44 -0
- package/dist/services/notification-backends/base-notification-backend.d.ts +5 -0
- package/dist/services/notification-service.js +11 -1
- package/dist/services/notification-template-renderers/base-email-template-renderer.d.ts +5 -0
- 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
|
+
});
|