Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

refactor(patient): decouple patient validator from redux code #2471

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(patient-validator): clean up code
  • Loading branch information
giulianovarriale committed Nov 3, 2020
commit 28782d7fdbaf3eccd462675cef8ba80def7a4f0f
66 changes: 46 additions & 20 deletions src/__tests__/patients/util/validate-patient.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { addDays } from 'date-fns'

import validatePatient from '../../../patients/util/validate-patient'
import validatePatient, { PatientValidationError } from '../../../patients/util/validate-patient'
import Patient from '../../../shared/model/Patient'

const patient = {
const patient: Patient = {
id: 'abc123',
sex: 'Male',
givenName: 'John',
Expand All @@ -23,12 +24,38 @@ const patient = {
}

describe('validate patient', () => {
describe('PatientValidationError class', () => {
it('should count the amount of errors', () => {
const error = new PatientValidationError()

expect(error.count).toEqual(0)

error.fieldErrors.givenName = 'patient.errors.patientGivenNameFeedback'
error.fieldErrors.dateOfBirth = 'patient.errors.patientDateOfBirthFeedback'
error.fieldErrors.suffix = 'patient.errors.patientNumInSuffixFeedback'

expect(error.count).toEqual(3)

error.fieldErrors.prefix = 'patient.errors.patientNumInPrefixFeedback'
error.fieldErrors.familyName = 'patient.errors.patientNumInFamilyNameFeedback'
error.fieldErrors.preferredLanguage = 'patient.errors.patientNumInPreferredLanguageFeedback'
error.fieldErrors.emails = ['patient.errors.invalidEmail']
error.fieldErrors.phoneNumbers = ['patient.errors.invalidPhoneNumber']

expect(error.count).toEqual(8)
})
})

it('returns null when patient is valid', () => {
const error = validatePatient({ ...patient })
expect(error).toEqual(null)
})

it('should validate the patient required fields', () => {
const error = validatePatient({ ...patient, givenName: '' })

expect(error).toEqual({
givenName: 'patient.errors.patientGivenNameFeedback',
})
expect(error?.fieldErrors.givenName).toEqual('patient.errors.patientGivenNameFeedback')
expect(error?.count).toEqual(1)
})

it('should validate that the patient birthday is not a future date', () => {
Expand All @@ -37,9 +64,8 @@ describe('validate patient', () => {
dateOfBirth: addDays(new Date(), 4).toISOString(),
})

expect(error).toEqual({
dateOfBirth: 'patient.errors.patientDateOfBirthFeedback',
})
expect(error?.fieldErrors.dateOfBirth).toEqual('patient.errors.patientDateOfBirthFeedback')
expect(error?.count).toEqual(1)
})

it('should validate that the patient phone number is a valid phone number', () => {
Expand All @@ -48,9 +74,8 @@ describe('validate patient', () => {
phoneNumbers: [{ id: 'abc', value: 'not a phone number' }],
})

expect(error).toEqual({
phoneNumbers: ['patient.errors.invalidPhoneNumber'],
})
expect(error?.fieldErrors.phoneNumbers).toEqual(['patient.errors.invalidPhoneNumber'])
expect(error?.count).toEqual(1)
})

it('should validate that the patient email is a valid email', () => {
Expand All @@ -59,9 +84,8 @@ describe('validate patient', () => {
emails: [{ id: 'abc', value: 'not a phone number' }],
})

expect(error).toEqual({
emails: ['patient.errors.invalidEmail'],
})
expect(error?.fieldErrors.emails).toEqual(['patient.errors.invalidEmail'])
expect(error?.count).toEqual(1)
})

it('should validate fields that should only contian alpha characters', () => {
Expand All @@ -73,11 +97,13 @@ describe('validate patient', () => {
preferredLanguage: 'D321',
})

expect(error).toEqual({
suffix: 'patient.errors.patientNumInSuffixFeedback',
familyName: 'patient.errors.patientNumInFamilyNameFeedback',
prefix: 'patient.errors.patientNumInPrefixFeedback',
preferredLanguage: 'patient.errors.patientNumInPreferredLanguageFeedback',
})
expect(error?.fieldErrors.suffix).toEqual('patient.errors.patientNumInSuffixFeedback')
expect(error?.fieldErrors.familyName).toEqual('patient.errors.patientNumInFamilyNameFeedback')
expect(error?.fieldErrors.prefix).toEqual('patient.errors.patientNumInPrefixFeedback')
expect(error?.fieldErrors.preferredLanguage).toEqual(
'patient.errors.patientNumInPreferredLanguageFeedback',
)

expect(error?.count).toEqual(4)
})
})
20 changes: 14 additions & 6 deletions src/patients/patient-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,20 @@ export const createPatient = (
const cleanPatient = cleanupPatient(patient)
const newPatientError = validatePatient(cleanPatient)

if (isEmpty(newPatientError)) {
if (!newPatientError) {
const newPatient = await PatientRepository.save(cleanPatient)
dispatch(createPatientSuccess())

if (onSuccess) {
onSuccess(newPatient)
}
} else {
newPatientError.message = 'patient.errors.createPatientError'
dispatch(createPatientError(newPatientError))
dispatch(
createPatientError({
...newPatientError.fieldErrors,
message: 'patient.errors.createPatientError',
}),
)
}
}

Expand All @@ -136,16 +140,20 @@ export const updatePatient = (
const cleanPatient = cleanupPatient(patient)
const updateError = validatePatient(cleanPatient)

if (isEmpty(updateError)) {
if (!updateError) {
const updatedPatient = await PatientRepository.saveOrUpdate(cleanPatient)
dispatch(updatePatientSuccess(updatedPatient))

if (onSuccess) {
onSuccess(updatedPatient)
}
} else {
updateError.message = 'patient.errors.updatePatientError'
dispatch(updatePatientError(updateError))
dispatch(
updatePatientError({
...updateError.fieldErrors,
message: 'patient.errors.updatePatientError',
}),
)
}
}

Expand Down
132 changes: 70 additions & 62 deletions src/patients/util/validate-patient.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,97 @@
import { isAfter, parseISO } from 'date-fns'
import validator from 'validator'

import { ContactInfoPiece } from '../../shared/model/ContactInformation'
import Patient from '../../shared/model/Patient'

interface Error {
message?: string
givenName?: string
dateOfBirth?: string
suffix?: string
prefix?: string
familyName?: string
preferredLanguage?: string
emails?: (string | undefined)[]
phoneNumbers?: (string | undefined)[]
const validateEmails = (emails: ContactInfoPiece[] | undefined) =>
(emails ?? []).map((email) =>
!validator.isEmail(email.value) ? 'patient.errors.invalidEmail' : undefined,
)

const validatePhoneNumbers = (phoneNumbers: ContactInfoPiece[] | undefined) =>
(phoneNumbers ?? []).map((phone) =>
!validator.isMobilePhone(phone.value) ? 'patient.errors.invalidPhoneNumber' : undefined,
)

const existAndIsAfterToday = (value: string | undefined) => {
if (!value) {
return false
}

const today = new Date(Date.now())
const dateOfBirth = parseISO(value)

return isAfter(dateOfBirth, today)
}

export default function validatePatient(patient: Patient) {
const error: Error = {}
const existAndHasNumbers = (value: string | undefined) => value && /\d/.test(value)

interface IPatientFieldErrors {
givenName?: 'patient.errors.patientGivenNameFeedback'
dateOfBirth?: 'patient.errors.patientDateOfBirthFeedback'
suffix?: 'patient.errors.patientNumInSuffixFeedback'
prefix?: 'patient.errors.patientNumInPrefixFeedback'
familyName?: 'patient.errors.patientNumInFamilyNameFeedback'
preferredLanguage?: 'patient.errors.patientNumInPreferredLanguageFeedback'
emails?: ('patient.errors.invalidEmail' | undefined)[]
phoneNumbers?: ('patient.errors.invalidPhoneNumber' | undefined)[]
}

export class PatientValidationError extends Error {
public fieldErrors: IPatientFieldErrors

const regexContainsNumber = /\d/
constructor() {
super('Patient data is invalid.')
this.name = 'PatientValidationError'
this.fieldErrors = {}
}

get count(): number {
return Object.keys(this.fieldErrors).length
}
}

export default function validatePatient(patient: Patient) {
const error = new PatientValidationError()

if (!patient.givenName) {
error.givenName = 'patient.errors.patientGivenNameFeedback'
error.fieldErrors.givenName = 'patient.errors.patientGivenNameFeedback'
}

if (existAndIsAfterToday(patient.dateOfBirth)) {
error.fieldErrors.dateOfBirth = 'patient.errors.patientDateOfBirthFeedback'
}

if (patient.dateOfBirth) {
const today = new Date(Date.now())
const dob = parseISO(patient.dateOfBirth)
if (isAfter(dob, today)) {
error.dateOfBirth = 'patient.errors.patientDateOfBirthFeedback'
}
if (existAndHasNumbers(patient.suffix)) {
error.fieldErrors.suffix = 'patient.errors.patientNumInSuffixFeedback'
}

if (patient.suffix) {
if (regexContainsNumber.test(patient.suffix)) {
error.suffix = 'patient.errors.patientNumInSuffixFeedback'
}
if (existAndHasNumbers(patient.prefix)) {
error.fieldErrors.prefix = 'patient.errors.patientNumInPrefixFeedback'
}

if (patient.prefix) {
if (regexContainsNumber.test(patient.prefix)) {
error.prefix = 'patient.errors.patientNumInPrefixFeedback'
}
if (existAndHasNumbers(patient.familyName)) {
error.fieldErrors.familyName = 'patient.errors.patientNumInFamilyNameFeedback'
}

if (patient.familyName) {
if (regexContainsNumber.test(patient.familyName)) {
error.familyName = 'patient.errors.patientNumInFamilyNameFeedback'
}
if (existAndHasNumbers(patient.preferredLanguage)) {
error.fieldErrors.preferredLanguage = 'patient.errors.patientNumInPreferredLanguageFeedback'
}

if (patient.preferredLanguage) {
if (regexContainsNumber.test(patient.preferredLanguage)) {
error.preferredLanguage = 'patient.errors.patientNumInPreferredLanguageFeedback'
}
const emailsErrors = validateEmails(patient.emails)
const phoneNumbersErrors = validatePhoneNumbers(patient.phoneNumbers)

if (emailsErrors.some(Boolean)) {
error.fieldErrors.emails = emailsErrors
}

if (patient.emails) {
const errors: (string | undefined)[] = []
patient.emails.forEach((email) => {
if (!validator.isEmail(email.value)) {
errors.push('patient.errors.invalidEmail')
} else {
errors.push(undefined)
}
})
// Only add to error obj if there's an error
if (errors.some((value) => value !== undefined)) {
error.emails = errors
}
if (phoneNumbersErrors.some(Boolean)) {
error.fieldErrors.phoneNumbers = phoneNumbersErrors
}

if (patient.phoneNumbers) {
const errors: (string | undefined)[] = []
patient.phoneNumbers.forEach((phoneNumber) => {
if (!validator.isMobilePhone(phoneNumber.value)) {
errors.push('patient.errors.invalidPhoneNumber')
} else {
errors.push(undefined)
}
})
// Only add to error obj if there's an error
if (errors.some((value) => value !== undefined)) {
error.phoneNumbers = errors
}
if (error.count === 0) {
return null
}

return error
Expand Down