Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Date validation easy renewals #2108

Merged
merged 4 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
2 changes: 0 additions & 2 deletions packages/gafl-webapp-service/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@
"disability_concession_title_you": "Do you receive any of the following?",
"dob_day": "day",
"dob_entry_hint": "For example, 23 11 1979",

"dob_error_date_real": "Date of birth must be a real date",
"dob_error_missing_day_and_month": "Date of birth must include a day and month",
"dob_error_missing_day_and_year": "Date of birth must include a day and year",
Expand All @@ -284,7 +283,6 @@
"dob_error_year_min": "Date of birth is too long ago",
"dob_error_year_max": "The date of birth must be in the past",
"dob_error": "Enter a date of birth",

"dob_month": "month",
"dob_privacy_link_prefix": "If you do not provide a correct date of birth, this may cause delays when a licence is renewed or mean that a licence is not valid. Read about ",
"dob_privacy_link": "how we use personal information (opens in new tab)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jest.mock('../../../../routes/page-route.js')

const mockTransactionSet = jest.fn()

const getMockRequest = (permission = getSamplePermission(), year, month, day) => ({
const getMockRequest = (permission = getSamplePermission(), year, month, day, error) => ({
jaucourt marked this conversation as resolved.
Show resolved Hide resolved
cache: () => ({
helpers: {
status: {
Expand All @@ -57,7 +57,8 @@ const getMockRequest = (permission = getSamplePermission(), year, month, day) =>
'licence-start-date-year': year,
'licence-start-date-month': month,
'licence-start-date-day': day
}
},
error
}),
setCurrentPermission: jest.fn()
},
Expand Down Expand Up @@ -131,6 +132,21 @@ describe('getData', () => {
expect(displayExpiryDate).toHaveBeenCalledWith(request, permission)
})

it.each([
['full-date', 'object.missing'],
['day', 'any.required']
])('should add error details ({%s: %s}) to the page data', async (errorKey, errorValue) => {
const error = { [errorKey]: errorValue }

const result = await getData(getMockRequest(undefined, undefined, undefined, undefined, error))
expect(result.error).toEqual({ errorKey, errorValue })
})

it('omits error if there is no error', async () => {
const result = await getData(getMockRequest())
expect(result.error).toBeUndefined()
})

it('getData returns expected outputs', async () => {
const expected = await getData(getMockRequest())
expect(expected).toMatchSnapshot()
Expand Down Expand Up @@ -187,38 +203,3 @@ describe('licenceStartTime and licenceToStart values', () => {
}
)
})

describe('validator', () => {
const getMockOptions = renewedEndDate => ({
context: {
app: {
request: {
permission: {
renewedEndDate
}
}
}
}
})

const validator = pageRoute.mock.calls[0][2]
beforeEach(jest.clearAllMocks)

it('validation fails', () => {
return expect(() =>
validator(
{ 'licence-start-date-year': 1990, 'licence-start-date-month': 11, 'licence-start-date-day': 11 },
getMockOptions('1990-10-10')
)
).toThrow()
})

it('validation succeeds', () => {
return expect(
validator(
{ 'licence-start-date-year': 1990, 'licence-start-date-month': 2, 'licence-start-date-day': 1 },
getMockOptions('1990-02-01')
)
).toBeUndefined()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,37 @@

{%
set errorMap = {
'licence-start-date': {
'date.format': { ref: '#licence-start-date-day', text: mssgs.renewal_start_date_error_format },
'date.max': { ref: '#licence-start-date-day', text: mssgs.renewal_start_date_error_max_1 + data.advancedPurchaseMaxDays + mssgs.renewal_start_date_error_max_2 + data.maxStartDate },
'date.min': { ref: '#licence-start-date-day', text: mssgs.renewal_start_date_error_min }
}
'full-date': {
'object.missing': { ref: '#licence-start-date-day', text: mssgs.licence_start_error }
},
'day-and-month': {
'object.missing': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_missing_day_and_month }
},
'day-and-year': {
'object.missing': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_missing_day_and_year }
},
'month-and-year': {
'object.missing': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_missing_month_and_year }
},
'day': {
'any.required': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_missing_day }
},
'month': {
'any.required': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_missing_month }
},
'year': {
'any.required': { ref: '#licence-start-date-year', text: mssgs.licence_start_error_missing_year }
},
'non-numeric': {
'number.base': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_non_numeric }
},
'invalid-date': {
'any.custom': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_date_real }
},
'date-range': {
'date.min': { ref: '#licence-start-date-day', text: mssgs.renewal_start_date_error_min },
'date.max': { ref: '#licence-start-date-day', text: mssgs.renewal_start_date_error_max_1 + data.advancedPurchaseMaxDays + mssgs.renewal_start_date_error_max_2 + data.maxStartDate }
jaucourt marked this conversation as resolved.
Show resolved Hide resolved
}
}
%}

Expand Down Expand Up @@ -43,7 +69,7 @@
id: "licence-start-date",
namePrefix: "licence-start-date",
items: dateInputItems,
errorMessage: { text: mssgs.renewal_start_date_error_valid_date } if error,
errorMessage: { text: errorMap[data.error.errorKey][data.error.errorValue].text } if data.error,
hint: {
text: mssgs.renewal_start_date_error_hint_1 + data.minStartDate + mssgs.and + data.maxStartDate
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,12 @@
import { ADVANCED_PURCHASE_MAX_DAYS, SERVICE_LOCAL_TIME } from '@defra-fish/business-rules-lib'
import { dateFormats } from '../../../constants.js'
import { RENEWAL_START_DATE, LICENCE_SUMMARY } from '../../../uri.js'
import pageRoute from '../../../routes/page-route.js'
import Joi from 'joi'
import JoiDate from '@hapi/joi-date'
import moment from 'moment-timezone'
import { displayExpiryDate, cacheDateFormat } from '../../../processors/date-and-time-display.js'
import { addLanguageCodeToUri } from '../../../processors/uri-helper.js'
import { licenceToStart } from '../../licence-details/licence-to-start/update-transaction.js'
import { ageConcessionHelper } from '../../../processors/concession-helper.js'

const JoiX = Joi.extend(JoiDate)

const validator = (payload, options) => {
const { permission } = options.context.app.request
const endDateMoment = moment.utc(permission.renewedEndDate).tz(SERVICE_LOCAL_TIME)
const licenceStartDate = `${payload['licence-start-date-year']}-${payload['licence-start-date-month']}-${payload['licence-start-date-day']}`

return Joi.assert(
{ 'licence-start-date': licenceStartDate },
Joi.object({
'licence-start-date': JoiX.date()
.format(dateFormats)
.min(endDateMoment.clone().startOf('day'))
.max(endDateMoment.clone().add(ADVANCED_PURCHASE_MAX_DAYS, 'days'))
.required()
}).options({ abortEarly: false, allowUnknown: true })
)
}
import { renewalStartDateValidator } from '../../../schema/validators/validators.js'

const setLicenceStartDateAndTime = async request => {
const permission = await request.cache().helpers.transaction.getCurrentPermission()
Expand Down Expand Up @@ -64,20 +43,35 @@ const setLicenceStartDateAndTime = async request => {

const getData = async request => {
const permission = await request.cache().helpers.transaction.getCurrentPermission()
const page = await request.cache().helpers.page.getCurrentPermission(RENEWAL_START_DATE.page)

const expiryTimeString = displayExpiryDate(request, permission)
const endDateMoment = moment.utc(permission.renewedEndDate, null, request.locale).tz(SERVICE_LOCAL_TIME)

return {
const pageData = {
expiryTimeString,
hasExpired: permission.renewedHasExpired,
minStartDate: endDateMoment.format('DD MM YYYY'),
maxStartDate: endDateMoment.clone().add(ADVANCED_PURCHASE_MAX_DAYS, 'days').format('DD MM YYYY'),
advancedPurchaseMaxDays: ADVANCED_PURCHASE_MAX_DAYS
}

if (page?.error) {
const [errorKey] = Object.keys(page.error)
const errorValue = page.error[errorKey]
pageData.error = { errorKey, errorValue }
}

return pageData
}

const route = pageRoute(RENEWAL_START_DATE.page, RENEWAL_START_DATE.uri, validator, request => setLicenceStartDateAndTime(request), getData)
const route = pageRoute(
RENEWAL_START_DATE.page,
RENEWAL_START_DATE.uri,
renewalStartDateValidator,
request => setLicenceStartDateAndTime(request),
getData
)
route.find(r => r.method === 'POST').options.ext = {
onPostAuth: {
method: async (request, reply) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Joi from 'joi'
import { dateOfBirthValidator, startDateValidator, getDateErrorFlags } from '../validators.js'
import { dateOfBirthValidator, startDateValidator, getDateErrorFlags, renewalStartDateValidator } from '../validators.js'
import moment from 'moment-timezone'
const dateSchema = require('../../date.schema.js')

Expand Down Expand Up @@ -206,3 +206,87 @@ describe('getErrorFlags', () => {
expect(result).toEqual(expect.objectContaining(expected))
})
})

describe('renewalStartDateValidator', () => {
beforeEach(jest.clearAllMocks)

const getSamplePayload = ({ day = '', month = '', year = '' } = {}) => ({
'licence-start-date-day': day,
'licence-start-date-month': month,
'licence-start-date-year': year
})
const renewedEndDate = moment()
const options = {
context: {
app: {
request: {
permission: {
renewedEndDate: renewedEndDate.toISOString()
}
}
}
}
}
it('throws an error for a licence starting before today', () => {
const renewedDate = moment().subtract(1, 'day')
const samplePayload = getSamplePayload({
day: renewedDate.format('DD'),
month: renewedDate.format('MM'),
year: renewedDate.format('YYYY')
})
expect(() => renewalStartDateValidator(samplePayload, options)).toThrow()
})

it('throws an error for a licence starting more than 30 days hence', () => {
const renewedDate = moment().add(31, 'days')
const samplePayload = getSamplePayload({
day: renewedDate.format('DD'),
month: renewedDate.format('MM'),
year: renewedDate.format('YYYY')
})
expect(() => renewalStartDateValidator(samplePayload, options)).toThrow()
})

it('validates for a date within the next 30 days', () => {
const renewedDate = moment().add(4, 'days')
const samplePayload = getSamplePayload({
day: renewedDate.format('DD'),
month: renewedDate.format('MM'),
year: renewedDate.format('YYYY')
})
expect(() => renewalStartDateValidator(samplePayload, options)).not.toThrow()
})

it.each([
['01', '03', '1994'],
['10', '12', '2004']
])('passes start date day (%s), month (%s) and year (%s) to dateSchemaInput', (day, month, year) => {
setupMocks()
renewalStartDateValidator(getSamplePayload({ day, month, year }), options)
expect(dateSchema.dateSchemaInput).toHaveBeenCalledWith(day, month, year)
tearDownMocks()
})

it('passes dateSchemaInput output and dateSchema to Joi.assert', () => {
setupMocks()
const dsi = Symbol('dsi')
dateSchema.dateSchemaInput.mockReturnValueOnce(dsi)
renewalStartDateValidator(getSamplePayload(), options)
expect(Joi.assert).toHaveBeenCalledWith(dsi, dateSchema.dateSchema)
tearDownMocks()
})

it('passes validation if licence is set to start after payment', () => {
const samplePayload = getSamplePayload({
day: moment().format('DD'),
month: moment().format('MM'),
year: moment().format('YYYY')
})
expect(() => renewalStartDateValidator(samplePayload, options)).not.toThrow()
})

it('throws an error if licence-to-start is set to an invalid value', () => {
const samplePayload = { 'licence-to-start': '12th-of-never' }
expect(() => renewalStartDateValidator(samplePayload, options)).toThrow()
})
})
13 changes: 13 additions & 0 deletions packages/gafl-webapp-service/src/schema/validators/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ export const startDateValidator = payload => {
}
}

export const renewalStartDateValidator = (payload, options) => {
const { permission } = options.context.app.request
const endDateMoment = moment.utc(permission.renewedEndDate).tz(SERVICE_LOCAL_TIME)
const day = payload['licence-start-date-day']
const month = payload['licence-start-date-month']
const year = payload['licence-start-date-year']

const minDate = endDateMoment.clone().startOf('day').toDate()
const maxDate = endDateMoment.clone().add(ADVANCED_PURCHASE_MAX_DAYS, 'days').toDate()

validateDate(day, month, year, minDate, maxDate)
}

export const getDateErrorFlags = error => {
const errorFlags = { isDayError: false, isMonthError: false, isYearError: false }
const commonErrors = ['full-date', 'invalid-date', 'date-range', 'non-numeric']
Expand Down
Loading