From cd0d6bc2ecf4fae11270c08eb7d85b29950099f4 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Tue, 3 Dec 2024 10:07:57 -0500 Subject: [PATCH] Adds initial mhv-supply-reordering application (#33224) * Adds initial mhv-supply-reorder form app * Adds mocks * Adds alerts * Moves breadcrumbs to IntroductionPage * Sets useTopBackLink: true in formConfig (large continue button) * Show subtitle on IntroductionPage, only * Sets 'Finish this order later' content for save link * Removes margin from paragraph in informative va-alert * Sets Select supplies as first page * Sets supply count, 'No supplies selected/found' text * Adds formatDate helper * Adds checkboxes for selecting supplies * Adds product description * Updates readme * No e2e specs, yet * Adds unit specs --- .../mhv-supply-reordering/README.md | 100 ++++++++++++ .../mhv-supply-reordering/actions/index.js | 13 ++ .../actions/mdotInProgressForm.js | 35 ++++ .../mhv-supply-reordering/app-entry.jsx | 15 ++ .../components/Breadcrumbs.jsx | 22 +++ .../components/DlcEmailLink.jsx | 6 + .../components/DlcTelephoneLink.jsx | 12 ++ .../components/EditAddress.jsx | 154 ++++++++++++++++++ .../components/EditEmail.jsx | 46 ++++++ .../mhv-supply-reordering/components/Help.jsx | 22 +++ .../components/SuppliesAvailable.jsx | 24 +++ .../components/SuppliesUnavailable.jsx | 67 ++++++++ .../components/UnsavedFieldNote.jsx | 21 +++ .../components/VerifiedPrefillAlert.jsx | 12 ++ .../components/alerts/AlertDeceased.jsx | 19 +++ .../alerts/AlertNoRecordForUser.jsx | 29 ++++ .../alerts/AlertNoSuppliesForReorder.jsx | 40 +++++ .../alerts/AlertReorderAccessExpired.jsx | 25 +++ .../alerts/AlertSomethingWentWrong.jsx | 25 +++ .../components/alerts/index.js | 13 ++ .../mhv-supply-reordering/config/form.js | 118 ++++++++++++++ .../config/prefillTransformer.js | 10 ++ .../mhv-supply-reordering/constants.js | 22 +++ .../containers/Alerts.jsx | 58 +++++++ .../mhv-supply-reordering/containers/App.jsx | 36 ++++ .../containers/ConfirmationPage.jsx | 95 +++++++++++ .../containers/IntroductionPage.jsx | 78 +++++++++ .../mhv-supply-reordering/manifest.json | 7 + .../mocks/feature-toggles/index.js | 33 ++++ .../mocks/in-progress-forms/mdot/errors.js | 39 +++++ .../mocks/in-progress-forms/mdot/index.js | 67 ++++++++ .../mocks/in-progress-forms/mdot/supplies.js | 39 +++++ .../mhv-supply-reordering/mocks/index.js | 18 ++ .../mocks/maintenance-windows/index.js | 4 + .../mocks/mdot/supplies/index.js | 8 + .../mhv-supply-reordering/mocks/user/index.js | 68 ++++++++ .../pages/chooseSupplies.jsx | 49 ++++++ .../pages/contactInformation.jsx | 56 +++++++ .../pages/mailingAddress.js | 26 +++ .../mhv-supply-reordering/reducers/index.js | 8 + .../reducers/mdotInProgressFormReducer.js | 30 ++++ .../mhv-supply-reordering/routes.jsx | 12 ++ .../sass/mhv-supply-reordering.scss | 1 + .../mhv-supply-reordering/selectors/index.js | 54 ++++++ .../tests/containers/Alerts.unit.spec.jsx | 73 +++++++++ .../containers/ConfirmationPage.unit.spec.jsx | 45 +++++ .../containers/IntroductionPage.unit.spec.jsx | 68 ++++++++ .../tests/fixtures/data/minimal-test.json | 9 + .../tests/fixtures/mocks/user.json | 56 +++++++ .../mhv-supply-reordering.cypress._spec.js | 37 +++++ .../mdotInProgressFormsReducer.unit.spec.js | 58 +++++++ .../tests/selectors/canReorderOn.unit.spec.js | 30 ++++ .../selectors/selectSupplies.unit.spec.js | 52 ++++++ .../tests/selectors/showAlert.unit.spec.js | 107 ++++++++++++ .../tests/utils/helpers.unit.spec.js | 75 +++++++++ .../mhv-supply-reordering/utils/helpers.js | 60 +++++++ .../mhv-supply-reordering/utils/validators.js | 12 ++ 57 files changed, 2318 insertions(+) create mode 100644 src/applications/mhv-supply-reordering/README.md create mode 100644 src/applications/mhv-supply-reordering/actions/index.js create mode 100644 src/applications/mhv-supply-reordering/actions/mdotInProgressForm.js create mode 100644 src/applications/mhv-supply-reordering/app-entry.jsx create mode 100644 src/applications/mhv-supply-reordering/components/Breadcrumbs.jsx create mode 100644 src/applications/mhv-supply-reordering/components/DlcEmailLink.jsx create mode 100644 src/applications/mhv-supply-reordering/components/DlcTelephoneLink.jsx create mode 100644 src/applications/mhv-supply-reordering/components/EditAddress.jsx create mode 100644 src/applications/mhv-supply-reordering/components/EditEmail.jsx create mode 100644 src/applications/mhv-supply-reordering/components/Help.jsx create mode 100644 src/applications/mhv-supply-reordering/components/SuppliesAvailable.jsx create mode 100644 src/applications/mhv-supply-reordering/components/SuppliesUnavailable.jsx create mode 100644 src/applications/mhv-supply-reordering/components/UnsavedFieldNote.jsx create mode 100644 src/applications/mhv-supply-reordering/components/VerifiedPrefillAlert.jsx create mode 100644 src/applications/mhv-supply-reordering/components/alerts/AlertDeceased.jsx create mode 100644 src/applications/mhv-supply-reordering/components/alerts/AlertNoRecordForUser.jsx create mode 100644 src/applications/mhv-supply-reordering/components/alerts/AlertNoSuppliesForReorder.jsx create mode 100644 src/applications/mhv-supply-reordering/components/alerts/AlertReorderAccessExpired.jsx create mode 100644 src/applications/mhv-supply-reordering/components/alerts/AlertSomethingWentWrong.jsx create mode 100644 src/applications/mhv-supply-reordering/components/alerts/index.js create mode 100644 src/applications/mhv-supply-reordering/config/form.js create mode 100644 src/applications/mhv-supply-reordering/config/prefillTransformer.js create mode 100644 src/applications/mhv-supply-reordering/constants.js create mode 100644 src/applications/mhv-supply-reordering/containers/Alerts.jsx create mode 100644 src/applications/mhv-supply-reordering/containers/App.jsx create mode 100644 src/applications/mhv-supply-reordering/containers/ConfirmationPage.jsx create mode 100644 src/applications/mhv-supply-reordering/containers/IntroductionPage.jsx create mode 100644 src/applications/mhv-supply-reordering/manifest.json create mode 100644 src/applications/mhv-supply-reordering/mocks/feature-toggles/index.js create mode 100644 src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/errors.js create mode 100644 src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/index.js create mode 100644 src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/supplies.js create mode 100644 src/applications/mhv-supply-reordering/mocks/index.js create mode 100644 src/applications/mhv-supply-reordering/mocks/maintenance-windows/index.js create mode 100644 src/applications/mhv-supply-reordering/mocks/mdot/supplies/index.js create mode 100644 src/applications/mhv-supply-reordering/mocks/user/index.js create mode 100644 src/applications/mhv-supply-reordering/pages/chooseSupplies.jsx create mode 100644 src/applications/mhv-supply-reordering/pages/contactInformation.jsx create mode 100644 src/applications/mhv-supply-reordering/pages/mailingAddress.js create mode 100644 src/applications/mhv-supply-reordering/reducers/index.js create mode 100644 src/applications/mhv-supply-reordering/reducers/mdotInProgressFormReducer.js create mode 100644 src/applications/mhv-supply-reordering/routes.jsx create mode 100644 src/applications/mhv-supply-reordering/sass/mhv-supply-reordering.scss create mode 100644 src/applications/mhv-supply-reordering/selectors/index.js create mode 100644 src/applications/mhv-supply-reordering/tests/containers/Alerts.unit.spec.jsx create mode 100644 src/applications/mhv-supply-reordering/tests/containers/ConfirmationPage.unit.spec.jsx create mode 100644 src/applications/mhv-supply-reordering/tests/containers/IntroductionPage.unit.spec.jsx create mode 100644 src/applications/mhv-supply-reordering/tests/fixtures/data/minimal-test.json create mode 100644 src/applications/mhv-supply-reordering/tests/fixtures/mocks/user.json create mode 100644 src/applications/mhv-supply-reordering/tests/mhv-supply-reordering.cypress._spec.js create mode 100644 src/applications/mhv-supply-reordering/tests/reducers/mdotInProgressFormsReducer.unit.spec.js create mode 100644 src/applications/mhv-supply-reordering/tests/selectors/canReorderOn.unit.spec.js create mode 100644 src/applications/mhv-supply-reordering/tests/selectors/selectSupplies.unit.spec.js create mode 100644 src/applications/mhv-supply-reordering/tests/selectors/showAlert.unit.spec.js create mode 100644 src/applications/mhv-supply-reordering/tests/utils/helpers.unit.spec.js create mode 100644 src/applications/mhv-supply-reordering/utils/helpers.js create mode 100644 src/applications/mhv-supply-reordering/utils/validators.js diff --git a/src/applications/mhv-supply-reordering/README.md b/src/applications/mhv-supply-reordering/README.md new file mode 100644 index 000000000000..28bc97915b51 --- /dev/null +++ b/src/applications/mhv-supply-reordering/README.md @@ -0,0 +1,100 @@ +# mhv-supply-reordering + +## Background Info + +About: This app provides an interface to re-order Hearing Aid and Sleep Apnea accessories +Slack Channel: [#va-cto-supply-reordering](https://dsva.slack.com/archives/C05DFSM57FW/p1689711688225089) + +## App + +Form app generated with `yarn new:app`. Changes to the following files were reverted, since `VA_FORM_IDS.FORM_VA_2346A` already exists. + +- `src/platform/forms/constants.js` +- `src/platform/forms/tests/forms.unit.spec.js` + +## Quick start to get running locally + +Before you get started check [this page](https://depo-platform-documentation.scrollhelp.site/developer-docs/setting-up-your-local-frontend-environment) first to make sure you are setup to use the correct version of Node and Yarn. + +- clone vets-website repo `git clone git@github.com:department-of-veterans-affairs/vets-website.git` +- run `yarn install` +- turn on local mocks `yarn mock-api --responses src/applications/mhv-supply-reordering/mocks/index.js` +- start app `yarn watch --env entry=mhv-supply-reordering` +- Run this in your browser console to simulate being logged in `localStorage.setItem('hasSession', true);` +- visit the app: `http://localhost:3001/my-health/order-supplies` + +Note: The application fetches supply data from `/v0/in_progress_forms/mdot`. This endpoint is mocked in the local development environment. + +## Running tests + +Unit tests can be run using this command: `yarn test:unit --app-folder mhv-supply-reordering`. To get detailed errors, run this command with `--log-level=error`. To get coverage reports run this command `yarn test:unit --app-folder mhv-supply-reordering --coverage --coverage-html`. View the report at `/coverage/index.html` + +Cypress tests can be run with the GUI using this command: `yarn cy:open`. From there you can filter by `mhv-supply-reordering` to run end to end tests for this app. + +Run Cypress from command line: + +- Run all `yarn cy:run --spec "src/applications/mhv-supply-reordering/**/**/*"` +- Specify browser `-b electron` + +## VA Forms - Web Component Fields and Patterns + +[[docs](https://depo-platform-documentation.scrollhelp.site/developer-docs/va-forms-library-web-component-fields-and-patterns)] + +[[examples](https://staging.va.gov/mock-form-patterns/introduction)] + +A web-component-field is a design system web component for use in forms. These can be found at `src/platform/forms-system/src/js/web-component-fields`. + +A web-component-pattern is a group of web-component-fields that can span one or more pages (e.g. - a multi-page form). These can be found at `src/platform/forms-system/src/js/web-component-patterns`. + +## Form Flow + +- IntroductionPage +- ChooseSupplies +- ContactInformation (optionally: EditEmail, EditAddress) +- ReviewPage +- ConfirmationPage + +## API Responses + +[[mocker-api](https://github.com/jaywcjlove/mocker-api/tree/v2.9.0?tab=readme-ov-file#usage)] + +[[vets-api OpenAPI documentation](https://department-of-veterans-affairs.github.io/va-digital-services-platform-docs/api-reference/#/in_progress_forms)] + +`GET /v0/in_progress_forms/MDOT` returns the following... (note the lack of a `data` property) + +```json +{ + "formData": { + "fullName": {}, + "permanentAddress": {}, + "temporaryAddress": {}, + "ssnLastFour": "", + "gender": "", + "vetEmail": "", + "dateOfBirth": "", + "eligibility": {}, + "supplies": [] + }, + "metadata": { + "version": 0, + "prefill": true, + "returnUrl": "" + } +} +``` + +When requesting `GET /v0/in_progress_forms/MDOT`, the MDOT client in vets-api will make a request to the system of record for veteran details and supplies available to the veteran. See `V0::InProgressFormsController.camelized_prefill_for_user` and `FormProfiles::MDOT#prefill`. On the front-end, test against the possible responses for `MDOT::Client.new(user).get_supplies` which are mapped to `mdot.exceptions` values in `vets-api/config/locales/exceptions.en.yml` and then passed along in the response. Also, see `vets-api/spec/support/vcr_cassettes/mdot/get_supplies*.yml`. + +## Dynamic Form Fields + +[[using update and replace schema funcs](https://depo-platform-documentation.scrollhelp.site/developer-docs/va-forms-library-how-to-use-updateschema-and-repla)] + +see `src/applications/disability-benefits/all-claims/pages/toxicExposure/toxicExposureConditions.js` for an example. + +## Device Types, Device Names + +How do we access other device types? (e.g. - assistive devices, nebulizers). Are these included in the request for supplies? + +The `productGroup` property of a supply can be one of the following values: `['accessories', 'batteries', 'apnea']`. `'assistive devices'` will be added to this list in the near future. + +The `deviceName` property of a supply indicates the associated device for the supply. diff --git a/src/applications/mhv-supply-reordering/actions/index.js b/src/applications/mhv-supply-reordering/actions/index.js new file mode 100644 index 000000000000..6dd774350f49 --- /dev/null +++ b/src/applications/mhv-supply-reordering/actions/index.js @@ -0,0 +1,13 @@ +import { + GET_MDOT_IN_PROGRESS_FORM_STARTED, + GET_MDOT_IN_PROGRESS_FORM_SUCCEEDED, + GET_MDOT_IN_PROGRESS_FORM_FAILED, + getMdotInProgressForm, +} from './mdotInProgressForm'; + +export { + GET_MDOT_IN_PROGRESS_FORM_STARTED, + GET_MDOT_IN_PROGRESS_FORM_SUCCEEDED, + GET_MDOT_IN_PROGRESS_FORM_FAILED, + getMdotInProgressForm, +}; diff --git a/src/applications/mhv-supply-reordering/actions/mdotInProgressForm.js b/src/applications/mhv-supply-reordering/actions/mdotInProgressForm.js new file mode 100644 index 000000000000..1d72132d7935 --- /dev/null +++ b/src/applications/mhv-supply-reordering/actions/mdotInProgressForm.js @@ -0,0 +1,35 @@ +import { apiRequest } from 'platform/utilities/api'; + +export const GET_MDOT_IN_PROGRESS_FORM_STARTED = + 'GET_MDOT_IN_PROGRESS_FORM_STARTED'; +export const GET_MDOT_IN_PROGRESS_FORM_SUCCEEDED = + 'GET_MDOT_IN_PROGRESS_FORM_SUCCEEDED'; +export const GET_MDOT_IN_PROGRESS_FORM_FAILED = + 'GET_MDOT_IN_PROGRESS_FORM_FAILED'; + +const getMdotInProgressFormStarted = () => ({ + type: GET_MDOT_IN_PROGRESS_FORM_STARTED, +}); + +const getMdotInProgressFormSucceeded = payload => ({ + type: GET_MDOT_IN_PROGRESS_FORM_SUCCEEDED, + payload, +}); + +const getMdotInProgressFormFailed = payload => ({ + type: GET_MDOT_IN_PROGRESS_FORM_FAILED, + payload, +}); + +const MDOT_IN_PROGRESS_FORM_PATH = '/in_progress_forms/MDOT'; + +export const getMdotInProgressForm = () => async dispatch => { + await dispatch(getMdotInProgressFormStarted()); + return apiRequest(MDOT_IN_PROGRESS_FORM_PATH) + .then(payload => { + return dispatch(getMdotInProgressFormSucceeded(payload)); + }) + .catch(err => { + return dispatch(getMdotInProgressFormFailed(err)); + }); +}; diff --git a/src/applications/mhv-supply-reordering/app-entry.jsx b/src/applications/mhv-supply-reordering/app-entry.jsx new file mode 100644 index 000000000000..dc4f15fc3ceb --- /dev/null +++ b/src/applications/mhv-supply-reordering/app-entry.jsx @@ -0,0 +1,15 @@ +import '@department-of-veterans-affairs/platform-polyfills'; +import './sass/mhv-supply-reordering.scss'; + +import { startAppFromIndex } from '@department-of-veterans-affairs/platform-startup/exports'; + +import routes from './routes'; +import reducer from './reducers'; +import manifest from './manifest.json'; + +startAppFromIndex({ + entryName: manifest.entryName, + url: manifest.rootUrl, + reducer, + routes, +}); diff --git a/src/applications/mhv-supply-reordering/components/Breadcrumbs.jsx b/src/applications/mhv-supply-reordering/components/Breadcrumbs.jsx new file mode 100644 index 000000000000..e31031728179 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/Breadcrumbs.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { VaBreadcrumbs } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import manifest from '../manifest.json'; + +const breadcrumbList = [ + { + href: '/', + label: 'VA.gov Home', + }, + { + href: '/my-health', + label: 'Health care', + }, + { + href: manifest.rootUrl, + label: manifest.appName, + }, +]; + +const Breadcrumbs = () => ; + +export default Breadcrumbs; diff --git a/src/applications/mhv-supply-reordering/components/DlcEmailLink.jsx b/src/applications/mhv-supply-reordering/components/DlcEmailLink.jsx new file mode 100644 index 000000000000..13a4f15ae411 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/DlcEmailLink.jsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { DLC_EMAIL } from '../constants'; + +const DlcEmailLink = () => {DLC_EMAIL}; + +export default DlcEmailLink; diff --git a/src/applications/mhv-supply-reordering/components/DlcTelephoneLink.jsx b/src/applications/mhv-supply-reordering/components/DlcTelephoneLink.jsx new file mode 100644 index 000000000000..d4dc96a88af8 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/DlcTelephoneLink.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; +import { DLC_TELEPHONE } from '../constants'; + +const DlcTelephoneLink = () => ( + <> + ( + ) + +); + +export default DlcTelephoneLink; diff --git a/src/applications/mhv-supply-reordering/components/EditAddress.jsx b/src/applications/mhv-supply-reordering/components/EditAddress.jsx new file mode 100644 index 000000000000..546789da5c49 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/EditAddress.jsx @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; +import { + VaCheckbox, + VaSelect, + VaTextInput, + VaButton, +} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import constants from 'vets-json-schema/dist/constants.json'; +import UnsavedFieldNote from './UnsavedFieldNote'; + +const COUNTRY_VALUES = constants.countries.map(country => country.value); +const COUNTRY_NAMES = constants.countries.map(country => country.label); + +const MILITARY_STATE_VALUES = constants.militaryStates.map( + state => state.value, +); + +// filtered States that include US territories +const filteredStates = constants.states.USA.filter( + state => !MILITARY_STATE_VALUES.includes(state.value), +); + +const STATE_VALUES = filteredStates.map(state => state.value); +const STATE_NAMES = filteredStates.map(state => state.label); + +const EditAddress = ({ data, goToPath, setFormData }) => { + const [address, setAddress] = useState(data.permanentAddress || {}); + + const handleSubmit = event => { + event.preventDefault(); + setFormData({ ...data, permanentAddress: address }); + goToPath('/contact-information'); + }; + + const handleInputChange = event => { + const { name, value } = event.target; + setAddress(prevAddress => ({ ...prevAddress, [name]: value })); + }; + + return ( +
+

Order medical supplies

+

Contact information

+ +
+ + setAddress(prevAddress => ({ + ...prevAddress, + isMilitary: e.detail.checked, + })) + } + checked={address.isMilitary} + /> + + + handleInputChange({ + target: { name: 'country', value: e.detail.value }, + }) + } + required + > + {COUNTRY_VALUES.map((value, index) => ( + + ))} + + + + handleInputChange({ + target: { name: 'street', value: e.target.value }, + }) + } + required + /> + + + handleInputChange({ + target: { name: 'street2', value: e.target.value }, + }) + } + /> + + + handleInputChange({ + target: { name: 'city', value: e.target.value }, + }) + } + required + /> + + + handleInputChange({ + target: { name: 'state', value: e.detail.value }, + }) + } + required + > + {STATE_VALUES.map((value, index) => ( + + ))} + + + + handleInputChange({ + target: { name: 'postalCode', value: e.target.value }, + }) + } + required + /> + +
+ + goToPath('/contact-information')} + uswds + /> +
+ +
+ ); +}; + +export default EditAddress; diff --git a/src/applications/mhv-supply-reordering/components/EditEmail.jsx b/src/applications/mhv-supply-reordering/components/EditEmail.jsx new file mode 100644 index 000000000000..c68a81c1cf7a --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/EditEmail.jsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import UnsavedFieldNote from './UnsavedFieldNote'; + +const EditEmail = ({ data, goToPath, setFormData }) => { + const [email, setEmail] = useState(data.emailAddress || ''); + + const handleSubmit = event => { + event.preventDefault(); + setFormData({ ...data, emailAddress: email }); + goToPath('/contact-information'); + }; + + return ( +
+

Order medical supplies

+

Contact information

+ +
+ + setEmail(e.target.value)} + required + /> +
+ + goToPath('/contact-information')} + /> +
+
+
+ ); +}; + +export default EditEmail; diff --git a/src/applications/mhv-supply-reordering/components/Help.jsx b/src/applications/mhv-supply-reordering/components/Help.jsx new file mode 100644 index 000000000000..e7368be8d44e --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/Help.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import DlcTelephoneLink from './DlcTelephoneLink'; +import { HEALTH_FACILITIES_URL } from '../constants'; + +const Help = () => ( + <> +

+ If you have trouble using your supplies,{' '} + + find the phone number for your local VA health facility + + . +

+

+ If you have questions about your supplies, call our VA + Denver Logistics Center at . We’re here Monday through + Friday, 8:15 a.m. to 5:00 p.m. ET. +

+ +); + +export default Help; diff --git a/src/applications/mhv-supply-reordering/components/SuppliesAvailable.jsx b/src/applications/mhv-supply-reordering/components/SuppliesAvailable.jsx new file mode 100644 index 000000000000..d05e8b9d0ac1 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/SuppliesAvailable.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { numberOfSuppliesPhrase } from '../utils/helpers'; + +const SuppliesAvailable = ({ supplies }) => ( + +

Available for reorder

+

+ You have {numberOfSuppliesPhrase(supplies?.length)} available for reorder. +

+
    + {supplies.map(({ productId, productName }) => ( +
  • {productName}
  • + ))} +
+
+); + +SuppliesAvailable.propTypes = { + supplies: PropTypes.array.isRequired, +}; + +export default SuppliesAvailable; diff --git a/src/applications/mhv-supply-reordering/components/SuppliesUnavailable.jsx b/src/applications/mhv-supply-reordering/components/SuppliesUnavailable.jsx new file mode 100644 index 000000000000..61fff63c37aa --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/SuppliesUnavailable.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import DlcTelephoneLink from './DlcTelephoneLink'; +import { HEALTH_FACILITIES_URL } from '../constants'; +import { formatDate, sortSupplies } from '../utils/helpers'; + +const SuppliesUnavailable = ({ supplies = [] }) => { + const cards = sortSupplies(supplies).map((supply, index) => ( +
+ +
+ {supply.productName}
+ Device: {supply.deviceName}
+ Quantity: {supply.quantity}
+ Last ordered on {formatDate(supply.lastOrderDate)} +
+ {supply.availableForReorder && ( +

+ You can’t order this supply online until{' '} + {formatDate(supply.nextAvailabilityDate)}. If you need this supply + now call us at . +

+ )} + {!supply.availableForReorder && ( +

+ This item is not available for reordering. To reorder, you can call{' '} + your VA healthcare team or{' '} + + send them a message + + . +

+ )} +
+
+ )); + + return ( + <> + {supplies.length > 0 && ( +
+

Unavailable for reorder

+

+ Showing {supplies.length} medical{' '} + {supplies.length > 1 + ? 'supplies, alphabetically by name' + : 'supply'} +

+
+ {cards} +
+ )} + + ); +}; + +SuppliesUnavailable.propTypes = { + supplies: PropTypes.array.isRequired, +}; + +export default SuppliesUnavailable; diff --git a/src/applications/mhv-supply-reordering/components/UnsavedFieldNote.jsx b/src/applications/mhv-supply-reordering/components/UnsavedFieldNote.jsx new file mode 100644 index 000000000000..c24d5005d4a2 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/UnsavedFieldNote.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import DlcTelephoneLink from './DlcTelephoneLink'; + +const UnsavedFieldNote = ({ fieldName }) => ( +

+ Note: Any updates you make to your {fieldName} will only apply to this + order. If you’d like to update for all future orders, you can either call us + at or change in your{' '} + + VA.gov profile + + . +

+); + +UnsavedFieldNote.propTypes = { + fieldName: PropTypes.string.isRequired, +}; + +export default UnsavedFieldNote; diff --git a/src/applications/mhv-supply-reordering/components/VerifiedPrefillAlert.jsx b/src/applications/mhv-supply-reordering/components/VerifiedPrefillAlert.jsx new file mode 100644 index 000000000000..8337f4832679 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/VerifiedPrefillAlert.jsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const VerifiedPrefillAlert = ( + +
+ Since you’re signed in to your account, you can save your order in + progress and come back later to finish filling it out. +
+
+); + +export default VerifiedPrefillAlert; diff --git a/src/applications/mhv-supply-reordering/components/alerts/AlertDeceased.jsx b/src/applications/mhv-supply-reordering/components/alerts/AlertDeceased.jsx new file mode 100644 index 000000000000..1b0014a16197 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/alerts/AlertDeceased.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; + +const AlertDeceased = () => ( + +

Our records show that this Veteran is deceased

+
+ We can’t fulfill an order for this Veteran + + If this information is incorrect, please call Veterans Benefits + Assistance at + , Monday through Friday, + 8:00 a.m. to 9:00 p.m. E.T. + +
+
+); + +export default AlertDeceased; diff --git a/src/applications/mhv-supply-reordering/components/alerts/AlertNoRecordForUser.jsx b/src/applications/mhv-supply-reordering/components/alerts/AlertNoRecordForUser.jsx new file mode 100644 index 000000000000..a1e647cb074a --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/alerts/AlertNoRecordForUser.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { HEALTH_FACILITIES_URL } from '../../constants'; + +const AlertNoRecordForUser = () => ( + +

We can’t find your records in our system

+
+ + You can’t order hearing aid or CPAP supplies at this time because we + find your records in our system or we’re missing some information needed + for you to order. + + + + If you think this is incorrect, call your health care provider to update + your record.{' '} + + Find contact information for your local medical center. + + +
+
+); + +export default AlertNoRecordForUser; diff --git a/src/applications/mhv-supply-reordering/components/alerts/AlertNoSuppliesForReorder.jsx b/src/applications/mhv-supply-reordering/components/alerts/AlertNoSuppliesForReorder.jsx new file mode 100644 index 000000000000..f8faaa9f3586 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/alerts/AlertNoSuppliesForReorder.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import DlcEmailLink from '../DlcEmailLink'; +import DlcTelephoneLink from '../DlcTelephoneLink'; +import { formatDate } from '../../utils/helpers'; + +/** + * Generates an alert for a veteran that is not eligible to order supplies. + * @param {string} reorderDate the date supplies will be available to reorder + * @returns the alert + */ +const AlertNoSuppliesForReorder = ({ reorderDate }) => { + const date = reorderDate ? formatDate(reorderDate) : undefined; + return ( + +

You can’t reorder your items at this time

+
+ {date && ( + + Our records show that your items aren’t available for reorder until{' '} + {date}. You can only order items once every 5 months. + + )} + + If you need an item sooner, call the DLC Customer Service Section at{' '} + or email . + +
+
+ ); +}; + +AlertNoSuppliesForReorder.propTypes = { + reorderDate: PropTypes.string.isRequired, +}; + +export default AlertNoSuppliesForReorder; diff --git a/src/applications/mhv-supply-reordering/components/alerts/AlertReorderAccessExpired.jsx b/src/applications/mhv-supply-reordering/components/alerts/AlertReorderAccessExpired.jsx new file mode 100644 index 000000000000..985119dd6d95 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/alerts/AlertReorderAccessExpired.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import DlcEmailLink from '../DlcEmailLink'; +import DlcTelephoneLink from '../DlcTelephoneLink'; + +const AlertReorderAccessExpired = () => ( + +

You can’t reorder your items at this time

+
+ + You can’t order hearing aid or CPAP supplies online at this time because + you haven’t placed an order within the past two years. + + + + If you need to place an order, call the DLC Customer Service Section at{' '} + or email . + +
+
+); + +export default AlertReorderAccessExpired; diff --git a/src/applications/mhv-supply-reordering/components/alerts/AlertSomethingWentWrong.jsx b/src/applications/mhv-supply-reordering/components/alerts/AlertSomethingWentWrong.jsx new file mode 100644 index 000000000000..3a551fb1da40 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/alerts/AlertSomethingWentWrong.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import DlcEmailLink from '../DlcEmailLink'; +import DlcTelephoneLink from '../DlcTelephoneLink'; + +const AlertSomethingWentWrong = () => ( + +

We’re sorry. Something went wrong on our end.

+
+

+ You can’t place an order for hearing aid or CPAP supplies because + something went wrong on our end. +

+

+ What you can do +

+

+ For help ordering hearing aid or CPAP supplies, please call the DLC + Customer Service Section at or email{' '} + . +

+
+
+); + +export default AlertSomethingWentWrong; diff --git a/src/applications/mhv-supply-reordering/components/alerts/index.js b/src/applications/mhv-supply-reordering/components/alerts/index.js new file mode 100644 index 000000000000..3b9c9ebd51e8 --- /dev/null +++ b/src/applications/mhv-supply-reordering/components/alerts/index.js @@ -0,0 +1,13 @@ +import AlertDeceased from './AlertDeceased'; +import AlertNoRecordForUser from './AlertNoRecordForUser'; +import AlertNoSuppliesForReorder from './AlertNoSuppliesForReorder'; +import AlertReorderAccessExpired from './AlertReorderAccessExpired'; +import AlertSomethingWentWrong from './AlertSomethingWentWrong'; + +export { + AlertDeceased, + AlertNoRecordForUser, + AlertNoSuppliesForReorder, + AlertReorderAccessExpired, + AlertSomethingWentWrong, +}; diff --git a/src/applications/mhv-supply-reordering/config/form.js b/src/applications/mhv-supply-reordering/config/form.js new file mode 100644 index 000000000000..e94e15419839 --- /dev/null +++ b/src/applications/mhv-supply-reordering/config/form.js @@ -0,0 +1,118 @@ +import environment from 'platform/utilities/environment'; +import footerContent from 'platform/forms/components/FormFooter'; +import { VA_FORM_IDS } from 'platform/forms/constants'; +import { TITLE as title } from '../constants'; +import manifest from '../manifest.json'; + +import chooseSupplies from '../pages/chooseSupplies'; +import contactInformation from '../pages/contactInformation'; + +import EditAddress from '../components/EditAddress'; +import EditEmail from '../components/EditEmail'; +import getHelp from '../components/Help'; + +import introduction from '../containers/IntroductionPage'; +import confirmation from '../containers/ConfirmationPage'; + +import prefillTransformer from './prefillTransformer'; + +const blankSchema = { type: 'object', properties: {} }; + +const savedFormMessages = { + notFound: 'Please start over to reorder health care supplies.', + noAuth: + 'Please sign in again to continue your application for health care supply reordering.', +}; + +const saveInProgress = { + messages: { + inProgress: + 'Your health care supply reordering application (2346) is in progress.', + expired: + 'Your saved health care supply reordering application (2346) has expired. If you want to reorder supplies, please start a new application.', + saved: 'Your health care supply reordering application has been saved.', + }, +}; + +const customText = { + // appSavedSuccessfullyMessage: '', + appType: 'order', + // continueAppButtonText: '', + finishAppLaterMessage: 'Finish this order later', + // reviewPageTitle: '', + // startNewAppButtonText: '', + // submitButtonText: '', +}; + +const chapters = { + chooseSuppliesChapter: { + title: 'Choose supplies', + pages: { + chooseSupplies: { + path: 'choose-supplies', + title: 'Choose supplies', + uiSchema: chooseSupplies.uiSchema, + schema: chooseSupplies.schema, + }, + }, + }, + contactInformationChapter: { + title: 'Contact information', + pages: { + contactInformation: { + path: 'contact-information', + title: 'Contact information', + uiSchema: contactInformation.uiSchema, + schema: contactInformation.schema, + }, + editEmailAddress: { + title: 'Edit email address', + taskListHide: true, + path: 'edit-email-address', + CustomPage: EditEmail, + CustomPageReview: EditEmail, + depends: () => false, + uiSchema: {}, + schema: blankSchema, + }, + editMailingAddress: { + title: 'Edit mailing address', + taskListHide: true, + path: 'edit-mailing-address', + CustomPage: EditAddress, + CustomPageReview: EditAddress, + depends: () => false, + uiSchema: {}, + schema: blankSchema, + }, + }, + }, +}; + +/** @type {FormConfig} */ +const formConfig = { + rootUrl: manifest.rootUrl, + urlPrefix: '/', + submitUrl: `${environment.API_URL}/v0/mdot/supplies`, + submit: () => + Promise.resolve({ attributes: { confirmationNumber: '123123123' } }), + trackingPrefix: 'mhv-supply-reordering-', + introduction, + confirmation, + formId: VA_FORM_IDS.FORM_VA_2346A, + savedFormMessages, + saveInProgress, + version: 0, + prefillEnabled: true, + prefillTransformer, + title, + // subTitle, + customText, + defaultDefinitions: {}, + chapters, + getHelp, + footerContent, + useTopBackLink: true, +}; + +export default formConfig; diff --git a/src/applications/mhv-supply-reordering/config/prefillTransformer.js b/src/applications/mhv-supply-reordering/config/prefillTransformer.js new file mode 100644 index 000000000000..37629222b34e --- /dev/null +++ b/src/applications/mhv-supply-reordering/config/prefillTransformer.js @@ -0,0 +1,10 @@ +const prefillTransformer = (pages, formData, metadata) => ({ + pages, + formData: { + ...formData, + emailAddress: formData?.vetEmail || formData?.emailAddress, + }, + metadata, +}); + +export default prefillTransformer; diff --git a/src/applications/mhv-supply-reordering/constants.js b/src/applications/mhv-supply-reordering/constants.js new file mode 100644 index 000000000000..355f49f85dc6 --- /dev/null +++ b/src/applications/mhv-supply-reordering/constants.js @@ -0,0 +1,22 @@ +const { freeze } = Object; + +export const TITLE = 'Order Medical Supplies'; +export const SUBTITLE = + 'Use this form to order hearing aid batteries and accessories and CPAP supplies'; + +export const DLC_EMAIL = 'dalc.css@va.gov'; +export const DLC_TELEPHONE = '3032736200'; + +export const HEALTH_FACILITIES_URL = '/find-locations/?facilityType=health'; + +export const MDOT_ERROR_CODES = freeze({ + DECEASED: 'MDOT_deceased', + INVALID: 'MDOT_invalid', + SUPPLIES_NOT_FOUND: 'MDOT_supplies_not_found', +}); + +export const PRODUCT_GROUPS = freeze({ + ACCESSORIES: 'accessories', + BATTERIES: 'batteries', + APNEA: 'apnea', +}); diff --git a/src/applications/mhv-supply-reordering/containers/Alerts.jsx b/src/applications/mhv-supply-reordering/containers/Alerts.jsx new file mode 100644 index 000000000000..a2fe7474735d --- /dev/null +++ b/src/applications/mhv-supply-reordering/containers/Alerts.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { + AlertDeceased, + AlertNoRecordForUser, + AlertNoSuppliesForReorder, + AlertReorderAccessExpired, + AlertSomethingWentWrong, +} from '../components/alerts'; + +import { + canReorderOn, + showAlertDeceased, + showAlertNoRecordForUser, + showAlertNoSuppliesForReorder, + showAlertReorderAccessExpired, + showAlertSomethingWentWrong, +} from '../selectors'; + +const Alerts = () => { + const reorderDate = useSelector(canReorderOn); + const renderAlertDeceased = useSelector(showAlertDeceased); + const renderAlertNoRecordForUser = useSelector(showAlertNoRecordForUser); + const renderAlertNoSuppliesForReorder = useSelector( + showAlertNoSuppliesForReorder, + ); + const renderAlertReorderAccessExpired = useSelector( + showAlertReorderAccessExpired, + ); + const renderAlertSomethingWentWrong = useSelector( + showAlertSomethingWentWrong, + ); + + if (renderAlertDeceased) { + return ; + } + + if (renderAlertNoRecordForUser) { + return ; + } + + if (renderAlertReorderAccessExpired) { + return ; + } + + if (renderAlertSomethingWentWrong) { + return ; + } + + if (renderAlertNoSuppliesForReorder) { + return ; + } + + return null; +}; + +export default Alerts; diff --git a/src/applications/mhv-supply-reordering/containers/App.jsx b/src/applications/mhv-supply-reordering/containers/App.jsx new file mode 100644 index 000000000000..d4d2bd9d3480 --- /dev/null +++ b/src/applications/mhv-supply-reordering/containers/App.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { RequiredLoginView } from '@department-of-veterans-affairs/platform-user/RequiredLoginView'; +import backendServices from '@department-of-veterans-affairs/platform-user/profile/backendServices'; +import RoutedSavableApp from 'platform/forms/save-in-progress/RoutedSavableApp'; + +import formConfig from '../config/form'; +import { signInServiceEnabled } from '../selectors'; + +const serviceRequired = [ + // backendServices.FACILITIES, + backendServices.FORM_PREFILL, + // backendServices.IDENTITY_PROOFED, + backendServices.SAVE_IN_PROGRESS, + backendServices.USER_PROFILE, +]; + +const App = ({ location, children }) => { + const { user } = useSelector(state => state); + const useSiS = useSelector(signInServiceEnabled); + + return ( + + + {children} + + + ); +}; + +export default App; diff --git a/src/applications/mhv-supply-reordering/containers/ConfirmationPage.jsx b/src/applications/mhv-supply-reordering/containers/ConfirmationPage.jsx new file mode 100644 index 000000000000..3705a1823795 --- /dev/null +++ b/src/applications/mhv-supply-reordering/containers/ConfirmationPage.jsx @@ -0,0 +1,95 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { format, isValid } from 'date-fns'; +import { useSelector } from 'react-redux'; +import { scrollTo, waitForRenderThenFocus } from 'platform/utilities/ui'; + +export const ConfirmationPage = () => { + const alertRef = useRef(null); + const form = useSelector(state => state.form || {}); + const { submission, formId, data = {} } = form; + const { fullName } = data; + const submitDate = submission?.timestamp; + const confirmationNumber = submission?.response?.confirmationNumber; + + useEffect( + () => { + if (alertRef?.current) { + scrollTo('topScrollElement'); + waitForRenderThenFocus('h2', alertRef.current); + } + }, + [alertRef], + ); + + return ( +
+
+ VA logo +
+ + +

Your application has been submitted

+
+ +

We may contact you for more information or documents.

+

Please print this page for your records.

+
+

+ Order Medical Supplies Claim{' '} + (Form {formId}) +

+ {fullName ? ( + + for {fullName.first} {fullName.middle} {fullName.last} + {fullName.suffix ? `, ${fullName.suffix}` : null} + + ) : null} + + {confirmationNumber ? ( + <> +

Confirmation number

+

{confirmationNumber}

+ + ) : null} + + {isValid(submitDate) ? ( +

+ Date submitted +
+ {format(submitDate, 'MMMM d, yyyy')} +

+ ) : null} + + +
+ + Go back to VA.gov + +
+ ); +}; + +ConfirmationPage.propTypes = { + form: PropTypes.shape({ + data: PropTypes.shape({ + fullName: { + first: PropTypes.string, + middle: PropTypes.string, + last: PropTypes.string, + suffix: PropTypes.string, + }, + }), + formId: PropTypes.string, + submission: PropTypes.shape({ + timestamp: PropTypes.string, + }), + }), + name: PropTypes.string, +}; + +export default ConfirmationPage; diff --git a/src/applications/mhv-supply-reordering/containers/IntroductionPage.jsx b/src/applications/mhv-supply-reordering/containers/IntroductionPage.jsx new file mode 100644 index 000000000000..d9e569707e00 --- /dev/null +++ b/src/applications/mhv-supply-reordering/containers/IntroductionPage.jsx @@ -0,0 +1,78 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; + +import { focusElement } from 'platform/utilities/ui'; +import FormTitle from 'platform/forms-system/src/js/components/FormTitle'; +import SaveInProgressIntro from 'platform/forms/save-in-progress/SaveInProgressIntro'; + +import { getMdotInProgressForm } from '../actions'; +import { TITLE, SUBTITLE } from '../constants'; + +import Breadcrumbs from '../components/Breadcrumbs'; +import VerifiedPrefillAlert from '../components/VerifiedPrefillAlert'; +import Alerts from './Alerts'; +import SuppliesAvailable from '../components/SuppliesAvailable'; +import SuppliesUnavailable from '../components/SuppliesUnavailable'; +import { selectSupplies, selectUnavailableSupplies } from '../selectors'; + +const Loading = () => ( +
+ +
+); + +export const IntroductionPage = ({ route }) => { + const dispatch = useDispatch(); + const state = useSelector(s => s); + const loading = + state?.mdotInProgressForm?.loading || + state?.user?.profile?.loading || + false; + + const supplies = useSelector(selectSupplies); + const unavailableSupplies = useSelector(selectUnavailableSupplies); + + useEffect(() => focusElement('h1'), [loading]); + + useEffect(() => dispatch(getMdotInProgressForm()), [dispatch]); + + if (loading) { + return ; + } + + return ( + <> + + +

{SUBTITLE}

+ + + + + + ); +}; + +IntroductionPage.propTypes = { + route: PropTypes.shape({ + formConfig: PropTypes.shape({ + prefillEnabled: PropTypes.bool.isRequired, + savedFormMessages: PropTypes.object.isRequired, + }).isRequired, + pageList: PropTypes.arrayOf(PropTypes.object).isRequired, + }).isRequired, + location: PropTypes.shape({ + basename: PropTypes.string, + }), +}; + +export default IntroductionPage; diff --git a/src/applications/mhv-supply-reordering/manifest.json b/src/applications/mhv-supply-reordering/manifest.json new file mode 100644 index 000000000000..cb9ba6357389 --- /dev/null +++ b/src/applications/mhv-supply-reordering/manifest.json @@ -0,0 +1,7 @@ +{ + "appName": "Order Medical Supplies", + "entryFile": "./app-entry.jsx", + "entryName": "mhv-supply-reordering", + "rootUrl": "/my-health/order-supplies", + "productId": "9f2025da-09d6-469f-8d6e-811e7afa7582" +} diff --git a/src/applications/mhv-supply-reordering/mocks/feature-toggles/index.js b/src/applications/mhv-supply-reordering/mocks/feature-toggles/index.js new file mode 100644 index 000000000000..32e3a43c565e --- /dev/null +++ b/src/applications/mhv-supply-reordering/mocks/feature-toggles/index.js @@ -0,0 +1,33 @@ +const { snakeCase } = require('lodash'); + +const APPLICATION_FEATURE_TOGGLES = Object.freeze({ + mhvVaHealthChatEnabled: false, + mhvLandingPagePersonalization: false, + mhvIntegrationMedicalRecordsToPhase1: false, + travelPayPowerSwitch: false, +}); + +const generateFeatureToggles = ({ + toggles = APPLICATION_FEATURE_TOGGLES, +} = {}) => { + const snakeCaseToggles = Object.entries(toggles).map(([key, value]) => ({ + name: key, + value, + })); + + const camelCaseToggles = Object.entries(toggles).map(([key, value]) => ({ + name: snakeCase(key), + value, + })); + + return { + data: { + type: 'feature_toggles', + features: [...snakeCaseToggles, ...camelCaseToggles], + }, + }; +}; + +module.exports = { + 'GET /v0/feature_toggles': generateFeatureToggles(), +}; diff --git a/src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/errors.js b/src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/errors.js new file mode 100644 index 000000000000..034ad3a2310e --- /dev/null +++ b/src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/errors.js @@ -0,0 +1,39 @@ +const unauthenticated = { + errors: [ + { + title: 'Not authorized', + detail: 'Not authorized', + code: '401', + status: '401', + }, + ], +}; + +const internalServerError = { + errors: [ + { + title: 'Internal server error', + detail: 'Internal server error', + code: '500', + status: '500', + }, + ], +}; + +const notFound = { + errors: [ + { + title: 'Veteran Not Found', + detail: 'The veteran could not be found', + code: 'MDOT_invalid', + source: 'MDOT::Client', + status: '404', + }, + ], +}; + +module.exports = { + unauthenticated, + internalServerError, + notFound, +}; diff --git a/src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/index.js b/src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/index.js new file mode 100644 index 000000000000..e6a511d58f77 --- /dev/null +++ b/src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/index.js @@ -0,0 +1,67 @@ +// eslint-disable-next-line no-unused-vars +const { unauthenticated, internalServerError, notFound } = require('./errors'); +const { supplies } = require('./supplies'); +// const supplies = []; + +const getOk = { + formData: { + fullName: { + first: 'Greg', + middle: 'A', + last: 'Anderson', + }, + permanentAddress: { + street: '101 EXAMPLE STREET', + street2: 'APT 2', + city: 'KANSAS CITY', + state: 'MO', + country: 'UNITED STATES', + postalCode: '64117', + }, + temporaryAddress: { + street: 'PSC 1234 BOX 12345', + street2: ', ', + city: 'APO', + state: 'AE', + country: 'ARMED FORCES AF,EU,ME,CA', + postalCode: '09324', + }, + ssnLastFour: '1200', + gender: 'M', + vetEmail: 'vets.gov.user+1@gmail.com', + dateOfBirth: '1933-04-05', + eligibility: { + accessories: true, + apneas: true, + batteries: true, + }, + supplies, + }, + metadata: { + version: 0, + prefill: true, + returnUrl: '/veteran-information', + }, +}; + +const putOk = { + data: { + id: '12345', + type: 'in_progress_forms', + attributes: { + formId: 'MDOT', + createdAt: '', + updatedAt: '', + metadata: {}, + }, + }, +}; + +module.exports = { + // `GET /v0/in_progress_forms/${VA_FORM_IDS.FORM_VA_2346A}`: getOk, + 'GET /v0/in_progress_forms/MDOT': getOk, + 'OPTIONS /v0/in_progress_forms/MDOT': 'OK', + // 'GET /v0/in_progress_forms/MDOT': (_, res) => res.status(404).json(notFound), + // 'GET /v0/in_progress_forms/MDOT': (_, res) => res.status(500).json(internalServerError), + 'PUT /v0/in_progress_forms/MDOT': putOk, +}; diff --git a/src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/supplies.js b/src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/supplies.js new file mode 100644 index 000000000000..7c1bbe86a856 --- /dev/null +++ b/src/applications/mhv-supply-reordering/mocks/in-progress-forms/mdot/supplies.js @@ -0,0 +1,39 @@ +const supplies = [ + { + productName: 'ERHK HE11 680 MINI', + productGroup: 'Accessory', + productId: 6584, + availableForReorder: true, + lastOrderDate: '2022-05-16', + nextAvailabilityDate: '2022-10-16', + quantity: 5, + }, + { + productName: 'AIRFIT F10 M', + productGroup: 'Apnea', + productId: 6641, + availableForReorder: true, + lastOrderDate: '2022-07-05', + nextAvailabilityDate: '2022-12-05', + quantity: 1, + }, + { + productName: 'AIRFIT P10', + productGroup: 'Apnea', + productId: 6650, + availableForReorder: true, + lastOrderDate: '2022-07-05', + nextAvailabilityDate: '2022-12-05', + quantity: 1, + }, + { + productName: 'AIRCURVE10-ASV-CLIMATELINE', + productGroup: 'Apnea', + productId: 8467, + lastOrderDate: '2022-07-06', + nextAvailabilityDate: '2022-12-06', + quantity: 1, + }, +]; + +module.exports = { supplies }; diff --git a/src/applications/mhv-supply-reordering/mocks/index.js b/src/applications/mhv-supply-reordering/mocks/index.js new file mode 100644 index 000000000000..d03a25cf2c47 --- /dev/null +++ b/src/applications/mhv-supply-reordering/mocks/index.js @@ -0,0 +1,18 @@ +const delay = require('mocker-api/lib/delay'); // eslint-disable-line no-unused-vars + +const featureToggles = require('./feature-toggles'); +const inProgressFormsMdot = require('./in-progress-forms/mdot'); +const maintenanceWindows = require('./maintenance-windows'); +const mdotSupplies = require('./mdot/supplies'); +const user = require('./user'); + +const mockApiResponses = { + ...featureToggles, + ...inProgressFormsMdot, + ...maintenanceWindows, + ...mdotSupplies, + ...user, +}; + +// module.exports = mockApiResponses; +module.exports = delay(mockApiResponses, 1000); diff --git a/src/applications/mhv-supply-reordering/mocks/maintenance-windows/index.js b/src/applications/mhv-supply-reordering/mocks/maintenance-windows/index.js new file mode 100644 index 000000000000..e81bc0bc3a80 --- /dev/null +++ b/src/applications/mhv-supply-reordering/mocks/maintenance-windows/index.js @@ -0,0 +1,4 @@ +module.exports = { + 'GET /v0/maintenance_windows': { data: [] }, + 'OPTIONS /v0/maintenance_windows': 'OK', +}; diff --git a/src/applications/mhv-supply-reordering/mocks/mdot/supplies/index.js b/src/applications/mhv-supply-reordering/mocks/mdot/supplies/index.js new file mode 100644 index 000000000000..7b410a2596ef --- /dev/null +++ b/src/applications/mhv-supply-reordering/mocks/mdot/supplies/index.js @@ -0,0 +1,8 @@ +const ok = { + status: 'Order Processed', + orderId: '12345', +}; + +module.exports = { + 'POST /v0/mdot/supplies': ok, +}; diff --git a/src/applications/mhv-supply-reordering/mocks/user/index.js b/src/applications/mhv-supply-reordering/mocks/user/index.js new file mode 100644 index 000000000000..37196982358c --- /dev/null +++ b/src/applications/mhv-supply-reordering/mocks/user/index.js @@ -0,0 +1,68 @@ +/* eslint-disable camelcase */ +const ok = { + data: { + attributes: { + profile: { + sign_in: { + service_name: 'idme', + auth_broker: 'iam', + ssoe: true, + }, + email: 'vets.gov.user+1@gmail.com', + loa: { current: 3 }, + first_name: 'Greg', + middle_name: '', + last_name: 'Anderson', + gender: 'F', + birth_date: '1933-04-05', + verified: true, + }, + session: { + auth_broker: 'iam', + ssoe: true, + transactionid: 'sf8mUOpuAoxkx8uWxI6yrBAS/t0yrsjDKqktFz255P0=', + }, + veteran_status: { + status: 'OK', + is_veteran: true, + served_in_military: true, + }, + in_progress_forms: [], + prefills_available: ['21-526EZ', 'MDOT'], + services: [ + 'facilities', + 'hca', + 'edu-benefits', + 'evss-claims', + 'form526', + 'user-profile', + 'health-records', + 'rx', + 'messaging', + ], + va_profile: { + status: 'OK', + birth_date: '19330405', + family_name: 'Anderson', + gender: 'M', + given_names: ['Greg', ''], + active_status: 'active', + facilities: [ + { + facility_id: '983', + is_cerner: false, + }, + { + facility_id: '984', + is_cerner: false, + }, + ], + }, + }, + }, + meta: { errors: null }, +}; + +module.exports = { + 'GET /v0/user': ok, +}; diff --git a/src/applications/mhv-supply-reordering/pages/chooseSupplies.jsx b/src/applications/mhv-supply-reordering/pages/chooseSupplies.jsx new file mode 100644 index 000000000000..dbd7f60f82a9 --- /dev/null +++ b/src/applications/mhv-supply-reordering/pages/chooseSupplies.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +// import { validateAtLeastOneSelected } from '../utils/validators'; +import { checkboxGroupSchema } from '@department-of-veterans-affairs/platform-forms-system/web-component-patterns'; + +import { + numberOfSuppliesPhrase, + suppliesReplaceSchema, + suppliesUpdateUiSchema, + suppliesUi, +} from '../utils/helpers'; + +const Description = ({ formData }) => { + const count = formData?.supplies?.length || 0; + + return ( + <> +

Available for reorder

+

+ You have {numberOfSuppliesPhrase(count)} that are available for reorder. +

+

+ Note: For CPAP supplies, each order is a 12-month + supply. You can only order each item once every 12 months. +

+

+ For hearing aid supplies, each order is a 6-month supply. You can only + order each item once every 6 months. +

+ + ); +}; + +/** @type {PageSchema} */ +export default { + uiSchema: { + 'ui:description': Description, + chosenSupplies: suppliesUi({ + title: 'Select available supplies for reorder', + replaceSchema: suppliesReplaceSchema, + updateUiSchema: suppliesUpdateUiSchema, + }), + }, + schema: { + type: 'object', + properties: { + chosenSupplies: checkboxGroupSchema([]), + }, + }, +}; diff --git a/src/applications/mhv-supply-reordering/pages/contactInformation.jsx b/src/applications/mhv-supply-reordering/pages/contactInformation.jsx new file mode 100644 index 000000000000..ca9783c63775 --- /dev/null +++ b/src/applications/mhv-supply-reordering/pages/contactInformation.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Link } from 'react-router'; +import { titleUI } from 'platform/forms-system/src/js/web-component-patterns'; +import AddressViewField from '@department-of-veterans-affairs/platform-forms-system/AddressViewField'; +import UnsavedFieldNote from '../components/UnsavedFieldNote'; + +const Description = ({ formData }) => { + const { emailAddress, permanentAddress } = formData; + + return ( + <> + +

+ We’ve prefilled some of your information from VA Denver Logistics + Center’s record. If you need to correct anything, you can edit the + contact information. +

+
+ +

Email address

+ {emailAddress || ''} +

+ + Edit + +

+ +

Mailing address

+ +

+ + Edit + +

+ + + + ); +}; + +/** @type {PageSchema} */ +export default { + uiSchema: { + ...titleUI('Contact information'), + 'ui:description': Description, + 'ui:required': () => true, // don't allow progressing without all contact info + 'ui:options': { + hideOnReview: true, + forceDivWrapper: true, + }, + }, + schema: { + type: 'object', + properties: {}, + }, +}; diff --git a/src/applications/mhv-supply-reordering/pages/mailingAddress.js b/src/applications/mhv-supply-reordering/pages/mailingAddress.js new file mode 100644 index 000000000000..0c0c1fcb31ec --- /dev/null +++ b/src/applications/mhv-supply-reordering/pages/mailingAddress.js @@ -0,0 +1,26 @@ +import { + addressSchema, + addressUI, + titleUI, +} from 'platform/forms-system/src/js/web-component-patterns'; + +/** @type {PageSchema} */ +export default { + uiSchema: { + ...titleUI( + 'Mailing address', + 'We’ll send any important information about your application to this address.', + ), + address: addressUI({ + omit: ['street3'], + }), + }, + schema: { + type: 'object', + properties: { + address: addressSchema({ + omit: ['street3'], + }), + }, + }, +}; diff --git a/src/applications/mhv-supply-reordering/reducers/index.js b/src/applications/mhv-supply-reordering/reducers/index.js new file mode 100644 index 000000000000..2c68ea6d4dbf --- /dev/null +++ b/src/applications/mhv-supply-reordering/reducers/index.js @@ -0,0 +1,8 @@ +import { createSaveInProgressFormReducer } from 'platform/forms/save-in-progress/reducers'; +import formConfig from '../config/form'; +import { mdotInProgressFormReducer } from './mdotInProgressFormReducer'; + +export default { + form: createSaveInProgressFormReducer(formConfig), + mdotInProgressForm: mdotInProgressFormReducer, +}; diff --git a/src/applications/mhv-supply-reordering/reducers/mdotInProgressFormReducer.js b/src/applications/mhv-supply-reordering/reducers/mdotInProgressFormReducer.js new file mode 100644 index 000000000000..f7ba3662db2d --- /dev/null +++ b/src/applications/mhv-supply-reordering/reducers/mdotInProgressFormReducer.js @@ -0,0 +1,30 @@ +import { + GET_MDOT_IN_PROGRESS_FORM_STARTED, + GET_MDOT_IN_PROGRESS_FORM_SUCCEEDED, + GET_MDOT_IN_PROGRESS_FORM_FAILED, +} from '../actions'; + +export const initialState = { + formData: {}, + error: false, + loading: false, +}; + +export const mdotInProgressFormReducer = (state = initialState, action) => { + const { payload, type } = action; + switch (type) { + case GET_MDOT_IN_PROGRESS_FORM_STARTED: + return { ...state, loading: true }; + case GET_MDOT_IN_PROGRESS_FORM_SUCCEEDED: + return { ...state, formData: payload.formData, loading: false }; + case GET_MDOT_IN_PROGRESS_FORM_FAILED: + return { + ...state, + formData: {}, + loading: false, + error: payload?.errors?.at(0), + }; + default: + return state; + } +}; diff --git a/src/applications/mhv-supply-reordering/routes.jsx b/src/applications/mhv-supply-reordering/routes.jsx new file mode 100644 index 000000000000..8b56e82cbf9a --- /dev/null +++ b/src/applications/mhv-supply-reordering/routes.jsx @@ -0,0 +1,12 @@ +import { createRoutesWithSaveInProgress } from 'platform/forms/save-in-progress/helpers'; +import formConfig from './config/form'; +import App from './containers/App'; + +const route = { + path: '/', + component: App, + indexRoute: { onEnter: (nextState, replace) => replace('/introduction') }, + childRoutes: createRoutesWithSaveInProgress(formConfig), +}; + +export default route; diff --git a/src/applications/mhv-supply-reordering/sass/mhv-supply-reordering.scss b/src/applications/mhv-supply-reordering/sass/mhv-supply-reordering.scss new file mode 100644 index 000000000000..822eeaedb6c2 --- /dev/null +++ b/src/applications/mhv-supply-reordering/sass/mhv-supply-reordering.scss @@ -0,0 +1 @@ +@import "~@department-of-veterans-affairs/css-library/dist/tokens/scss/variables"; diff --git a/src/applications/mhv-supply-reordering/selectors/index.js b/src/applications/mhv-supply-reordering/selectors/index.js new file mode 100644 index 000000000000..31397fb925cd --- /dev/null +++ b/src/applications/mhv-supply-reordering/selectors/index.js @@ -0,0 +1,54 @@ +import { signInServiceEnabled } from '~/platform/user/authentication/selectors'; + +import { isLOA3, isLoggedIn, isVAPatient } from '~/platform/user/selectors'; + +import { MDOT_ERROR_CODES } from '../constants'; + +const selectSupplies = state => + state?.mdotInProgressForm?.formData?.supplies?.filter( + s => !!s?.availableForReorder, + ) || []; + +const selectUnavailableSupplies = state => + state?.mdotInProgressForm?.formData?.supplies?.filter( + s => !s?.availableForReorder, + ) || []; + +const canReorderOn = state => + selectUnavailableSupplies(state) + ?.map(s => s?.nextAvailabilityDate) + ?.sort() + ?.at(0); + +const showAlertDeceased = state => + state?.mdotInProgressForm?.error?.code === MDOT_ERROR_CODES.DECEASED || false; + +const showAlertNoRecordForUser = state => + state?.mdotInProgressForm?.error?.code === MDOT_ERROR_CODES.INVALID || false; + +const showAlertNoSuppliesForReorder = state => + state?.mdotInProgressForm?.formData?.supplies?.every( + supply => !supply?.availableForReorder, + ) || false; + +const showAlertReorderAccessExpired = state => + state?.mdotInProgressForm?.error?.code === + MDOT_ERROR_CODES.SUPPLIES_NOT_FOUND || false; + +const showAlertSomethingWentWrong = state => + Math.trunc(+state?.mdotInProgressForm?.error?.status / 500) === 1; + +export { + canReorderOn, + isLOA3, + isLoggedIn, + isVAPatient, + selectSupplies, + selectUnavailableSupplies, + signInServiceEnabled, + showAlertDeceased, + showAlertNoRecordForUser, + showAlertNoSuppliesForReorder, + showAlertReorderAccessExpired, + showAlertSomethingWentWrong, +}; diff --git a/src/applications/mhv-supply-reordering/tests/containers/Alerts.unit.spec.jsx b/src/applications/mhv-supply-reordering/tests/containers/Alerts.unit.spec.jsx new file mode 100644 index 000000000000..d4d7b4614bcb --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/containers/Alerts.unit.spec.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { expect } from 'chai'; + +import { renderInReduxProvider } from '@department-of-veterans-affairs/platform-testing/react-testing-library-helpers'; + +import Alerts from '../../containers/Alerts'; + +import { MDOT_ERROR_CODES } from '../../constants'; +import reducers from '../../reducers'; + +const stateFn = ({ + supplies = [{ availableForReorder: true }], + error = false, +} = {}) => ({ + mdotInProgressForm: { + formData: { + supplies, + }, + error, + loading: false, + }, +}); + +const setup = ({ initialState = stateFn() } = {}) => + renderInReduxProvider(, { initialState, reducers }); + +describe(' container', () => { + it('renders nothing', () => { + const { container } = setup(); + expect(container).to.be.empty; + }); + + it('renders ', () => { + const initialState = stateFn({ + error: { code: MDOT_ERROR_CODES.DECEASED }, + }); + const { getAllByTestId, getByTestId } = setup({ initialState }); + getByTestId('reorder-alert--deceased'); + expect(getAllByTestId(/^reorder-alert--/).length).to.eq(1); + }); + + it('renders ', () => { + const initialState = stateFn({ error: { code: MDOT_ERROR_CODES.INVALID } }); + const { getAllByTestId, getByTestId } = setup({ initialState }); + getByTestId('reorder-alert--no-record-for-user'); + expect(getAllByTestId(/^reorder-alert--/).length).to.eq(1); + }); + + it('renders ', () => { + const initialState = stateFn({ + error: { code: MDOT_ERROR_CODES.SUPPLIES_NOT_FOUND }, + }); + const { getAllByTestId, getByTestId } = setup({ initialState }); + getByTestId('reorder-alert--reorder-access-expired'); + expect(getAllByTestId(/^reorder-alert--/).length).to.eq(1); + }); + + it('renders ', () => { + const initialState = stateFn({ error: { status: 500 } }); + const { getAllByTestId, getByTestId } = setup({ initialState }); + getByTestId('reorder-alert--something-went-wrong'); + expect(getAllByTestId(/^reorder-alert--/).length).to.eq(1); + }); + + it('renders ', () => { + const initialState = stateFn({ + supplies: [{ nextAvailabilityDate: '2199-01-01' }], + }); + const { getAllByTestId, getByTestId } = setup({ initialState }); + getByTestId('reorder-alert--no-supplies-for-reorder'); + expect(getAllByTestId(/^reorder-alert--/).length).to.eq(1); + }); +}); diff --git a/src/applications/mhv-supply-reordering/tests/containers/ConfirmationPage.unit.spec.jsx b/src/applications/mhv-supply-reordering/tests/containers/ConfirmationPage.unit.spec.jsx new file mode 100644 index 000000000000..5bfb439956cb --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/containers/ConfirmationPage.unit.spec.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { expect } from 'chai'; +import formConfig from '../../config/form'; +import ConfirmationPage from '../../containers/ConfirmationPage'; + +const storeBase = { + form: { + formId: formConfig.formId, + submission: { + response: { + confirmationNumber: '123456', + }, + timestamp: Date.now(), + }, + data: { + fullName: { + first: 'John', + middle: '', + last: 'Doe', + }, + }, + }, +}; + +describe('Confirmation page', () => { + const middleware = [thunk]; + const mockStore = configureStore(middleware); + + it('it should show status success and the correct name of person', () => { + const { container, getByText } = render( + + + , + ); + expect(container.querySelector('va-alert')).to.have.attr( + 'status', + 'success', + ); + getByText(/John Doe/); + }); +}); diff --git a/src/applications/mhv-supply-reordering/tests/containers/IntroductionPage.unit.spec.jsx b/src/applications/mhv-supply-reordering/tests/containers/IntroductionPage.unit.spec.jsx new file mode 100644 index 000000000000..96236de1f574 --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/containers/IntroductionPage.unit.spec.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import { expect } from 'chai'; +import formConfig from '../../config/form'; +import IntroductionPage from '../../containers/IntroductionPage'; + +const props = { + route: { + path: 'introduction', + pageList: [], + formConfig, + }, + userLoggedIn: false, + userIdVerified: true, +}; + +const mockStore = { + getState: () => ({ + user: { + login: { + currentlyLoggedIn: false, + }, + profile: { + savedForms: [], + prefillsAvailable: [], + loa: { + current: 3, + highest: 3, + }, + verified: true, + dob: '2000-01-01', + claims: { + appeals: false, + }, + }, + }, + form: { + formId: formConfig.formId, + loadedStatus: 'success', + savedStatus: '', + loadedData: { + metadata: {}, + }, + data: {}, + }, + scheduledDowntime: { + globalDowntime: null, + isReady: true, + isPending: false, + serviceMap: { get() {} }, + dismissedDowntimeWarnings: [], + }, + }), + subscribe: () => {}, + dispatch: () => {}, +}; + +describe('IntroductionPage', () => { + it('should render', () => { + const { container } = render( + + + , + ); + expect(container).to.exist; + }); +}); diff --git a/src/applications/mhv-supply-reordering/tests/fixtures/data/minimal-test.json b/src/applications/mhv-supply-reordering/tests/fixtures/data/minimal-test.json new file mode 100644 index 000000000000..b57dee5b87cf --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/fixtures/data/minimal-test.json @@ -0,0 +1,9 @@ +{ + "data": { + "fullName": { + "first": "John", + "last": "Doe" + }, + "dateOfBirth": "1980-01-01" + } +} diff --git a/src/applications/mhv-supply-reordering/tests/fixtures/mocks/user.json b/src/applications/mhv-supply-reordering/tests/fixtures/mocks/user.json new file mode 100644 index 000000000000..c707324756e1 --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/fixtures/mocks/user.json @@ -0,0 +1,56 @@ +{ + "data": { + "attributes": { + "profile": { + "sign_in": { + "service_name": "idme" + }, + "email": "john.doe@example.com", + "loa": { "current": 3 }, + "first_name": "John", + "middle_name": "", + "last_name": "Doe", + "gender": "M", + "birth_date": "1985-01-01", + "verified": true + }, + "veteran_status": { + "status": "OK", + "is_veteran": true, + "served_in_military": true + }, + "in_progress_forms": [], + "prefills_available": [], + "services": [ + "facilities", + "hca", + "edu-benefits", + "evss-claims", + "form526", + "user-profile", + "health-records", + "rx", + "messaging" + ], + "va_profile": { + "status": "OK", + "birth_date": "19850101", + "family_name": "Doe", + "gender": "M", + "given_names": ["John", ""], + "active_status": "active", + "facilities": [ + { + "facility_id": "983", + "is_cerner": false + }, + { + "facility_id": "984", + "is_cerner": false + } + ] + } + } + }, + "meta": { "errors": null } +} diff --git a/src/applications/mhv-supply-reordering/tests/mhv-supply-reordering.cypress._spec.js b/src/applications/mhv-supply-reordering/tests/mhv-supply-reordering.cypress._spec.js new file mode 100644 index 000000000000..27beb5a90b5b --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/mhv-supply-reordering.cypress._spec.js @@ -0,0 +1,37 @@ +import path from 'path'; +import testForm from 'platform/testing/e2e/cypress/support/form-tester'; +import { createTestConfig } from 'platform/testing/e2e/cypress/support/form-tester/utilities'; +import mockUser from './fixtures/mocks/user.json'; +import formConfig from '../config/form'; +import manifest from '../manifest.json'; + +const testConfig = createTestConfig( + { + dataPrefix: 'data', + dataDir: path.join(__dirname, 'fixtures', 'data'), + dataSets: ['minimal-test'], + pageHooks: { + introduction: ({ afterHook }) => { + afterHook(() => { + cy.findAllByText(/^start/i, { selector: 'a[href="#start"]' }) + .last() + .click({ force: true }); + }); + }, + }, + + setupPerTest: () => { + cy.intercept('GET', '/v0/user', mockUser); + cy.intercept('POST', formConfig.submitUrl, { status: 200 }); + cy.login(mockUser); + }, + + // Skip tests in CI until the form is released. + // Remove this setting when the form has a content page in production. + skip: Cypress.env('CI'), // doesn't work as advertised. + }, + manifest, + formConfig, +); + +testForm(testConfig); diff --git a/src/applications/mhv-supply-reordering/tests/reducers/mdotInProgressFormsReducer.unit.spec.js b/src/applications/mhv-supply-reordering/tests/reducers/mdotInProgressFormsReducer.unit.spec.js new file mode 100644 index 000000000000..753b5b78b70e --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/reducers/mdotInProgressFormsReducer.unit.spec.js @@ -0,0 +1,58 @@ +import { expect } from 'chai'; + +import { + GET_MDOT_IN_PROGRESS_FORM_STARTED, + GET_MDOT_IN_PROGRESS_FORM_SUCCEEDED, + GET_MDOT_IN_PROGRESS_FORM_FAILED, +} from '../../actions/mdotInProgressForm'; +import { mdotInProgressFormReducer as reducer } from '../../reducers/mdotInProgressFormReducer'; +import { internalServerError } from '../../mocks/in-progress-forms/mdot/errors'; +import mockEndpoints from '../../mocks'; + +const inProgressFormBody = mockEndpoints['GET /v0/in_progress_forms/MDOT']; + +describe('mdotInProgressFormReducer', () => { + let state; + let nextState; + let action; + + beforeEach(() => { + state = undefined; + }); + + describe('action.type: GET_MDOT_IN_PROGRESS_FORM_STARTED', () => { + it('sets loading', () => { + action = { + type: GET_MDOT_IN_PROGRESS_FORM_STARTED, + }; + nextState = reducer(state, action); + expect(nextState.loading).to.be.true; + }); + }); + + describe('action.type: GET_MDOT_IN_PROGRESS_FORM_SUCCEEDED', () => { + it('sets formData', () => { + action = { + type: GET_MDOT_IN_PROGRESS_FORM_SUCCEEDED, + payload: inProgressFormBody, + }; + nextState = reducer(state, action); + expect(nextState.formData).to.deep.equal(inProgressFormBody.formData); + expect(nextState.loading).to.be.false; + expect(nextState.error).to.be.false; + }); + }); + + describe('action.type: GET_MDOT_IN_PROGRESS_FORM_FAILED', () => { + it('sets error', () => { + action = { + type: GET_MDOT_IN_PROGRESS_FORM_FAILED, + payload: internalServerError, + }; + nextState = reducer(state, action); + expect(nextState.error).to.deep.equal(internalServerError.errors.at(0)); + expect(nextState.loading).to.be.false; + expect(nextState.formData).to.deep.equal({}); + }); + }); +}); diff --git a/src/applications/mhv-supply-reordering/tests/selectors/canReorderOn.unit.spec.js b/src/applications/mhv-supply-reordering/tests/selectors/canReorderOn.unit.spec.js new file mode 100644 index 000000000000..5f64d23bce63 --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/selectors/canReorderOn.unit.spec.js @@ -0,0 +1,30 @@ +import { expect } from 'chai'; +import { canReorderOn } from '../../selectors'; +import { supplies as suppliesData } from '../../mocks/in-progress-forms/mdot/supplies'; + +const stateFn = ({ supplies = [] } = {}) => ({ + mdotInProgressForm: { + formData: { + supplies, + }, + error: false, + loading: false, + }, +}); + +let state; + +describe('canReorderOn', () => { + it('returns undefined when state is not set', () => { + expect(canReorderOn({})).to.equal(undefined); + }); + + it('returns the closest date that supplies are able to be reordered', () => { + const unavailableSupplies = suppliesData.map(s => ({ + productName: s.productName, + nextAvailabilityDate: s.nextAvailabilityDate, + })); + state = stateFn({ supplies: unavailableSupplies }); + expect(canReorderOn(state)).to.equal('2022-10-16'); + }); +}); diff --git a/src/applications/mhv-supply-reordering/tests/selectors/selectSupplies.unit.spec.js b/src/applications/mhv-supply-reordering/tests/selectors/selectSupplies.unit.spec.js new file mode 100644 index 000000000000..ae7d3bf422f7 --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/selectors/selectSupplies.unit.spec.js @@ -0,0 +1,52 @@ +import { expect } from 'chai'; +import { selectSupplies, selectUnavailableSupplies } from '../../selectors'; +import { supplies as suppliesData } from '../../mocks/in-progress-forms/mdot/supplies'; + +const stateFn = ({ supplies = [] } = {}) => ({ + mdotInProgressForm: { + formData: { + supplies, + }, + error: false, + loading: false, + }, +}); + +let result; +let state; + +describe('selectSupplies', () => { + it('returns [] when state is not set', () => { + expect(selectSupplies({})).to.deep.equal([]); + }); + + it('returns supplies that are availableForReorder', () => { + state = stateFn({ supplies: suppliesData }); + result = selectSupplies(state); + expect(suppliesData.length).to.eq(4); + expect(result.length).to.eq(3); + const productNames = result.map(({ productName }) => productName); + const expectedProductNames = [ + 'ERHK HE11 680 MINI', + 'AIRFIT F10 M', + 'AIRFIT P10', + ]; + expect(productNames).to.deep.equal(expectedProductNames); + }); +}); + +describe('selectUnavailableSupplies', () => { + it('returns [] when state is not set', () => { + expect(selectUnavailableSupplies({})).to.deep.equal([]); + }); + + it('returns supplies that are not availableForReorder', () => { + state = stateFn({ supplies: suppliesData }); + result = selectUnavailableSupplies(state); + expect(suppliesData.length).to.eq(4); + expect(result.length).to.eq(1); + const productNames = result.map(({ productName }) => productName); + const expectedProductNames = ['AIRCURVE10-ASV-CLIMATELINE']; + expect(productNames).to.deep.equal(expectedProductNames); + }); +}); diff --git a/src/applications/mhv-supply-reordering/tests/selectors/showAlert.unit.spec.js b/src/applications/mhv-supply-reordering/tests/selectors/showAlert.unit.spec.js new file mode 100644 index 000000000000..20a2dabb5a03 --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/selectors/showAlert.unit.spec.js @@ -0,0 +1,107 @@ +import { expect } from 'chai'; +import { MDOT_ERROR_CODES } from '../../constants'; +import { + showAlertDeceased, + showAlertNoRecordForUser, + showAlertNoSuppliesForReorder, + showAlertReorderAccessExpired, + showAlertSomethingWentWrong, +} from '../../selectors'; + +const stateFn = ({ formData = {}, error = false, loading = false } = {}) => ({ + mdotInProgressForm: { + formData, + error, + loading, + }, +}); + +let result; +let state; + +describe('showAlertDeceased', () => { + it('returns true when error.code is MDOT_ERROR_CODES.DECEASED', () => { + state = stateFn({ error: { code: MDOT_ERROR_CODES.DECEASED } }); + result = showAlertDeceased(state); + expect(result).to.eq(true); + }); + + it('returns false, otherwise', () => { + expect(showAlertDeceased(stateFn())).to.eq(false); + expect(showAlertDeceased({})).to.eq(false); + }); +}); + +describe('showAlertNoRecordForUser', () => { + it('returns true when error.code is MDOT_ERROR_CODES.INVALID', () => { + state = stateFn({ error: { code: MDOT_ERROR_CODES.INVALID } }); + result = showAlertNoRecordForUser(state); + expect(result).to.eq(true); + }); + + it('returns false, otherwise', () => { + expect(showAlertNoRecordForUser(stateFn())).to.eq(false); + expect(showAlertNoRecordForUser({})).to.eq(false); + }); +}); + +describe('showAlertNoSuppliesForReorder', () => { + it('returns true when supply.availableForReorder prop is falsy for all supplies', () => { + const supplies = [ + { productId: '123', availableForReorder: null }, + { productId: '456', availableForReorder: false }, + { productId: '789', availableForReorder: undefined }, + { productId: '101' }, + ]; + state = stateFn({ formData: { supplies } }); + result = showAlertNoSuppliesForReorder(state); + expect(result).to.eq(true); + }); + + it('returns false when supplies are availableForReorder', () => { + const supplies = [{ productId: '123', availableForReorder: true }]; + result = showAlertNoSuppliesForReorder(stateFn({ formData: { supplies } })); + expect(result).to.eq(false); + }); + + it('returns false, otherwise', () => { + expect(showAlertNoSuppliesForReorder(stateFn())).to.eq(false); + expect(showAlertNoSuppliesForReorder({})).to.eq(false); + }); +}); + +describe('showAlertReorderAccessExpired', () => { + it('returns true when error.code is MDOT_ERROR_CODES.SUPPLIES_NOT_FOUND', () => { + state = stateFn({ error: { code: MDOT_ERROR_CODES.SUPPLIES_NOT_FOUND } }); + result = showAlertReorderAccessExpired(state); + expect(result).to.eq(true); + }); + + it('returns false, otherwise', () => { + expect(showAlertReorderAccessExpired(stateFn())).to.eq(false); + expect(showAlertReorderAccessExpired({})).to.eq(false); + }); +}); + +describe('showAlertSomethingWentWrong', () => { + [500, 502, 503].forEach(status => { + it(`returns true when error.status is ${status} (server error)`, () => { + state = stateFn({ error: { status } }); + result = showAlertSomethingWentWrong(state); + expect(result).to.eq(true); + }); + }); + + [401, 403, 404].forEach(status => { + it(`returns false when error.status is ${status} (client error)`, () => { + state = stateFn({ error: { status } }); + result = showAlertSomethingWentWrong(state); + expect(result).to.eq(false); + }); + }); + + it('returns false, otherwise', () => { + expect(showAlertSomethingWentWrong(stateFn())).to.eq(false); + expect(showAlertSomethingWentWrong({})).to.eq(false); + }); +}); diff --git a/src/applications/mhv-supply-reordering/tests/utils/helpers.unit.spec.js b/src/applications/mhv-supply-reordering/tests/utils/helpers.unit.spec.js new file mode 100644 index 000000000000..7ec9798412b1 --- /dev/null +++ b/src/applications/mhv-supply-reordering/tests/utils/helpers.unit.spec.js @@ -0,0 +1,75 @@ +import { expect } from 'chai'; +import { + suppliesReplaceSchema, + suppliesUpdateUiSchema, +} from '../../utils/helpers'; + +describe('suppliesReplaceSchema', () => { + it('handles an empty supplies array', () => { + const result = suppliesReplaceSchema({ supplies: [] }); + expect(result).to.deep.equal({ type: 'object', properties: {} }); + }); + + it('handles a null supplies value', () => { + const result = suppliesReplaceSchema({ supplies: null }); + expect(result).to.deep.equal({ type: 'object', properties: {} }); + }); + + it('handles an undefined supplies value', () => { + const result = suppliesReplaceSchema({ supplies: undefined }); + expect(result).to.deep.equal({ type: 'object', properties: {} }); + }); + + it('handles an empty object', () => { + const result = suppliesReplaceSchema({}); + expect(result).to.deep.equal({ type: 'object', properties: {} }); + }); + + it('handles an array of supply objects', () => { + const supplies = [ + { productId: 123, productName: 'Product 123' }, + { productId: 456, productName: 'Product 456' }, + ]; + const expected = { + type: 'object', + properties: { + '123': { type: 'boolean' }, + '456': { type: 'boolean' }, + }, + }; + const result = suppliesReplaceSchema({ supplies }); + expect(result).to.deep.equal(expected); + }); +}); + +describe('suppliesUpdateUiSchema', () => { + it('handles an empty supplies array', () => { + const result = suppliesUpdateUiSchema({ supplies: [] }); + expect(result).to.deep.equal({}); + }); + + it('handles a null supplies value', () => { + const result = suppliesUpdateUiSchema({ supplies: null }); + expect(result).to.deep.equal({}); + }); + + it('handles an undefined supplies value', () => { + const result = suppliesUpdateUiSchema({ supplies: undefined }); + expect(result).to.deep.equal({}); + }); + + it('handles an empty object', () => { + const result = suppliesUpdateUiSchema({}); + expect(result).to.deep.equal({}); + }); + + it('handles an array of supply objects', () => { + const supplies = [ + { productId: 123, productName: 'Product 123' }, + { productId: 456, productName: 'Product 456' }, + ]; + const expectedKeys = ['123', '456']; + const result = suppliesUpdateUiSchema({ supplies }); + expect(Object.keys(result)).to.deep.equal(expectedKeys); + }); +}); diff --git a/src/applications/mhv-supply-reordering/utils/helpers.js b/src/applications/mhv-supply-reordering/utils/helpers.js new file mode 100644 index 000000000000..0a2ac65188e6 --- /dev/null +++ b/src/applications/mhv-supply-reordering/utils/helpers.js @@ -0,0 +1,60 @@ +import { format } from 'date-fns'; +import { + checkboxGroupSchema, + checkboxGroupUI, +} from '@department-of-veterans-affairs/platform-forms-system/web-component-patterns'; + +const formatDate = dateString => + dateString ? format(new Date(dateString), 'MMMM d, yyyy') : undefined; + +const numberOfSuppliesPhrase = count => { + if (count > 1) return `${count} supplies`; + if (count === 1) return `${count} supply`; + return 'no supplies'; +}; + +const sortSupplies = supplies => + supplies?.sort((a, b) => a.productName.localCompare(b.productName)); + +const suppliesReplaceSchema = formData => + checkboxGroupSchema((formData?.supplies || []).map(s => s.productId)); + +const suppliesUpdateUiSchema = formData => + (formData?.supplies || []).reduce( + (acc, { deviceName, lastOrderDate, productId, productName, quantity }) => ({ + ...acc, + [productId]: { + 'ui:title': productName, + 'ui:description': `Device: ${deviceName}\nQuantity: ${quantity}\nLast ordered on ${formatDate( + lastOrderDate, + )}`, + }, + }), + {}, + ); + +const suppliesUi = ({ + title, + description, + hint, + replaceSchema, + updateUiSchema, +}) => + checkboxGroupUI({ + title, + description, + hint, + labels: {}, + required: false, + replaceSchema, + updateUiSchema, + }); + +export { + formatDate, + numberOfSuppliesPhrase, + sortSupplies, + suppliesReplaceSchema, + suppliesUpdateUiSchema, + suppliesUi, +}; diff --git a/src/applications/mhv-supply-reordering/utils/validators.js b/src/applications/mhv-supply-reordering/utils/validators.js new file mode 100644 index 000000000000..26ec96b8fa22 --- /dev/null +++ b/src/applications/mhv-supply-reordering/utils/validators.js @@ -0,0 +1,12 @@ +export function validateAtLeastOneSelected(errors, fieldData, formData) { + if (!formData.supplies || formData.supplies.length === 0) { + errors.addError('Please select at least one supply item.'); + return; + } + + const selectedCount = formData.supplies.filter(item => item['view:selected']) + .length; + if (selectedCount < 1) { + errors.addError('Please select at least one supply item.'); + } +}