import { MessageDescriptor } from 'react-intl' ; import { validationMessages as messages } from '@client/i18n/messages' ; import { IFormFieldValue , IFormData } from '@opencrvs/client/src/forms' ; import { REGEXP_BLOCK_ALPHA_NUMERIC_DOT , REGEXP_ALPHA_NUMERIC , REGEXP_DECIMAL_POINT_NUMBER } from '@client/utils/constants' ; import { validate as validateEmail } from 'email-validator' ; import * as XRegExp from 'xregexp' ; import { isArray } from 'util' ; import { NATIONAL_ID , BIRTH_REGISTRATION_NUMBER , DEATH_REGISTRATION_NUMBER , PASSPORT , DRIVING_LICENSE } from '@client/forms/identity' ; import moment from 'moment' ; import { IOfflineData } from '@client/offline/reducer' ; import { getListOfLocations } from '@client/forms/utils' ; export interface IValidationResult { message : MessageDescriptor ; props ? : { [ key ] : any ; } ; } export type RangeValidation = ( min : number , max : number ) => ( value : IFormFieldValue ) => IValidationResult | undefined ; export type MaxLengthValidation = ( customisation : number ) => ( value : IFormFieldValue ) => IValidationResult | undefined ; export type Validation = ( value : IFormFieldValue , drafts ? : IFormData , resources ? : IOfflineData ) => IValidationResult | undefined ; export type ValidationInitializer = ( ... value : any [ ] ) => Validation ; export const isAValidPhoneNumberFormat = ( value ) : boolean => { const { pattern } = window . config . PHONE_NUMBER_PATTERN ; return pattern . test ( value ) ; } ; export const isAValidEmailAddressFormat = ( value ) : boolean => { return validateEmail ( value ) ; } ; export const isAValidDateFormat = ( value ) : boolean => { const dateFormatRegex = '^\\d{4}-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])$' ; const dateRegex = new RegExp ( dateFormatRegex ) ; if ( ! dateRegex . test ( value ) ) { return false ; } const pad = ( n ) => { return ( str ) => { while ( str . length < n ) { str = '0' + str ; } return str ; } ; } ; const valueISOString = value . split ( /-/g ) . map ( pad ( 2 ) ) . join ( '-' ) ; const givenDate = new Date ( valueISOString ) ; return givenDate . toISOString ( ) . slice ( 0 , 10 ) === valueISOString ; } ; export const requiredSymbol = ( value ) => value ? undefined : { message : messages . requiredSymbol } ; export const required = ( message = messages . required ) : Validation => ( value ) => { if ( typeof value === 'string' ) { return value !== '' ? undefined : { message } ; } if ( isArray ( value ) ) { return value . length > 0 ? undefined : { message } ; } return value !== undefined && value !== null ? undefined : { message } ; } ; export const minLength = ( min ) => ( value ) => { return value && value . length < min ? { message : messages . minLength , props : { min } } : undefined ; } ; export const validLength = ( length ) => ( value ) => { return value && value . toString ( ) . length === length ? undefined : { message : messages . validNationalId , props : { validLength : length } } ; } ; const isLessOrEqual = ( value , max ) => { return value && value . toString ( ) . length <= max ; } ; export const maxLength = ( max ) => ( value ) => { return isLessOrEqual ( value as string , max ) ? undefined : { message : messages . maxLength , props : { max } } ; } ; const isNumber = ( value ) : boolean => ! value || ! isNaN ( Number ( value ) ) ; const isRegexpMatched = ( value , regexp ) : boolean => ! value || value . match ( regexp ) !== null ; export const blockAlphaNumericDot = ( value ) => { const cast = value as string ; return isRegexpMatched ( cast , REGEXP_BLOCK_ALPHA_NUMERIC_DOT ) ? undefined : { message : messages . blockAlphaNumericDot } ; } ; export const nonDecimalPointNumber = ( value ) => { const cast = value as string ; if ( cast ) { return ! isRegexpMatched ( cast . toString ( ) , REGEXP_DECIMAL_POINT_NUMBER ) ? undefined : { message : messages . nonDecimalPointNumber } ; } } ; export const numeric = ( value ) => { const cast = value as string ; return isNumber ( cast ) ? undefined : { message : messages . numberRequired } ; } ; export const facilityMustBeSelected = ( value , drafts , resources ) => { const locationsList = getListOfLocations ( resources as IOfflineData , 'facilities' ) ; const isValid = ! value || locationsList [ value as string ] ; return isValid ? undefined : { message : messages . facilityMustBeSelected } ; } ; export const phoneNumberFormat = ( value ) => { const { start , num } = window . config . PHONE_NUMBER_PATTERN ; const validationProps = { start , num } ; const cast = value as string ; const trimmedValue = cast === undefined || cast === null ? '' : cast . trim ( ) ; if ( ! trimmedValue ) { return undefined ; } return isAValidPhoneNumberFormat ( trimmedValue ) ? undefined : { message : messages . phoneNumberFormat , props : validationProps } ; } ; export const emailAddressFormat = ( value ) => { const cast = value as string ; return cast && isAValidEmailAddressFormat ( cast ) ? undefined : { message : messages . emailAddressFormat } ; } ; export const dateFormat = ( value ) => { const cast = value as string ; return cast && isAValidDateFormat ( cast ) ? undefined : { message : messages . dateFormat } ; } ; export const isDateNotInFuture = ( date ) => { return new Date ( date ) <= new Date ( Date . now ( ) ) ; } ; export const isDateNotBeforeBirth = ( date , drafts ) => { const birthDate = drafts . deceased && drafts . deceased . birthDate ; return birthDate ? new Date ( date ) >= new Date ( JSON . stringify ( birthDate ) ) : true ; } ; export const isDateNotAfterBirthEvent = ( date , drafts ? ) => { const dateOfBirth = drafts && drafts . child && drafts . child . childBirthDate ; return dateOfBirth ? new Date ( date ) <= new Date ( JSON . stringify ( dateOfBirth ) ) : true ; } ; export const isDateNotAfterDeath = ( date , drafts ? ) => { const deathDate = drafts && drafts . deathEvent && drafts . deathEvent . deathDate ; return deathDate ? new Date ( date ) . setHours ( 0 , 0 , 0 , 0 ) <= new Date ( JSON . stringify ( deathDate ) ) . setHours ( 0 , 0 , 0 , 0 ) : true ; } ; export const isDateAfter = ( first , second ) => { return new Date ( first ) >= new Date ( second ) ; } ; export const minAgeGapExist = ( first , second , minAgeGap ) : boolean => { const diff = ( new Date ( first ) . getTime ( ) - new Date ( second ) . getTime ( ) ) / ( 1000 * 60 * 60 * 24 ) / 365 ; return diff >= minAgeGap ; } ; export const isValidBirthDate = ( value , drafts ? ) => { const cast = value as string ; return ! cast ? { message : messages . required } : cast && isDateNotInFuture ( cast ) && isAValidDateFormat ( cast ) && isDateNotAfterBirthEvent ( cast , drafts as IFormData ) ? isDateNotAfterDeath ( cast , drafts as IFormData ) ? undefined : { message : messages . isDateNotAfterDeath } : { message : messages . isValidBirthDate } ; } ; export const isValidChildBirthDate = ( value ) => { const childBirthDate = value as string ; return ! childBirthDate ? { message : messages . required } : childBirthDate && isAValidDateFormat ( childBirthDate ) && isDateNotInFuture ( childBirthDate ) ? undefined : { message : messages . isValidBirthDate } ; } ; export const isValidParentsBirthDate = ( minAgeGap ) : Validation => ( value , drafts ) => { const parentsBirthDate = value as string ; const childBirthDate = drafts && drafts . child && ( drafts . child . childBirthDate as string ) ; return parentsBirthDate && isAValidDateFormat ( parentsBirthDate ) && isDateNotInFuture ( parentsBirthDate ) ? childBirthDate ? minAgeGapExist ( childBirthDate , parentsBirthDate , minAgeGap ) ? undefined : { message : messages . isValidBirthDate } : undefined : { message : messages . isValidBirthDate } ; } ; export const checkBirthDate = ( marriageDate ) : Validation => ( value ) => { const cast = value as string ; if ( ! isAValidDateFormat ( cast ) ) { return { message : messages . dateFormat } ; } const bDate = new Date ( cast ) ; if ( bDate > new Date ( ) ) { return { message : messages . dateFormat } ; } if ( ! marriageDate || ! isAValidDateFormat ( marriageDate ) ) { return undefined ; } return bDate < new Date ( marriageDate ) ? undefined : { message : messages . dobEarlierThanDom } ; } ; export const checkMarriageDate = ( birthDate ) : Validation => ( value ) => { const cast = value as string ; if ( ! isAValidDateFormat ( cast ) ) { return { message : messages . dateFormat } ; } const mDate = new Date ( cast ) ; if ( mDate > new Date ( ) ) { return { message : messages . dateFormat } ; } if ( ! birthDate || ! isAValidDateFormat ( birthDate ) ) { return undefined ; } return mDate > new Date ( birthDate ) ? undefined : { message : messages . domLaterThanDob } ; } ; export const dateGreaterThan = ( previousDate ) : Validation => ( value ) => { const cast = value as string ; if ( ! previousDate || ! isAValidDateFormat ( previousDate ) ) { return undefined ; } return new Date ( cast ) > new Date ( previousDate ) ? undefined : { message : messages . domLaterThanDob } ; } ; export const dateLessThan = ( laterDate ) : Validation => ( value ) => { const cast = value as string ; if ( ! laterDate || ! isAValidDateFormat ( laterDate ) ) { return undefined ; } return new Date ( cast ) < new Date ( laterDate ) ? undefined : { message : messages . dobEarlierThanDom } ; } ; export const dateNotInFuture = ( ) : Validation => ( value ) => { const cast = value as string ; if ( isDateNotInFuture ( cast ) ) { return undefined ; } else { return { message : messages . dateFormat } ; } } ; export const dateNotToday = ( date ) : boolean => { const today = new Date ( ) . setHours ( 0 , 0 , 0 , 0 ) ; const day = new Date ( date ) . setHours ( 0 , 0 , 0 , 0 ) ; return day !== today ; } ; export const isDateInPast = ( value ) => { const cast = value as string ; if ( isDateNotInFuture ( cast ) && dateNotToday ( cast ) ) { return undefined ; } else { return { message : messages . isValidBirthDate } ; } } ; export const dateInPast = ( ) : Validation => ( value ) => isDateInPast ( value ) ; export const dateFormatIsCorrect = ( ) : Validation => ( value ) => dateFormat ( value ) ; export const isValidBengaliWord = ( value ) : boolean => { const bengaliRe = XRegExp . cache ( '(^[\\p{Bengali}.-]*\\([\\p{Bengali}.-]+\\)[\\p{Bengali}.-]*$)|(^[\\p{Bengali}.-]+$)' , '' ) ; const lettersRe = XRegExp . cache ( '(^[\\pL\\pM.-]*\\([\\pL\\pM.-]+\\)[\\pL\\pM.-]*$)|(^[\\pL\\pM.-]+$)' , '' ) ; return bengaliRe . test ( value ) && lettersRe . test ( value ) ; } ; export const isValidEnglishWord = ( value ) : boolean => { const englishRe = XRegExp . cache ( '(^[\\p{Latin}.-]*\\([\\p{Latin}.-]+\\)[\\p{Latin}.-]*$)|(^[\\p{Latin}.-]+$)' , '' ) ; return englishRe . test ( value ) ; } ; type Checker = ( value : string ) => boolean ; const checkNameWords = ( value , checker ) : boolean => { const trimmedValue = value === undefined || value === null ? '' : value . trim ( ) ; if ( ! trimmedValue ) { return true ; } const parts = trimmedValue . split ( /\s+/ ) ; return parts . every ( checker ) ; } ; export const isValidBengaliName = ( value ) : boolean => { return checkNameWords ( value , isValidBengaliWord ) ; } ; export const isValidEnglishName = ( value ) : boolean => { return checkNameWords ( value , isValidEnglishWord ) ; } ; export const isLengthWithinRange = ( value , min , max ) => ! value || ( value . length >= min && value . length <= max ) ; export const isValueWithinRange = ( min , max ) => ( value ) : boolean => { return ! isNaN ( value ) && value >= min && value <= max ; } ; export const bengaliOnlyNameFormat = ( value ) => { const cast = value as string ; return isValidBengaliName ( cast ) ? undefined : { message : messages . bengaliOnlyNameFormat } ; } ; export const englishOnlyNameFormat = ( value ) => { const cast = value as string ; return isValidEnglishName ( cast ) ? undefined : { message : messages . englishOnlyNameFormat } ; } ; export const range = ( min , max ) => ( value ) => { const cast = value as string ; return isValueWithinRange ( min , max ) ( parseFloat ( cast ) ) ? undefined : { message : messages . range , props : { min , max } } ; } ; const hasValidLength = ( value , length ) : boolean => ! value || value . length === length ; export const isAValidNIDNumberFormat = ( value ) : boolean => { const { pattern } = window . config . NID_NUMBER_PATTERN ; return pattern . test ( value ) ; } ; export const validIDNumber = ( typeOfID ) : Validation => ( value ) => { const validNationalIDLengths = [ 10 , 17 ] ; const validBirthRegistrationNumberLength = 17 ; const validDeathRegistrationNumberLength = 18 ; const validPassportLength = 9 ; const validDrivingLicenseLength = 15 ; value = ( value && value . toString ( ) ) || '' ; switch ( typeOfID ) { case NATIONAL_ID : const { num } = window . config . NID_NUMBER_PATTERN ; const cast = value as string ; const trimmedValue = cast === undefined || cast === null ? '' : cast . trim ( ) ; if ( isAValidNIDNumberFormat ( trimmedValue ) || ! trimmedValue ) { return undefined ; } return { message : messages . validNationalId , props : { min : validNationalIDLengths [ 0 ] , max : validNationalIDLengths [ 1 ] , validLength : num } } ; case BIRTH_REGISTRATION_NUMBER : return hasValidLength ( value . toString ( ) , validBirthRegistrationNumberLength ) ? undefined : { message : messages . validBirthRegistrationNumber , props : { validLength : validBirthRegistrationNumberLength } } ; case DEATH_REGISTRATION_NUMBER : return hasValidLength ( value . toString ( ) , validDeathRegistrationNumberLength ) ? undefined : { message : messages . validDeathRegistrationNumber , props : { validLength : validDeathRegistrationNumberLength } } ; case PASSPORT : return hasValidLength ( value . toString ( ) , validPassportLength ) && isRegexpMatched ( value , REGEXP_ALPHA_NUMERIC ) ? undefined : { message : messages . validPassportNumber , props : { validLength : validPassportLength } } ; case DRIVING_LICENSE : return hasValidLength ( value , validDrivingLicenseLength ) && isRegexpMatched ( value , REGEXP_ALPHA_NUMERIC ) ? undefined : { message : messages . validDrivingLicenseNumber , props : { validLength : validDrivingLicenseLength } } ; default : return undefined ; } } ; export const isValidDeathOccurrenceDate = ( value , drafts ) => { const cast = value && value . toString ( ) ; return cast && isDateNotInFuture ( cast ) && isAValidDateFormat ( cast ) ? isDateNotBeforeBirth ( cast , drafts as IFormData ) ? undefined : { message : messages . isDateNotBeforeBirth } : { message : messages . isValidDateOfDeath } ; } ; export const isMoVisitDateAfterBirthDateAndBeforeDeathDate = ( value , drafts ) => { const cast = value && value . toString ( ) ; if ( drafts && drafts . deathEvent && ! drafts . deathEvent . birthDate ) { if ( drafts && drafts . deathEvent && cast <= drafts . deathEvent . deathDate ) { return undefined ; } else if ( drafts && drafts . deathEvent && cast > drafts . deathEvent . deathDate ) { return { message : messages . isMoVisitAfterDeath } ; } } else { if ( drafts && drafts . deathEvent && cast <= drafts . deathEvent . deathDate && cast >= drafts . deceased . birthDate ) { return undefined ; } else if ( drafts && drafts . deathEvent && cast > drafts . deathEvent . deathDate ) { return { message : messages . isMoVisitAfterDeath } ; } else if ( drafts && drafts . deathEvent && cast < drafts . deceased . birthDate ) { return { message : messages . isMoVisitBeforeBirth } ; } } } ; export const isInformantOfLegalAge = ( value ) => { if ( value ) { if ( minAgeGapExist ( moment ( new Date ( ) ) . format ( 'YYYY-MM-DD' ) , value . toString ( ) , window . config . INFORMANT_MINIMUM_AGE ) ) { return undefined ; } else { return { message : messages . isInformantOfLegalAge } ; } } } ; export const greaterThanZero = ( value ) => { return ! value && value !== 0 ? { message : messages . required } : value && Number ( value ) > 0 ? undefined : { message : messages . greaterThanZero } ; } ; export const notGreaterThan = ( maxValue ) : Validation => ( value ) => { const numericValue = Number . parseInt ( value as string ) ; return value && ! Number . isNaN ( numericValue ) && numericValue <= maxValue ? undefined : { message : messages . notGreaterThan , props : { maxValue } } ; } ;