From 2fb08c360a14eb66419b1153dfa801accff049b9 Mon Sep 17 00:00:00 2001 From: Ryan Shaw <587812+ryanshaw@users.noreply.github.com> Date: Wed, 11 Dec 2024 09:04:15 -0800 Subject: [PATCH] Adds patient relationship mocks to VAOS (#33349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds getPatientRelationships to the VAOS service Signed-off-by: Ryan Shaw <587812+ryanshaw@users.noreply.github.com> * Stubbed out patient relationship mock Signed-off-by: Ryan Shaw <587812+ryanshaw@users.noreply.github.com> * Updated patient relationship mock data Signed-off-by: Ryan Shaw <587812+ryanshaw@users.noreply.github.com> * Stubs out transformer * Stub out types * Flattened relationship object * Updated types * Moved typedef’s * Name changes for consistency * Removes chained parseApiObject, will not work with endpoint * Updates types * Updates transformer to deal with array in response * Added additional mock * Updates service * Stubs out actions and reducers * Updates patient service * Adds reducer * Updates action * Added TODO and cleaned up * Adds failed reducer for providers and updates action * Adds selectors * Adds comment * Adds new hook for fetching patient provider relationships * Adds TODO * Updates types * Fixes transformer * Types reorg * Adds unit test * Update comment * Update src/applications/vaos/services/patient/types.js Co-authored-by: John Luo * Adds links to GH issue * Updates comment style --------- Signed-off-by: Ryan Shaw <587812+ryanshaw@users.noreply.github.com> Co-authored-by: John Luo --- .../hooks/useGetPatientRelationships.js | 41 ++++++++++ .../hooks/useOHDirectScheduling.js | 2 + .../vaos/new-appointment/redux/actions.js | 29 ++++++- .../vaos/new-appointment/redux/reducer.js | 21 +++++ .../vaos/new-appointment/redux/selectors.js | 8 ++ src/applications/vaos/services/mocks/index.js | 4 + .../v2/patient_provider_relationships.json | 64 +++++++++++++++ .../vaos/services/patient/index.js | 78 ++++++++----------- .../vaos/services/patient/index.unit.spec.js | 55 +++++++++++++ .../vaos/services/patient/transformers.js | 27 +++++++ .../vaos/services/patient/types.js | 60 ++++++++++++++ src/applications/vaos/services/vaos/index.js | 9 +++ 12 files changed, 352 insertions(+), 46 deletions(-) create mode 100644 src/applications/vaos/new-appointment/hooks/useGetPatientRelationships.js create mode 100644 src/applications/vaos/services/mocks/v2/patient_provider_relationships.json create mode 100644 src/applications/vaos/services/patient/index.unit.spec.js create mode 100644 src/applications/vaos/services/patient/transformers.js create mode 100644 src/applications/vaos/services/patient/types.js diff --git a/src/applications/vaos/new-appointment/hooks/useGetPatientRelationships.js b/src/applications/vaos/new-appointment/hooks/useGetPatientRelationships.js new file mode 100644 index 000000000000..7c34097836e2 --- /dev/null +++ b/src/applications/vaos/new-appointment/hooks/useGetPatientRelationships.js @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; +import { useSelector, shallowEqual, useDispatch } from 'react-redux'; +import { FETCH_STATUS } from '../../utils/constants'; +import { useOHDirectScheduling } from './useOHDirectScheduling'; +import { getPatientProviderRelationships } from '../redux/selectors'; +import { fetchPatientProviderRelationships } from '../redux/actions'; + +export function useGetPatientRelationships() { + const dispatch = useDispatch(); + const featureOHDirectSchedule = useOHDirectScheduling(); + + const { + patientProviderRelationships, + patientProviderRelationshipsStatus, + } = useSelector( + state => getPatientProviderRelationships(state), + shallowEqual, + ); + + useEffect( + () => { + if ( + featureOHDirectSchedule && + !patientProviderRelationships.length && + patientProviderRelationshipsStatus === FETCH_STATUS.notStarted + ) { + dispatch(fetchPatientProviderRelationships()); + } + }, + [ + dispatch, + featureOHDirectSchedule, + patientProviderRelationshipsStatus, + patientProviderRelationships, + ], + ); + return { + patientProviderRelationships, + patientProviderRelationshipsStatus, + }; +} diff --git a/src/applications/vaos/new-appointment/hooks/useOHDirectScheduling.js b/src/applications/vaos/new-appointment/hooks/useOHDirectScheduling.js index a5d5e7a9ce5d..7a9d638d6b12 100644 --- a/src/applications/vaos/new-appointment/hooks/useOHDirectScheduling.js +++ b/src/applications/vaos/new-appointment/hooks/useOHDirectScheduling.js @@ -2,6 +2,8 @@ import { shallowEqual, useSelector } from 'react-redux'; import { getFacilityPageV2Info } from '../redux/selectors'; import { selectFeatureOHDirectSchedule } from '../../redux/selectors'; +// Currently we are only allowing OH direct scheduling for Food and Nutrition +// appointments const OH_DIRECT_SCHEDULE_ENABLED_TYPES_OF_CARE = ['foodAndNutrition']; export function useOHDirectScheduling() { diff --git a/src/applications/vaos/new-appointment/redux/actions.js b/src/applications/vaos/new-appointment/redux/actions.js index 7beb1a854ab1..68e31d45d931 100644 --- a/src/applications/vaos/new-appointment/redux/actions.js +++ b/src/applications/vaos/new-appointment/redux/actions.js @@ -59,7 +59,10 @@ import { STARTED_NEW_APPOINTMENT_FLOW, FORM_SUBMIT_SUCCEEDED, } from '../../redux/sitewide'; -import { fetchFlowEligibilityAndClinics } from '../../services/patient'; +import { + fetchFlowEligibilityAndClinics, + fetchPatientRelationships, +} from '../../services/patient'; import { getTimezoneByFacilityId } from '../../utils/timezone'; import { getCommunityCareV2 } from '../../services/vaos/index'; @@ -143,6 +146,12 @@ export const FORM_REQUESTED_PROVIDERS_FAILED = 'newAppointment/FORM_REQUESTED_PROVIDERS_FAILED'; export const FORM_PAGE_CC_FACILITY_SORT_METHOD_UPDATED = 'newAppointment/FORM_PAGE_CC_FACILITY_SORT_METHOD_UPDATED'; +export const FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS = + 'newAppointment/FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS'; +export const FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS_SUCCEEDED = + 'newAppointment/FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS_SUCCEEDED'; +export const FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS_FAILED = + 'newAppointment/FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS_FAILED'; export function openFormPage(page, uiSchema, schema) { return { @@ -217,6 +226,23 @@ export function startRequestAppointmentFlow(isCommunityCare) { }; } +export function fetchPatientProviderRelationships() { + return async dispatch => { + try { + dispatch({ type: FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS }); + + const patientProviderRelationships = await fetchPatientRelationships(); + + dispatch({ type: FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS_SUCCEEDED }); + + return patientProviderRelationships; + } catch (e) { + dispatch({ type: FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS_FAILED }); + return captureError(e); + } + }; +} + export function fetchFacilityDetails(facilityId) { let facilityDetails; @@ -241,6 +267,7 @@ export function fetchFacilityDetails(facilityId) { }); }; } + export function checkEligibility({ location, showModal }) { return async (dispatch, getState) => { const state = getState(); diff --git a/src/applications/vaos/new-appointment/redux/reducer.js b/src/applications/vaos/new-appointment/redux/reducer.js index 0146ec47fb55..270f483f606b 100644 --- a/src/applications/vaos/new-appointment/redux/reducer.js +++ b/src/applications/vaos/new-appointment/redux/reducer.js @@ -49,6 +49,9 @@ import { FORM_REQUESTED_PROVIDERS, FORM_REQUESTED_PROVIDERS_SUCCEEDED, FORM_REQUESTED_PROVIDERS_FAILED, + FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS, + FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS_SUCCEEDED, + FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS_FAILED, } from './actions'; import { @@ -87,6 +90,8 @@ const initialState = { facilityDetails: {}, clinics: {}, eligibility: {}, + patientProviderRelationships: [], + patientProviderRelationshipsStatus: FETCH_STATUS.notStarted, parentFacilities: null, ccEnabledSystems: null, pageChangeInProgress: false, @@ -587,6 +592,22 @@ export default function formReducer(state = initialState, action) { }, flowType: FLOW_TYPES.REQUEST, }; + case FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS: + return { + ...state, + patientProviderRelationshipsStatus: FETCH_STATUS.loading, + }; + case FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS_SUCCEEDED: + return { + ...state, + patientProviderRelationshipsStatus: FETCH_STATUS.succeeded, + patientProviderRelationships: action.patientProviderRelationships, + }; + case FORM_FETCH_PATIENT_PROVIDER_RELATIONSHIPS_FAILED: + return { + ...state, + patientProviderRelationshipsStatus: FETCH_STATUS.failed, + }; case FORM_FETCH_FACILITY_DETAILS: return { ...state, diff --git a/src/applications/vaos/new-appointment/redux/selectors.js b/src/applications/vaos/new-appointment/redux/selectors.js index ad0edc4b3da2..1e42b3a93640 100644 --- a/src/applications/vaos/new-appointment/redux/selectors.js +++ b/src/applications/vaos/new-appointment/redux/selectors.js @@ -315,6 +315,14 @@ export function selectChosenFacilityInfo(state) { ); } +export function getPatientProviderRelationships(state) { + return { + patientProviderRelationships: state.patientProviderRelationships, + patientProviderRelationshipsStatus: + state.patientProviderRelationshipsStatus, + }; +} + export function getChosenVACityState(state) { const schema = state.newAppointment.pages.ccPreferences?.properties.communityCareSystemId; diff --git a/src/applications/vaos/services/mocks/index.js b/src/applications/vaos/services/mocks/index.js index 1dd92c9140a1..9dbcbe414d57 100644 --- a/src/applications/vaos/services/mocks/index.js +++ b/src/applications/vaos/services/mocks/index.js @@ -30,6 +30,7 @@ const schedulingConfigurationsCC = require('./v2/scheduling_configurations_cc.js const schedulingConfigurations = require('./v2/scheduling_configurations.json'); const appointmentSlotsV2 = require('./v2/slots.json'); const clinicsV2 = require('./v2/clinics.json'); +const patientProviderRelationships = require('./v2/patient_provider_relationships.json'); // To locally test appointment details null state behavior, comment out // the inclusion of confirmed.json and uncomment the inclusion of @@ -496,6 +497,9 @@ const responses = { data: [], }); }, + 'GET /vaos/v2/relationships': (req, res) => { + return res.json(patientProviderRelationships); + }, // EPS api 'GET /vaos/v2/epsApi/referralDetails': (req, res) => { diff --git a/src/applications/vaos/services/mocks/v2/patient_provider_relationships.json b/src/applications/vaos/services/mocks/v2/patient_provider_relationships.json new file mode 100644 index 000000000000..631fe785202d --- /dev/null +++ b/src/applications/vaos/services/mocks/v2/patient_provider_relationships.json @@ -0,0 +1,64 @@ +{ + "data": [ + { + "type": "relationship", + "attributes": { + "type": "string", + "attributes": { + "provider": { + "cernerId": "Practitioner/123456", + "name": "Doe, John D, MD" + }, + "location": { + "vhaFacilityId": "string", + "name": "Marion VA Clinic" + }, + "clinic": { + "vistaSite": "534", + "ien": "6569", + "name": "Zanesville Primary Care" + }, + "serviceType": { + "coding": [ + { + "code": "Routine Follow-up" + } + ], + "text": "string" + }, + "lastSeen": "2024-11-26T00:32:34.216Z" + } + } + }, + { + "type": "relationship", + "attributes": { + "type": "string", + "attributes": { + "provider": { + "cernerId": "Practitioner/1111", + "name": "Doe, Mary D, MD" + }, + "location": { + "vhaFacilityId": "string", + "name": "Marion VA Clinic" + }, + "clinic": { + "vistaSite": "534", + "ien": "6569", + "name": "Zanesville Primary Care" + }, + "serviceType": { + "coding": [ + { + "code": "New Problem" + } + ], + "text": "string" + }, + "lastSeen": "2024-10-15T00:32:34.216Z" + } + } + } + ] +} diff --git a/src/applications/vaos/services/patient/index.js b/src/applications/vaos/services/patient/index.js index eea08c4fae29..3583c984962a 100644 --- a/src/applications/vaos/services/patient/index.js +++ b/src/applications/vaos/services/patient/index.js @@ -8,42 +8,9 @@ import { captureError } from '../../utils/error'; import { ELIGIBILITY_REASONS } from '../../utils/constants'; import { promiseAllFromObject } from '../../utils/data'; import { getAvailableHealthcareServices } from '../healthcare-service'; -import { getPatientEligibility } from '../vaos'; +import { getPatientEligibility, getPatientRelationships } from '../vaos'; import { getLongTermAppointmentHistoryV2 } from '../appointment'; - -/** - * @typedef PatientEligibilityForType - * @global - * - * @property {boolean} hasRequiredAppointmentHistory Has had appointment in the past that meets VATS requirements - * - Mapped from past visits check - * @property {?boolean} isEligibleForNewAppointmentRequest Is under the request limit - * - Mapped from request limits check - */ - -/** - * @typedef PatientEligibility - * @global - * - * @property {?PatientEligibilityForType} direct Patient eligibility for direct scheduling - * @property {?PatientEligibilityForType} request Patient eligibility for requests - */ - -/** - * @typedef {'error'|'overRequestLimit'|'noEnabled'|'notSupported'|'noRecentVisit'| - * 'noClinics'|'noMatchingClinics'} EligibilityReason - * @global - */ - -/** - * @typedef FlowEligibility - * @global - * - * @property {boolean} direct Can the patient use the direct schedule flow - * @property {Array} directReason The reason the patient isn't eligible for direct flow - * @property {boolean} request Can the patient use the request flow - * @property {Array} requestReason The reason the patient isn't eligible for request flow - */ +import { transformPatientRelationships } from './transformers'; function createErrorHandler(errorKey) { return data => { @@ -137,6 +104,37 @@ export async function fetchPatientEligibility({ return output; } +/** + * Fetch the logged in user's patient/provider relationships + * + * @export + * @async + * @param {Object} params + * @param {TypeOfCare} params.typeOfCare Type of care object for which to check patient relationships + * @param {Location} params.location Location of where patient should have relationships checked, + * @returns {Array} clinics An array of clinics pulled when checking eligibility - * @property {Array} pastAppointments An array of untransformed appointments pulled - * when checking eligibility - */ - /** * Checks eligibility for new appointment flow and returns * results, plus clinics and past appointments fetched along the way diff --git a/src/applications/vaos/services/patient/index.unit.spec.js b/src/applications/vaos/services/patient/index.unit.spec.js new file mode 100644 index 000000000000..50476c62a1ea --- /dev/null +++ b/src/applications/vaos/services/patient/index.unit.spec.js @@ -0,0 +1,55 @@ +import { expect } from 'chai'; +import { mockFetch, setFetchJSONResponse } from 'platform/testing/unit/helpers'; +import { fetchPatientRelationships } from '.'; + +describe('VAOS Services: Patient ', () => { + describe('fetchPatientRelationships', () => { + beforeEach(() => { + mockFetch(); + }); + + it('should make successful request', async () => { + const relationships = [ + { + type: 'relationship', + attributes: { + type: 'string', + attributes: { + provider: { + cernerId: 'Practitioner/123456', + name: 'Doe, John D, MD', + }, + location: { + vhaFacilityId: 'string', + name: 'Marion VA Clinic', + }, + clinic: { + vistaSite: '534', + ien: '6569', + name: 'Zanesville Primary Care', + }, + serviceType: { + coding: [ + { + code: 'Routine Follow-up', + }, + ], + text: 'string', + }, + lastSeen: '2024-11-26T00:32:34.216Z', + }, + }, + }, + ]; + + setFetchJSONResponse(global.fetch, { data: relationships }); + + const data = await fetchPatientRelationships(); + + expect(global.fetch.firstCall.args[0]).to.contain( + `/vaos/v2/relationships`, + ); + expect(data[0].providerName).to.equal('Doe, John D, MD'); + }); + }); +}); diff --git a/src/applications/vaos/services/patient/transformers.js b/src/applications/vaos/services/patient/transformers.js new file mode 100644 index 000000000000..9ae7fb167a0d --- /dev/null +++ b/src/applications/vaos/services/patient/transformers.js @@ -0,0 +1,27 @@ +/** + * @module services/Patient/transformers + */ + +/** + * Transforms a patient relationship object returned from the VPG endpoint to the + * VAOS PatientProviderRelationship format + * + * @export + * @param {patientRelationship} patientRelationships A patient relationships object returned from VPG + * @returns {Array ({ + resourceType: 'PatientProviderRelationship', + providerName: relationship.attributes.attributes.provider.name, + providerId: relationship.attributes.attributes.provider.cernerId, + serviceType: relationship.attributes.attributes.serviceType.coding[0].code, + locationName: relationship.attributes.attributes.location.name, + clinicName: relationship.attributes.attributes.clinic.name, + vistaId: relationship.attributes.attributes.clinic.vistaSite, + lastSeen: relationship.attributes.attributes.lastSeen, + })); +} diff --git a/src/applications/vaos/services/patient/types.js b/src/applications/vaos/services/patient/types.js new file mode 100644 index 000000000000..ef11cc06948b --- /dev/null +++ b/src/applications/vaos/services/patient/types.js @@ -0,0 +1,60 @@ +/** + * @summary + * Patient Provider Relationship + * + * @typedef {Object} PatientProviderRelationship + * @global + * + * @property {'PatientProviderRelationship'} resourceType Static resource type string + * @property {String} providerName The full name of the provider + * @property {String} providerId The Cerner id for the provider + * @property {String} serviceType The service type code + * @property {String} loctionName The VA facility name + * @property {String} clinicName The clinic name + * @property {String} vistaId The three digit VistA id + * @property {string} lastSeen Date in ISO format of the when the patient was last seen by this provider + */ + +/** + * @typedef PatientEligibilityForType + * @global + * + * @property {boolean} hasRequiredAppointmentHistory Has had appointment in the past that meets VATS requirements + * - Mapped from past visits check + * @property {?boolean} isEligibleForNewAppointmentRequest Is under the request limit + * - Mapped from request limits check + */ + +/** + * @typedef PatientEligibility + * @global + * + * @property {?PatientEligibilityForType} direct Patient eligibility for direct scheduling + * @property {?PatientEligibilityForType} request Patient eligibility for requests + */ + +/** + * @typedef {'error'|'overRequestLimit'|'noEnabled'|'notSupported'|'noRecentVisit'| + * 'noClinics'|'noMatchingClinics'} EligibilityReason + * @global + */ + +/** + * @typedef FlowEligibility + * @global + * + * @property {boolean} direct Can the patient use the direct schedule flow + * @property {Array} directReason The reason the patient isn't eligible for direct flow + * @property {boolean} request Can the patient use the request flow + * @property {Array} requestReason The reason the patient isn't eligible for request flow + */ + +/** + * @typedef FlowEligibilityReturnData + * @global + * + * @property {FlowEligibility} eligibility The eligibility info for the patient + * @property {Array} clinics An array of clinics pulled when checking eligibility + * @property {Array} pastAppointments An array of untransformed appointments pulled + * when checking eligibility + */ diff --git a/src/applications/vaos/services/vaos/index.js b/src/applications/vaos/services/vaos/index.js index 93a39028e704..d791fa61519f 100644 --- a/src/applications/vaos/services/vaos/index.js +++ b/src/applications/vaos/services/vaos/index.js @@ -108,6 +108,15 @@ export function getPatientEligibility( ).then(parseApiObject); } +export function getPatientRelationships() { + // TODO: https://github.com/department-of-veterans-affairs/va.gov-team/issues/98864 + // export function getPatientRelationships({ locationId, typeOfCareId }) { + return apiRequestWithUrl( + // `/vaos/v2/relationships?facility_id=${locationId}&clinical_service_id=${typeOfCareId}`, + `/vaos/v2/relationships`, + ); +} + export function getFacilityById(id) { return apiRequestWithUrl(`/vaos/v2/facilities/${id}`).then(parseApiObject); }