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

Commit

Permalink
refactor(patient): decouple patient validator from redux code (#2471)
Browse files Browse the repository at this point in the history
  • Loading branch information
giulianovarriale committed Nov 10, 2020
1 parent 35a1b70 commit b753d90
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 82 deletions.
109 changes: 109 additions & 0 deletions src/__tests__/patients/util/validate-patient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { addDays } from 'date-fns'

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

const patient: Patient = {
id: 'abc123',
sex: 'Male',
givenName: 'John',
dateOfBirth: '01/11/1988',
isApproximateDateOfBirth: false,
code: 'abc123',
index: '1',
carePlans: [],
careGoals: [],
bloodType: 'A+',
visits: [],
rev: 'asd',
createdAt: '01/01/2020',
updatedAt: '01/01/2020',
phoneNumbers: [],
emails: [],
addresses: [],
}

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?.fieldErrors.givenName).toEqual('patient.errors.patientGivenNameFeedback')
expect(error?.count).toEqual(1)
})

it('should validate that the patient birthday is not a future date', () => {
const error = validatePatient({
...patient,
dateOfBirth: addDays(new Date(), 4).toISOString(),
})

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', () => {
const error = validatePatient({
...patient,
phoneNumbers: [{ id: 'abc', value: 'not a phone number' }],
})

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

it('should validate that the patient email is a valid email', () => {
const error = validatePatient({
...patient,
emails: [{ id: 'abc', value: 'not a phone number' }],
})

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

it('should validate fields that should only contian alpha characters', () => {
const error = validatePatient({
...patient,
suffix: 'A123',
familyName: 'B456',
prefix: 'C987',
preferredLanguage: 'D321',
})

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)
})
})
97 changes: 15 additions & 82 deletions src/patients/patient-slice.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { isAfter, parseISO } from 'date-fns'
import { isEmpty } from 'lodash'
import validator from 'validator'

import PatientRepository from '../shared/db/PatientRepository'
import Diagnosis from '../shared/model/Diagnosis'
import Patient from '../shared/model/Patient'
import { AppThunk } from '../shared/store'
import { uuid } from '../shared/util/uuid'
import { cleanupPatient } from './util/set-patient-helper'
import validatePatient from './util/validate-patient'

interface PatientState {
status: 'loading' | 'error' | 'completed'
Expand Down Expand Up @@ -106,80 +105,6 @@ export const {
addDiagnosisError,
} = patientSlice.actions

function validatePatient(patient: Patient) {
const error: Error = {}

const regexContainsNumber = /\d/

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

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

if (patient.suffix) {
if (regexContainsNumber.test(patient.suffix)) {
error.suffix = 'patient.errors.patientNumInSuffixFeedback'
}
}

if (patient.prefix) {
if (regexContainsNumber.test(patient.prefix)) {
error.prefix = 'patient.errors.patientNumInPrefixFeedback'
}
}

if (patient.familyName) {
if (regexContainsNumber.test(patient.familyName)) {
error.familyName = 'patient.errors.patientNumInFamilyNameFeedback'
}
}

if (patient.preferredLanguage) {
if (regexContainsNumber.test(patient.preferredLanguage)) {
error.preferredLanguage = 'patient.errors.patientNumInPreferredLanguageFeedback'
}
}

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 (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
}
}

return error
}

export const createPatient = (
patient: Patient,
onSuccess?: (patient: Patient) => void,
Expand All @@ -189,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 @@ -211,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
98 changes: 98 additions & 0 deletions src/patients/util/validate-patient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { isAfter, parseISO } from 'date-fns'
import validator from 'validator'

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

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)
}

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

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.fieldErrors.givenName = 'patient.errors.patientGivenNameFeedback'
}

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

if (existAndHasNumbers(patient.suffix)) {
error.fieldErrors.suffix = 'patient.errors.patientNumInSuffixFeedback'
}

if (existAndHasNumbers(patient.prefix)) {
error.fieldErrors.prefix = 'patient.errors.patientNumInPrefixFeedback'
}

if (existAndHasNumbers(patient.familyName)) {
error.fieldErrors.familyName = 'patient.errors.patientNumInFamilyNameFeedback'
}

if (existAndHasNumbers(patient.preferredLanguage)) {
error.fieldErrors.preferredLanguage = 'patient.errors.patientNumInPreferredLanguageFeedback'
}

const emailsErrors = validateEmails(patient.emails)
const phoneNumbersErrors = validatePhoneNumbers(patient.phoneNumbers)

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

if (phoneNumbersErrors.some(Boolean)) {
error.fieldErrors.phoneNumbers = phoneNumbersErrors
}

if (error.count === 0) {
return null
}

return error
}

1 comment on commit b753d90

@vercel
Copy link

@vercel vercel bot commented on b753d90 Nov 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.