From dc613b26cfe9b1f4aafad079350cffee9a0b47ee Mon Sep 17 00:00:00 2001 From: Kyle Henson <145150351+khenson-oddball@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:58:55 -0700 Subject: [PATCH] Add veteran status confirmation integration (#34067) --- .../ProofOfVeteranStatus.jsx | 320 +++++++++++++----- .../ProofOfVeteranStatusNew.jsx | 280 +++++++++++---- .../ProofOfVeteranStatusNew.unit.spec.jsx | 129 ++++++- .../vet-verification-status/index.js | 29 ++ .../personalization/profile/mocks/server.js | 6 + .../MilitaryInformation.unit.spec.jsx | 23 +- .../PeriodOfServiceTypeText.unit.spec.jsx | 12 + .../proof-of-veteran-status.cypress.spec.js | 6 + 8 files changed, 660 insertions(+), 145 deletions(-) create mode 100644 src/applications/personalization/profile/mocks/endpoints/vet-verification-status/index.js diff --git a/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatus.jsx b/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatus.jsx index e5424c1625e0..0fe58bade12b 100644 --- a/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatus.jsx +++ b/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatus.jsx @@ -6,6 +6,8 @@ import { generatePdf } from '~/platform/pdf'; import { focusElement } from '~/platform/utilities/ui'; import { captureError } from '~/platform/user/profile/vap-svc/util/analytics'; import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; +import { useFeatureToggle } from '~/platform/utilities/feature-toggles'; +import { apiRequest } from '~/platform/utilities/api'; import { formatFullName } from '../../../common/helpers'; import { getServiceBranchDisplayName } from '../../helpers'; @@ -23,6 +25,8 @@ const ProofOfVeteranStatus = ({ mockUserAgent, }) => { const [errors, setErrors] = useState([]); + const [data, setData] = useState(null); + const [shouldFocusError, setShouldFocusError] = useState(false); const { first, middle, last, suffix } = userFullName; const userAgent = @@ -69,13 +73,45 @@ const ProofOfVeteranStatus = ({ }, }; + const { TOGGLE_NAMES, useToggleValue } = useFeatureToggle(); + const useLighthouseApi = useToggleValue( + TOGGLE_NAMES.veteranStatusCardUseLighthouseFrontend, + ); + + useEffect(() => { + let isMounted = true; + + const fetchVerificationStatus = async () => { + try { + const path = '/profile/vet_verification_status'; + const response = await apiRequest(path); + if (isMounted) { + setData(response.data); + } + } catch (error) { + if (isMounted) { + setErrors([ + "We're sorry. There's a problem with our system. We can't show your Veteran status card right now. Try again later.", + ]); + captureError(error, { eventName: 'vet-status-fetch-verification' }); + } + } + }; + fetchVerificationStatus(); + + return () => { + isMounted = false; + }; + }, []); + useEffect( () => { - if (errors?.length > 0) { + if (shouldFocusError && errors?.length > 0) { focusElement('.vet-status-pdf-download-error'); + setShouldFocusError(false); } }, - [errors], + [shouldFocusError, errors], ); const createPdf = async () => { @@ -93,6 +129,7 @@ const ProofOfVeteranStatus = ({ "We're sorry. Something went wrong on our end. Please try to download your Veteran status card later.", ]); captureError(error, { eventName: 'vet-status-pdf-download' }); + setShouldFocusError(true); } }; @@ -123,93 +160,216 @@ const ProofOfVeteranStatus = ({ ); }); + const contactInfoElements = data?.attributes?.message?.map(item => { + const contactNumber = `${CONTACTS.DS_LOGON.slice( + 0, + 3, + )}-${CONTACTS.DS_LOGON.slice(3, 6)}-${CONTACTS.DS_LOGON.slice(6)}`; + const startIndex = item.indexOf(contactNumber); + + if (startIndex === -1) { + return item; + } + + const before = item.slice(0, startIndex); + const telephone = item.slice( + startIndex, + startIndex + contactNumber.length + 11, + ); + const after = item.slice(startIndex + telephone.length); + + return ( + <> + {before} + ( + ){after} + + ); + }); + return ( <> -
-

- Proof of Veteran status -

-

- You can use your Veteran status card to get discounts offered to - Veterans at many restaurants, hotels, stores, and other businesses. -

-

- Note: - This card doesn’t entitle you to any VA benefits. -

- - {vetStatusEligibility.confirmed ? ( - <> -
- -
+ {useLighthouseApi ? ( +
+

+ Proof of Veteran status +

+

+ You can use your Veteran status card to get discounts offered to + Veterans at many restaurants, hotels, stores, and other businesses. +

+

+ Note: + This card doesn’t entitle you to any VA benefits. +

- {errors?.length > 0 ? ( -
- - {errors[0]} - + {data?.attributes?.veteranStatus === 'confirmed' ? ( + <> +
+
- ) : null} - -
-
-
- sample proof of veteran status card featuring name, date of birth, disability rating and period of service + + {errors?.length > 0 ? ( +
+ + {errors[0]} + +
+ ) : null} + +
+
+
+ sample proof of veteran status card featuring name, date of birth, disability rating and period of service +
-
-
- - You can use our mobile app to get proof of Veteran status. - To get started, download the{' '} - VA: Health and Benefits mobile app. - - } - /> -
- - ) : null} - - {!vetStatusEligibility.confirmed && - vetStatusEligibility.message.length > 0 ? ( - <> -
- - {componentizedMessage.map((message, i) => { - if (i === 0) { - return ( -

- {message} -

- ); +
+ + You can use our mobile app to get proof of Veteran status. + To get started, download the{' '} + VA: Health and Benefits mobile app. + } - return

{message}

; - })} + /> +
+ + ) : null} + + {data?.attributes?.veteranStatus !== 'confirmed' && + data?.attributes?.message.length > 0 ? ( + <> +
+ + {contactInfoElements.map((message, i) => { + if (i === 0) { + return ( +

+ {message} +

+ ); + } + return

{message}

; + })} +
+
+ + ) : null} + + {errors?.length > 0 ? ( +
+ + {errors[0]}
- - ) : null} -
+ ) : null} +
+ ) : ( +
+

+ Proof of Veteran status +

+

+ You can use your Veteran status card to get discounts offered to + Veterans at many restaurants, hotels, stores, and other businesses. +

+

+ Note: + This card doesn’t entitle you to any VA benefits. +

+ + {vetStatusEligibility.confirmed ? ( + <> +
+ +
+ + {errors?.length > 0 ? ( +
+ + {errors[0]} + +
+ ) : null} + +
+
+
+ sample proof of veteran status card featuring name, date of birth, disability rating and period of service +
+
+
+
+ + You can use our mobile app to get proof of Veteran status. + To get started, download the{' '} + VA: Health and Benefits mobile app. + + } + /> +
+ + ) : null} + + {!vetStatusEligibility.confirmed && + vetStatusEligibility.message.length > 0 ? ( + <> +
+ + {componentizedMessage.map((message, i) => { + if (i === 0) { + return ( +

+ {message} +

+ ); + } + return

{message}

; + })} +
+
+ + ) : null} +
+ )} ); }; diff --git a/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatusNew.jsx b/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatusNew.jsx index 81df2cad97f7..d2533e9c4ba9 100644 --- a/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatusNew.jsx +++ b/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatusNew.jsx @@ -6,6 +6,8 @@ import { generatePdf } from '~/platform/pdf'; import { focusElement } from '~/platform/utilities/ui'; import { captureError } from '~/platform/user/profile/vap-svc/util/analytics'; import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; +import { useFeatureToggle } from '~/platform/utilities/feature-toggles'; +import { apiRequest } from '~/platform/utilities/api'; import { formatFullName } from '../../../common/helpers'; import { getServiceBranchDisplayName } from '../../helpers'; import ProofOfVeteranStatusCard from './ProofOfVeteranStatusCard/ProofOfVeteranStatusCard'; @@ -24,6 +26,8 @@ const ProofOfVeteranStatusNew = ({ mockUserAgent, }) => { const [errors, setErrors] = useState([]); + const [data, setData] = useState(null); + const [shouldFocusError, setShouldFocusError] = useState(false); const { first, middle, last, suffix } = userFullName; const userAgent = @@ -59,6 +63,8 @@ const ProofOfVeteranStatusNew = ({ serviceHistory.length && formattedFullName ); + const hasConfirmationData = !!(data && data.attributes); + const pdfData = { title: `Veteran status card for ${formattedFullName}`, details: { @@ -86,13 +92,45 @@ const ProofOfVeteranStatusNew = ({ }, }; + const { TOGGLE_NAMES, useToggleValue } = useFeatureToggle(); + const useLighthouseApi = useToggleValue( + TOGGLE_NAMES.veteranStatusCardUseLighthouseFrontend, + ); + + useEffect(() => { + let isMounted = true; + + const fetchVerificationStatus = async () => { + try { + const path = '/profile/vet_verification_status'; + const response = await apiRequest(path); + if (isMounted) { + setData(response.data); + } + } catch (error) { + if (isMounted) { + setErrors([ + "We're sorry. There's a problem with our system. We can't show your Veteran status card right now. Try again later.", + ]); + captureError(error, { eventName: 'vet-status-fetch-verification' }); + } + } + }; + fetchVerificationStatus(); + + return () => { + isMounted = false; + }; + }, []); + useEffect( () => { - if (errors?.length > 0) { + if (shouldFocusError && errors?.length > 0) { focusElement('.vet-status-pdf-download-error'); + setShouldFocusError(false); } }, - [errors], + [shouldFocusError, errors], ); const createPdf = async () => { @@ -140,6 +178,33 @@ const ProofOfVeteranStatusNew = ({ ); }); + const contactInfoElements = data?.attributes?.message?.map(item => { + const contactNumber = `${CONTACTS.DS_LOGON.slice( + 0, + 3, + )}-${CONTACTS.DS_LOGON.slice(3, 6)}-${CONTACTS.DS_LOGON.slice(6)}`; + const startIndex = item.indexOf(contactNumber); + + if (startIndex === -1) { + return item; + } + + const before = item.slice(0, startIndex); + const telephone = item.slice( + startIndex, + startIndex + contactNumber.length + 11, + ); + const after = item.slice(startIndex + telephone.length); + + return ( + <> + {before} + ( + ){after} + + ); + }); + return ( <>
@@ -152,74 +217,165 @@ const ProofOfVeteranStatusNew = ({ {userHasRequiredCardData ? ( <> - {vetStatusEligibility.confirmed ? ( + {!useLighthouseApi ? ( <> - {errors?.length > 0 ? ( -
- - {errors[0]} - -
+ {vetStatusEligibility.confirmed ? ( + <> + {errors?.length > 0 ? ( +
+ + {errors[0]} + +
+ ) : null} +
+
+ +
+
+
+ +
+
+ + You can use our mobile app to get proof of Veteran + status. To get started, download the{' '} + VA: Health and Benefits mobile + app. + + } + /> +
+ + ) : null} + {!vetStatusEligibility.confirmed && + vetStatusEligibility.message.length > 0 ? ( + <> +
+ + {componentizedMessage.map((message, i) => { + if (i === 0) { + return ( +

+ {message} +

+ ); + } + return

{message}

; + })} +
+
+ ) : null} -
-
- -
-
-
- -
-
- - You can use our mobile app to get proof of Veteran - status. To get started, download the{' '} - VA: Health and Benefits mobile app. - - } - /> -
) : null} - {!vetStatusEligibility.confirmed && - vetStatusEligibility.message.length > 0 ? ( + {useLighthouseApi && hasConfirmationData ? ( <> -
- - {componentizedMessage.map((message, i) => { - if (i === 0) { - return ( -

- {message} -

- ); - } - return

{message}

; - })} -
-
+ {data?.attributes?.veteranStatus === 'confirmed' ? ( + <> + {errors?.length > 0 ? ( +
+ + {errors[0]} + +
+ ) : null} +
+
+ +
+
+
+ +
+
+ + You can use our mobile app to get proof of Veteran + status. To get started, download the{' '} + VA: Health and Benefits mobile + app. + + } + /> +
+ + ) : null} + + {data?.attributes?.veteranStatus !== 'confirmed' && + data?.attributes?.message.length > 0 ? ( + <> +
+ + {contactInfoElements.map((message, i) => { + if (i === 0) { + return ( +

+ {message} +

+ ); + } + return

{message}

; + })} +
+
+ + ) : null} ) : null} + + {useLighthouseApi && !hasConfirmationData ? ( + +

+ We’re sorry. There’s a problem with our system. We can’t show + your Veteran status card right now. Try again later. +

+
+ ) : null} ) : ( { }); }); + describe('should fetch verification status on render', () => { + let apiRequestStub; + const initialState = createBasicInitialState( + [eligibleServiceHistoryItem], + confirmedEligibility, + true, + ); + + beforeEach(() => { + apiRequestStub = sinon.stub(api, 'apiRequest'); + }); + + afterEach(() => { + apiRequestStub.restore(); + }); + + it('displays the card successfully', async () => { + const mockData = { + data: { + id: '', + type: 'veteran_status_confirmations', + attributes: { veteranStatus: 'confirmed' }, + }, + }; + + apiRequestStub.resolves(mockData); + + const view = renderWithProfileReducers(, { + initialState, + }); + + sinon.assert.calledWith( + apiRequestStub, + '/profile/vet_verification_status', + ); + await waitFor(() => { + expect( + view.queryByText( + /Get proof of Veteran status on your mobile device/i, + ), + ).to.exist; + expect( + view.queryByText( + /We’re sorry. There’s a problem with your discharge status records. We can’t provide a Veteran status card for you right now./, + ), + ).to.not.exist; + }); + }); + + it('displays the returned not confirmed message', async () => { + const mockData = { + data: { + id: '', + type: 'veteran_status_confirmations', + attributes: { + veteranStatus: 'not confirmed', + notConfirmedReason: 'PERSON_NOT_FOUND', + message: problematicEligibility.message, + }, + }, + }; + + apiRequestStub.resolves(mockData); + const view = renderWithProfileReducers(, { + initialState, + }); + + await waitFor(() => { + expect( + view.queryByText( + /Get proof of Veteran Status on your mobile device/i, + ), + ).to.not.exist; + expect( + view.queryByText( + /We’re sorry. There’s a problem with your discharge status records. We can’t provide a Veteran status card for you right now./, + ), + ).to.exist; + }); + }); + + it('handles empty API response', async () => { + const mockData = { + data: {}, + }; + apiRequestStub.resolves(mockData); + const view = renderWithProfileReducers(, { + initialState, + }); + + await waitFor(() => { + expect( + view.queryByText( + 'We’re sorry. There’s a problem with our system. We can’t show your Veteran status card right now. Try again later.', + ), + ).to.exist; + }); + }); + + it('handles API error', async () => { + apiRequestStub.rejects(new Error('API Error')); + const view = renderWithProfileReducers(, { + initialState, + }); + + await waitFor(() => { + expect( + view.getByText( + 'We’re sorry. There’s a problem with our system. We can’t show your Veteran status card right now. Try again later.', + ), + ).to.exist; + }); + }); + }); + describe('when eligible', () => { const initialState = createBasicInitialState( [ diff --git a/src/applications/personalization/profile/mocks/endpoints/vet-verification-status/index.js b/src/applications/personalization/profile/mocks/endpoints/vet-verification-status/index.js new file mode 100644 index 000000000000..ba62fadf4807 --- /dev/null +++ b/src/applications/personalization/profile/mocks/endpoints/vet-verification-status/index.js @@ -0,0 +1,29 @@ +const confirmed = { + data: { + id: '', + type: 'veteran_status_confirmations', + attributes: { + veteranStatus: 'confirmed', + }, + }, +}; + +const notConfirmed = { + data: { + id: null, + type: 'veteran_status_confirmations', + attributes: { + veteranStatus: 'not confirmed', + notConfirmedReason: 'PERSON_NOT_FOUND', + message: [ + 'We’re sorry. There’s a problem with your discharge status records. We can’t provide a Veteran status card for you right now.', + 'To fix the problem with your records, call the Defense Manpower Data Center at 800-538-9552 (TTY: 711). They’re open Monday through Friday, 8:00 a.m. to 8:00 p.m. ET.', + ], + }, + }, +}; + +module.exports = { + confirmed, + notConfirmed, +}; diff --git a/src/applications/personalization/profile/mocks/server.js b/src/applications/personalization/profile/mocks/server.js index 9177f471ca1d..fcb081e41bc3 100644 --- a/src/applications/personalization/profile/mocks/server.js +++ b/src/applications/personalization/profile/mocks/server.js @@ -22,6 +22,7 @@ const mockDisabilityCompensations = require('./endpoints/disability-compensation const directDeposits = require('./endpoints/direct-deposits'); const bankAccounts = require('./endpoints/bank-accounts'); const serviceHistory = require('./endpoints/service-history'); +const vetVerificationStatus = require('./endpoints/vet-verification-status'); const fullName = require('./endpoints/full-name'); const { baseUserTransitionAvailabilities, @@ -107,6 +108,7 @@ const responses = { profileShowPrivacyPolicy: true, veteranOnboardingContactInfoFlow: true, veteranStatusCardUseLighthouse: true, + veteranStatusCardUseLighthouseFrontend: true, }), ), secondsOfDelay, @@ -236,6 +238,10 @@ const responses = { // .status(200) // .json(serviceHistory.generateServiceHistoryError('403')); }, + 'GET /v0/profile/vet_verification_status': (_req, res) => { + return res.status(200).json(vetVerificationStatus.confirmed); + // return res.status(200).json(vetVerificationStatus.notConfirmed); + }, 'GET /v0/disability_compensation_form/rating_info': (_req, res) => { // return res.status(200).json(ratingInfo.success.serviceConnected0); return res.status(200).json(ratingInfo.success.serviceConnected40); diff --git a/src/applications/personalization/profile/tests/components/military-information/MilitaryInformation.unit.spec.jsx b/src/applications/personalization/profile/tests/components/military-information/MilitaryInformation.unit.spec.jsx index 3ac7fdcd0b6d..fcf3bb8ebf91 100644 --- a/src/applications/personalization/profile/tests/components/military-information/MilitaryInformation.unit.spec.jsx +++ b/src/applications/personalization/profile/tests/components/military-information/MilitaryInformation.unit.spec.jsx @@ -1,8 +1,8 @@ import React from 'react'; import { expect } from 'chai'; - +import * as api from '~/platform/utilities/api'; +import sinon from 'sinon'; import { renderWithProfileReducers } from '../../unit-test-helpers'; - import MilitaryInformation from '../../../components/military-information/MilitaryInformation'; function createBasicInitialState(toggles = {}) { @@ -61,6 +61,16 @@ function createBasicInitialState(toggles = {}) { describe('MilitaryInformation', () => { let initialState; let view; + let apiRequestStub; + + beforeEach(() => { + apiRequestStub = sinon.stub(api, 'apiRequest'); + }); + + afterEach(() => { + apiRequestStub.restore(); + }); + describe('when military history exists', () => { it('should render data for each entry of military history', () => { initialState = createBasicInitialState(); @@ -91,6 +101,15 @@ describe('MilitaryInformation', () => { initialState = createBasicInitialState(); initialState.vaProfile.militaryInformation.serviceHistory.serviceHistory[0].branchOfService = null; initialState.vaProfile.militaryInformation.serviceHistory.serviceHistory[1].branchOfService = undefined; + const mockData = { + data: { + id: '', + type: 'veteran_status_confirmations', + attributes: { veteranStatus: 'confirmed' }, + }, + }; + + apiRequestStub.resolves(mockData); view = renderWithProfileReducers(, { initialState, }); diff --git a/src/applications/personalization/profile/tests/components/military-information/PeriodOfServiceTypeText.unit.spec.jsx b/src/applications/personalization/profile/tests/components/military-information/PeriodOfServiceTypeText.unit.spec.jsx index 2cd5b272046a..3b6a2c289116 100644 --- a/src/applications/personalization/profile/tests/components/military-information/PeriodOfServiceTypeText.unit.spec.jsx +++ b/src/applications/personalization/profile/tests/components/military-information/PeriodOfServiceTypeText.unit.spec.jsx @@ -1,5 +1,7 @@ import React from 'react'; import { expect } from 'chai'; +import * as api from '~/platform/utilities/api'; +import sinon from 'sinon'; import { renderWithProfileReducers } from '../../unit-test-helpers'; import MilitaryInformation from '../../../components/military-information/MilitaryInformation'; @@ -58,6 +60,16 @@ function createBasicInitialState(toggles = {}) { } describe('MilitaryInformation - Period of Service Type Text', () => { + let apiRequestStub; + + beforeEach(() => { + apiRequestStub = sinon.stub(api, 'apiRequest'); + }); + + afterEach(() => { + apiRequestStub.restore(); + }); + describe('when military history exists', () => { it('should render periodOfServiceTypeText when present and when periodOfServiceTypeCode is A or V', () => { const initialState = createBasicInitialState(); diff --git a/src/applications/personalization/profile/tests/e2e/proof-of-veteran-status/proof-of-veteran-status.cypress.spec.js b/src/applications/personalization/profile/tests/e2e/proof-of-veteran-status/proof-of-veteran-status.cypress.spec.js index 5f007816d5c0..08cb01bf1704 100644 --- a/src/applications/personalization/profile/tests/e2e/proof-of-veteran-status/proof-of-veteran-status.cypress.spec.js +++ b/src/applications/personalization/profile/tests/e2e/proof-of-veteran-status/proof-of-veteran-status.cypress.spec.js @@ -8,6 +8,10 @@ import { dishonorableDischarge, unknownDischarge, } from '../../../mocks/endpoints/service-history'; +import { + confirmed, + notConfirmed, +} from '../../../mocks/endpoints/vet-verification-status'; import MilitaryInformation from '../military-information/MilitaryInformation'; describe('Proof of Veteran status', () => { @@ -16,6 +20,7 @@ describe('Proof of Veteran status', () => { cy.intercept('GET', '/v0/user', loa3User72); cy.intercept('GET', '/v0/profile/full_name', fullName.success); cy.intercept('GET', '/v0/profile/service_history', airForce); + cy.intercept('GET', '/v0/profile/vet_verification_status', confirmed); }); it('Should display the Proof of Veteran Status component', () => { @@ -31,6 +36,7 @@ const login = ({ dischargeCode }) => { cy.intercept('GET', '/v0/user', loa3User72); cy.intercept('GET', '/v0/profile/full_name', fullName.success); cy.intercept('GET', '/v0/profile/service_history', dischargeCode); + cy.intercept('GET', '/v0/profile/vet_verification_status', notConfirmed); }; describe('Veteran is not eligible', () => {