From 5b12bcc32775bc3f749a3e22c9add41a72be3fba Mon Sep 17 00:00:00 2001 From: mattwrightva <107576133+mattwrightva@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:48:44 -0700 Subject: [PATCH] MHV-65380: Appointments API call being fed dates to filter by (#33669) * MHV-65380: Appointments API call being fed dates to filter by * MHV-65380: Invalidate appointments when selected date changes * MHV-65380: Fixed and added unit tests --- .../actions/blueButtonReport.js | 36 ++++++++-- .../mhv-medical-records/actions/downloads.js | 1 + .../mhv-medical-records/api/MrApi.js | 9 +-- .../DownloadRecords/DownloadFileType.jsx | 30 ++++---- .../DownloadRecords/DownloadRecordType.jsx | 19 ++--- .../reducers/blueButton.js | 6 ++ .../mhv-medical-records/reducers/downloads.js | 1 + .../actions/blueButtonReport.unit.spec.js | 71 +++++++++++++++++++ .../tests/actions/download.unit.spec.js | 2 +- .../tests/api/MrApi.unit.spec.js | 32 ++++++++- .../mhv-medical-records/util/actionTypes.js | 1 + 11 files changed, 169 insertions(+), 39 deletions(-) diff --git a/src/applications/mhv-medical-records/actions/blueButtonReport.js b/src/applications/mhv-medical-records/actions/blueButtonReport.js index 0cadc2bfbf2f..05a8fe93b022 100644 --- a/src/applications/mhv-medical-records/actions/blueButtonReport.js +++ b/src/applications/mhv-medical-records/actions/blueButtonReport.js @@ -1,3 +1,4 @@ +import { formatISO, subYears } from 'date-fns'; import { getLabsAndTests, getNotes, @@ -18,7 +19,10 @@ export const clearFailedList = domain => dispatch => { dispatch({ type: Actions.BlueButtonReport.CLEAR_FAILED, payload: domain }); }; -export const getBlueButtonReportData = (options = {}) => async dispatch => { +export const getBlueButtonReportData = ( + options = {}, + dateFilter, +) => async dispatch => { const fetchMap = { labsAndTests: getLabsAndTests, notes: getNotes, @@ -36,15 +40,37 @@ export const getBlueButtonReportData = (options = {}) => async dispatch => { const promises = Object.entries(fetchMap) .filter(([key]) => options[key]) // Only include enabled fetches - .map(([key, fetchFn]) => - fetchFn() + .map(([key, fetchFn]) => { + if (key === 'appointments') { + let fromDate; + let toDate; + if (dateFilter.option === 'any') { + const currentDate = new Date(); + const dateTwoYearsAgo = subYears(currentDate, 2); + const farFutureDate = new Date(2099, 0, 1); // January 1, 2099 + fromDate = formatISO(dateTwoYearsAgo); + toDate = formatISO(farFutureDate); + } else { + fromDate = formatISO(new Date(dateFilter.fromDate)); + toDate = formatISO(new Date(dateFilter.toDate)); + } + + return fetchFn(fromDate, toDate) + .then(response => ({ key, response })) + .catch(error => { + const newError = new Error(error); + newError.key = key; + throw newError; + }); + } + return fetchFn() .then(response => ({ key, response })) .catch(error => { const newError = new Error(error); newError.key = key; throw newError; - }), - ); + }); + }); const results = await Promise.allSettled(promises); diff --git a/src/applications/mhv-medical-records/actions/downloads.js b/src/applications/mhv-medical-records/actions/downloads.js index 6d61724180b1..3014f6f866ac 100644 --- a/src/applications/mhv-medical-records/actions/downloads.js +++ b/src/applications/mhv-medical-records/actions/downloads.js @@ -68,6 +68,7 @@ export const updateReportDateRange = ( toDate, }, }); + dispatch({ type: Actions.BlueButtonReport.CLEAR_APPOINTMENTS }); }; export const updateReportRecordType = selectedTypes => async dispatch => { diff --git a/src/applications/mhv-medical-records/api/MrApi.js b/src/applications/mhv-medical-records/api/MrApi.js index 68bb0c2ec992..f4c5a446df99 100644 --- a/src/applications/mhv-medical-records/api/MrApi.js +++ b/src/applications/mhv-medical-records/api/MrApi.js @@ -1,6 +1,5 @@ import environment from '@department-of-veterans-affairs/platform-utilities/environment'; import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/exports'; -import { formatISO } from 'date-fns'; import { findMatchingPhrAndCvixStudies } from '../util/radiologyUtil'; import edipiNotFound from '../util/edipiNotFound'; @@ -219,14 +218,10 @@ export const getMedications = async () => { * Get a patient's appointments * @returns list of patient's appointments */ -export const getAppointments = async () => { - const beginningOfTime = new Date(0); - const farFutureDate = new Date(2100, 0, 1); // January 1, 2100 - const startDate = formatISO(beginningOfTime); - const endDate = formatISO(farFutureDate); +export const getAppointments = async (fromDate, toDate) => { const statusParams = '&statuses[]=booked&statuses[]=arrived&statuses[]=fulfilled&statuses[]=cancelled'; - const params = `_include=facilities,clinics&start=${startDate}&end=${endDate}${statusParams}`; + const params = `_include=facilities,clinics&start=${fromDate}&end=${toDate}${statusParams}`; return apiRequest(`${environment.API_URL}/vaos/v2/appointments?${params}`, { headers, diff --git a/src/applications/mhv-medical-records/components/DownloadRecords/DownloadFileType.jsx b/src/applications/mhv-medical-records/components/DownloadRecords/DownloadFileType.jsx index c32a1b2bf063..210cbe35337f 100644 --- a/src/applications/mhv-medical-records/components/DownloadRecords/DownloadFileType.jsx +++ b/src/applications/mhv-medical-records/components/DownloadRecords/DownloadFileType.jsx @@ -75,6 +75,8 @@ const DownloadFileType = props => { const [downloadStarted, setDownloadStarted] = useState(false); + const { fromDate, toDate, option: dateFilterOption } = dateFilter; + useEffect( () => { focusElement(document.querySelector('h1')); @@ -85,26 +87,26 @@ const DownloadFileType = props => { useEffect( () => { - if (!dateFilter) { + if (!dateFilterOption) { history.push('/download/date-range'); } else if (!recordFilter) { history.push('/download/record-type'); } }, - [dateFilter, history, recordFilter], + [dateFilterOption, history, recordFilter], ); const filterByDate = useCallback( recDate => { - if (dateFilter.option === 'any') { + if (dateFilterOption === 'any') { return true; } return ( - isBefore(new Date(dateFilter.fromDate), new Date(recDate)) && - isAfter(new Date(dateFilter.toDate), new Date(recDate)) + isBefore(new Date(fromDate), new Date(recDate)) && + isAfter(new Date(toDate), new Date(recDate)) ); }, - [dateFilter], + [dateFilterOption, fromDate, toDate], ); /** @@ -191,10 +193,10 @@ const DownloadFileType = props => { }; if (!isDataFetched) { - dispatch(getBlueButtonReportData(options)); + dispatch(getBlueButtonReportData(options, dateFilter)); } }, - [isDataFetched, recordFilter, dispatch], + [isDataFetched, recordFilter, dispatch, dateFilter], ); const recordData = useMemo( @@ -306,13 +308,9 @@ const DownloadFileType = props => { const pdfName = `VA-Blue-Button-report-${getNameDateAndTime(user)}`; const pdfData = { fromDate: - dateFilter?.fromDate && dateFilter.fromDate !== 'any' - ? formatDateLong(dateFilter.fromDate) - : 'Any', + fromDate && fromDate !== 'any' ? formatDateLong(fromDate) : 'Any', toDate: - dateFilter?.fromDate && dateFilter.fromDate !== 'any' - ? formatDateLong(dateFilter.toDate) - : 'any', + fromDate && fromDate !== 'any' ? formatDateLong(toDate) : 'any', recordSets: generateBlueButtonData(recordData, recordFilter), ...scaffold, name, @@ -337,8 +335,8 @@ const DownloadFileType = props => { } }, [ - dateFilter.fromDate, - dateFilter.toDate, + fromDate, + toDate, dispatch, dob, isDataFetched, diff --git a/src/applications/mhv-medical-records/components/DownloadRecords/DownloadRecordType.jsx b/src/applications/mhv-medical-records/components/DownloadRecords/DownloadRecordType.jsx index 208bd0d28e3c..0f419b495ea6 100644 --- a/src/applications/mhv-medical-records/components/DownloadRecords/DownloadRecordType.jsx +++ b/src/applications/mhv-medical-records/components/DownloadRecords/DownloadRecordType.jsx @@ -30,6 +30,7 @@ const DownloadRecordType = () => { const [milServCheck, setMilServCheck] = useState(false); const dateFilter = useSelector(state => state.mr.downloads?.dateFilter); + const { fromDate, toDate, option: dateFilterOption } = dateFilter; const [selectedRecords, setSelectedRecords] = useState([]); const [selectionError, setSelectionError] = useState(null); @@ -98,24 +99,24 @@ const DownloadRecordType = () => { useEffect( () => { - if (!dateFilter) { + if (!dateFilterOption) { history.push('/download/date-range'); } }, - [dateFilter, history], + [dateFilterOption, history], ); const selectedDateRange = useMemo( () => { - if (dateFilter?.option === 'any') { + if (dateFilterOption === 'any') { return 'Any'; } - if (dateFilter?.option === 'custom') { + if (dateFilterOption === 'custom') { return 'Custom'; } - return `Last ${dateFilter?.option} months`; + return `Last ${dateFilterOption} months`; }, - [dateFilter], + [dateFilterOption], ); return ( @@ -136,9 +137,9 @@ const DownloadRecordType = () => {
Date range: {selectedDateRange}{' '} - {dateFilter && dateFilter?.option !== 'any' - ? `(${format(new Date(dateFilter?.fromDate), 'PPP')} to ${format( - new Date(dateFilter?.toDate), + {dateFilterOption && dateFilterOption !== 'any' + ? `(${format(new Date(fromDate), 'PPP')} to ${format( + new Date(toDate), 'PPP', )})` : ''} diff --git a/src/applications/mhv-medical-records/reducers/blueButton.js b/src/applications/mhv-medical-records/reducers/blueButton.js index 81fe79940229..ab1ff217ac2b 100644 --- a/src/applications/mhv-medical-records/reducers/blueButton.js +++ b/src/applications/mhv-medical-records/reducers/blueButton.js @@ -360,6 +360,12 @@ export const blueButtonReducer = (state = initialState, action) => { failedDomains: [], }; } + case Actions.BlueButtonReport.CLEAR_APPOINTMENTS: { + return { + ...state, + appointmentsList: undefined, + }; + } default: return state; } diff --git a/src/applications/mhv-medical-records/reducers/downloads.js b/src/applications/mhv-medical-records/reducers/downloads.js index 3a9574b94f9a..40e26169d76c 100644 --- a/src/applications/mhv-medical-records/reducers/downloads.js +++ b/src/applications/mhv-medical-records/reducers/downloads.js @@ -16,6 +16,7 @@ const initialState = { * @type {Boolean} */ error: false, + dateFilter: {}, }; export const downloadsReducer = (state = initialState, action) => { diff --git a/src/applications/mhv-medical-records/tests/actions/blueButtonReport.unit.spec.js b/src/applications/mhv-medical-records/tests/actions/blueButtonReport.unit.spec.js index 6bb175c9afb9..7f4f2faae110 100644 --- a/src/applications/mhv-medical-records/tests/actions/blueButtonReport.unit.spec.js +++ b/src/applications/mhv-medical-records/tests/actions/blueButtonReport.unit.spec.js @@ -1,12 +1,14 @@ import { expect } from 'chai'; import { mockApiRequest } from '@department-of-veterans-affairs/platform-testing/helpers'; import sinon from 'sinon'; +import { formatISO, subYears } from 'date-fns'; import { getBlueButtonReportData, clearFailedList, } from '../../actions/blueButtonReport'; import { Actions } from '../../util/actionTypes'; import allergies from '../fixtures/allergies.json'; +import * as MrApi from '../../api/MrApi'; describe('Blue Button Actions', () => { describe('getBlueButtonReportData', () => { @@ -159,6 +161,75 @@ describe('Blue Button Actions', () => { ); }); }); + + it('should fetch appointments with the correct date range when dateFilter.option is "any"', async () => { + const mockData = { mockData: 'appointmentsMockData' }; + const getAppointmentsStub = sinon + .stub(MrApi, 'getAppointments') + .resolves(mockData); + + const dispatch = sinon.spy(); + const action = getBlueButtonReportData( + { appointments: true }, + { option: 'any' }, + ); + await action(dispatch); + + // Verify that getAppointments was called once + expect(getAppointmentsStub.calledOnce).to.be.true; + + // The 'any' option should calculate fromDate as 2 years ago and toDate as far future + const currentDate = new Date(); + const dateTwoYearsAgo = subYears(currentDate, 2); + const farFutureDate = new Date(2099, 0, 1); + + const expectedFromDate = formatISO(dateTwoYearsAgo); + const expectedToDate = formatISO(farFutureDate); + + const [fromDateArg, toDateArg] = getAppointmentsStub.firstCall.args; + expect(fromDateArg).to.equal(expectedFromDate); + expect(toDateArg).to.equal(expectedToDate); + + // Verify dispatch + expect(dispatch.calledOnce).to.be.true; + const dispatchedAction = dispatch.firstCall.args[0]; + expect(dispatchedAction.type).to.equal(Actions.BlueButtonReport.GET); + expect(dispatchedAction.appointmentsResponse).to.deep.equal(mockData); + + getAppointmentsStub.restore(); + }); + + it('should fetch appointments with the provided custom dateFilter range', async () => { + const mockData = { mockData: 'appointmentsMockData' }; + const getAppointmentsStub = sinon + .stub(MrApi, 'getAppointments') + .resolves(mockData); + + const fromDateCustom = '2021-01-01'; + const toDateCustom = '2021-12-31'; + const dispatch = sinon.spy(); + const action = getBlueButtonReportData( + { appointments: true }, + { fromDate: fromDateCustom, toDate: toDateCustom }, + ); + await action(dispatch); + + // Verify that getAppointments was called once + expect(getAppointmentsStub.calledOnce).to.be.true; + + // Check arguments passed to getAppointments + const [fromDateArg, toDateArg] = getAppointmentsStub.firstCall.args; + expect(fromDateArg).to.equal(formatISO(new Date(fromDateCustom))); + expect(toDateArg).to.equal(formatISO(new Date(toDateCustom))); + + // Verify dispatch + expect(dispatch.calledOnce).to.be.true; + const dispatchedAction = dispatch.firstCall.args[0]; + expect(dispatchedAction.type).to.equal(Actions.BlueButtonReport.GET); + expect(dispatchedAction.appointmentsResponse).to.deep.equal(mockData); + + getAppointmentsStub.restore(); + }); }); it('should not dispatch any actions if no options are enabled', async () => { diff --git a/src/applications/mhv-medical-records/tests/actions/download.unit.spec.js b/src/applications/mhv-medical-records/tests/actions/download.unit.spec.js index 67c50a339cb1..2590e6ed00b2 100644 --- a/src/applications/mhv-medical-records/tests/actions/download.unit.spec.js +++ b/src/applications/mhv-medical-records/tests/actions/download.unit.spec.js @@ -87,7 +87,7 @@ describe('Download Actions', () => { const fromDate = 'from'; const toDate = 'to'; updateReportDateRange(option, fromDate, toDate)(dispatch); - expect(dispatch.calledOnce).to.be.true; + expect(dispatch.calledTwice).to.be.true; expect(dispatch.firstCall.args[0].type).to.equal( Actions.Downloads.SET_DATE_FILTER, ); diff --git a/src/applications/mhv-medical-records/tests/api/MrApi.unit.spec.js b/src/applications/mhv-medical-records/tests/api/MrApi.unit.spec.js index 69f63a7205a9..8d6b3db821e9 100644 --- a/src/applications/mhv-medical-records/tests/api/MrApi.unit.spec.js +++ b/src/applications/mhv-medical-records/tests/api/MrApi.unit.spec.js @@ -2,6 +2,7 @@ import fs from 'fs'; import { expect } from 'chai'; import { mockApiRequest } from '@department-of-veterans-affairs/platform-testing/helpers'; import Sinon from 'sinon'; +import environment from '@department-of-veterans-affairs/platform-utilities/environment'; import labsAndTests from '../fixtures/labsAndTests.json'; import pathology from '../fixtures/pathology.json'; import notes from '../fixtures/notes.json'; @@ -261,10 +262,39 @@ describe('Get appointments api call', () => { const mockData = appointments; mockApiRequest(mockData); - return getAppointments().then(res => { + const fromDate = '2020-01-01T00:00:00Z'; + const toDate = '2020-12-31T23:59:59Z'; + + return getAppointments(fromDate, toDate).then(res => { expect(res.data.length).to.equal(2); }); }); + + it('should call the correct endpoint with provided from and to dates', async () => { + const fetchStub = Sinon.stub(global, 'fetch'); + const fromDate = '2021-01-01T00:00:00Z'; + const toDate = '2021-12-31T23:59:59Z'; + + const mockResponse = new Response(JSON.stringify({ data: [{}, {}] }), { + status: 200, + headers: { 'Content-type': 'application/json' }, + }); + + fetchStub.resolves(mockResponse); + + const result = await getAppointments(fromDate, toDate); + expect(fetchStub.calledOnce).to.be.true; + + const statusParams = + '&statuses[]=booked&statuses[]=arrived&statuses[]=fulfilled&statuses[]=cancelled'; + const expectedUrl = `${ + environment.API_URL + }/vaos/v2/appointments?_include=facilities,clinics&start=${fromDate}&end=${toDate}${statusParams}`; + expect(fetchStub.firstCall.args[0]).to.equal(expectedUrl); + expect(result.data.length).to.equal(2); + + fetchStub.restore(); + }); }); describe('Get demographic info api call', () => { diff --git a/src/applications/mhv-medical-records/util/actionTypes.js b/src/applications/mhv-medical-records/util/actionTypes.js index 06f96d5a0b67..541326c167ee 100644 --- a/src/applications/mhv-medical-records/util/actionTypes.js +++ b/src/applications/mhv-medical-records/util/actionTypes.js @@ -86,6 +86,7 @@ export const Actions = { GET: 'MR_BLUE_BUTTON_GET_DATA', ADD_FAILED: 'MR_BLUE_BUTTON_ADD_FAILED', CLEAR_FAILED: 'MR_BLUE_BUTTON_CLEAR_FAILED', + CLEAR_APPOINTMENTS: 'MR_CLEAR_APPOINTMENTS', }, PageTracker: { SET_PAGE_TRACKER: 'MR_SET_PAGE_TRACKER',