diff --git a/package.json b/package.json index c23dd474ec..44dd24138a 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "i18next": "~19.4.0", "i18next-browser-languagedetector": "~4.1.0", "i18next-xhr-backend": "~3.2.2", - "node-sass": "~4.14.0", "lodash": "^4.17.15", + "node-sass": "~4.14.0", "pouchdb": "~7.2.1", "pouchdb-adapter-memory": "~7.2.1", "pouchdb-find": "~7.2.1", @@ -33,7 +33,8 @@ "redux-thunk": "~2.3.0", "shortid": "^2.2.15", "typescript": "~3.8.2", - "uuid": "^8.0.0" + "uuid": "^8.0.0", + "validator": "^13.0.0" }, "repository": { "type": "git", @@ -64,6 +65,7 @@ "@types/redux-mock-store": "~1.0.1", "@types/shortid": "^0.0.29", "@types/uuid": "^7.0.0", + "@types/validator": "~13.0.0", "@typescript-eslint/eslint-plugin": "~2.30.0", "@typescript-eslint/parser": "~2.30.0", "commitizen": "~4.0.3", diff --git a/src/__tests__/patients/GeneralInformation.test.tsx b/src/__tests__/patients/GeneralInformation.test.tsx index 1e8738c0b4..cfd0c525a9 100644 --- a/src/__tests__/patients/GeneralInformation.test.tsx +++ b/src/__tests__/patients/GeneralInformation.test.tsx @@ -15,6 +15,8 @@ describe('Error handling', () => { message: 'some message', givenName: 'given name message', dateOfBirth: 'date of birth message', + phoneNumber: 'phone number message', + email: 'email message', } const history = createMemoryHistory() const wrapper = mount( @@ -27,12 +29,18 @@ describe('Error handling', () => { const errorMessage = wrapper.find(Alert) const givenNameInput = wrapper.findWhere((w: any) => w.prop('name') === 'givenName') const dateOfBirthInput = wrapper.findWhere((w: any) => w.prop('name') === 'dateOfBirth') + const emailInput = wrapper.findWhere((w: any) => w.prop('name') === 'email') + const phoneNumberInput = wrapper.findWhere((w: any) => w.prop('name') === 'phoneNumber') expect(errorMessage).toBeTruthy() expect(errorMessage.prop('message')).toMatch(error.message) expect(givenNameInput.prop('isInvalid')).toBeTruthy() expect(givenNameInput.prop('feedback')).toEqual(error.givenName) expect(dateOfBirthInput.prop('isInvalid')).toBeTruthy() expect(dateOfBirthInput.prop('feedback')).toEqual(error.dateOfBirth) + expect(emailInput.prop('feedback')).toEqual(error.email) + expect(emailInput.prop('isInvalid')).toBeTruthy() + expect(phoneNumberInput.prop('feedback')).toEqual(error.phoneNumber) + expect(phoneNumberInput.prop('isInvalid')).toBeTruthy() }) }) diff --git a/src/__tests__/patients/edit/EditPatient.test.tsx b/src/__tests__/patients/edit/EditPatient.test.tsx index 4b0b237d0b..62de9c53a9 100644 --- a/src/__tests__/patients/edit/EditPatient.test.tsx +++ b/src/__tests__/patients/edit/EditPatient.test.tsx @@ -30,7 +30,7 @@ describe('Edit Patient', () => { type: 'charity', occupation: 'occupation', preferredLanguage: 'preferredLanguage', - phoneNumber: 'phoneNumber', + phoneNumber: '123456789', email: 'email@email.com', address: 'address', code: 'P00001', diff --git a/src/__tests__/patients/patient-slice.test.ts b/src/__tests__/patients/patient-slice.test.ts index 58a8d1456b..28ed65761c 100644 --- a/src/__tests__/patients/patient-slice.test.ts +++ b/src/__tests__/patients/patient-slice.test.ts @@ -197,13 +197,12 @@ describe('patients slice', () => { expect(onSuccessSpy).toHaveBeenCalledWith(expectedPatient) }) - it('should validate the patient', async () => { + it('should validate the patient required fields', async () => { const store = mockStore() const expectedPatientId = 'sliceId10' const expectedPatient = { id: expectedPatientId, givenName: undefined, - dateOfBirth: addDays(new Date(), 4).toISOString(), } as Patient const saveOrUpdateSpy = jest .spyOn(PatientRepository, 'saveOrUpdate') @@ -218,10 +217,84 @@ describe('patients slice', () => { createPatientError({ message: 'patient.errors.createPatientError', givenName: 'patient.errors.patientGivenNameFeedback', + }), + ) + }) + + it('should validate that the patient birthday is not a future date', async () => { + const store = mockStore() + const expectedPatientId = 'sliceId10' + const expectedPatient = { + id: expectedPatientId, + givenName: 'some given name', + dateOfBirth: addDays(new Date(), 4).toISOString(), + } as Patient + const saveOrUpdateSpy = jest + .spyOn(PatientRepository, 'saveOrUpdate') + .mockResolvedValue(expectedPatient) + const onSuccessSpy = jest.fn() + + await store.dispatch(createPatient(expectedPatient, onSuccessSpy)) + + expect(onSuccessSpy).not.toHaveBeenCalled() + expect(saveOrUpdateSpy).not.toHaveBeenCalled() + expect(store.getActions()[1]).toEqual( + createPatientError({ + message: 'patient.errors.createPatientError', dateOfBirth: 'patient.errors.patientDateOfBirthFeedback', }), ) }) + + it('should validate that the patient email is a valid email', async () => { + const store = mockStore() + const expectedPatientId = 'sliceId10' + const expectedPatient = { + id: expectedPatientId, + givenName: 'some given name', + phoneNumber: 'not a phone number', + } as Patient + const saveOrUpdateSpy = jest + .spyOn(PatientRepository, 'saveOrUpdate') + .mockResolvedValue(expectedPatient) + const onSuccessSpy = jest.fn() + + await store.dispatch(createPatient(expectedPatient, onSuccessSpy)) + + expect(onSuccessSpy).not.toHaveBeenCalled() + expect(saveOrUpdateSpy).not.toHaveBeenCalled() + expect(store.getActions()[1]).toEqual( + createPatientError({ + message: 'patient.errors.createPatientError', + phoneNumber: 'patient.errors.invalidPhoneNumber', + }), + ) + }) + + it('should validate that the patient phone number is a valid phone number', async () => { + const store = mockStore() + const expectedPatientId = 'sliceId10' + const expectedPatient = { + id: expectedPatientId, + givenName: 'some given name', + phoneNumber: 'not a phone number', + } as Patient + const saveOrUpdateSpy = jest + .spyOn(PatientRepository, 'saveOrUpdate') + .mockResolvedValue(expectedPatient) + const onSuccessSpy = jest.fn() + + await store.dispatch(createPatient(expectedPatient, onSuccessSpy)) + + expect(onSuccessSpy).not.toHaveBeenCalled() + expect(saveOrUpdateSpy).not.toHaveBeenCalled() + expect(store.getActions()[1]).toEqual( + createPatientError({ + message: 'patient.errors.createPatientError', + phoneNumber: 'patient.errors.invalidPhoneNumber', + }), + ) + }) }) describe('fetch patient', () => { diff --git a/src/locales/enUs/translations/patient/index.ts b/src/locales/enUs/translations/patient/index.ts index dd39f9126e..32b6b4b331 100644 --- a/src/locales/enUs/translations/patient/index.ts +++ b/src/locales/enUs/translations/patient/index.ts @@ -94,6 +94,8 @@ export default { updatePatientError: 'Could not update patient.', patientGivenNameFeedback: 'Given Name is required.', patientDateOfBirthFeedback: 'Date of Birth can not be greater than today', + invalidEmail: 'Must be a valid email.', + invalidPhoneNumber: 'Must be a valid phone number.', }, }, } diff --git a/src/patients/GeneralInformation.tsx b/src/patients/GeneralInformation.tsx index 231b9832ef..6f926a6d2d 100644 --- a/src/patients/GeneralInformation.tsx +++ b/src/patients/GeneralInformation.tsx @@ -211,6 +211,8 @@ const GeneralInformation = (props: Props) => { onChange={(event: React.ChangeEvent) => { onInputElementChange(event, 'phoneNumber') }} + feedback={t(error?.phoneNumber)} + isInvalid={!!error?.phoneNumber} type="tel" /> @@ -225,6 +227,8 @@ const GeneralInformation = (props: Props) => { onInputElementChange(event, 'email') }} type="email" + feedback={t(error?.email)} + isInvalid={!!error?.email} /> diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index deb9e2a58d..4488ef5eef 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { isAfter, parseISO } from 'date-fns' import _ from 'lodash' +import validator from 'validator' import { uuid } from '../util/uuid' import Patient from '../model/Patient' import PatientRepository from '../clients/db/PatientRepository' @@ -27,6 +28,8 @@ interface Error { message?: string givenName?: string dateOfBirth?: string + email?: string + phoneNumber?: string } interface AddRelatedPersonError { @@ -150,6 +153,18 @@ function validatePatient(patient: Patient) { } } + if (patient.email) { + if (!validator.isEmail(patient.email)) { + error.email = 'patient.errors.invalidEmail' + } + } + + if (patient.phoneNumber) { + if (!validator.isMobilePhone(patient.phoneNumber)) { + error.phoneNumber = 'patient.errors.invalidPhoneNumber' + } + } + return error }