Skip to content

Commit

Permalink
Merge pull request #2219 from ministryofjustice/bugfix/APS-1631-depar…
Browse files Browse the repository at this point in the history
…ture-before-arrival

APS-1631: departure before arrival
  • Loading branch information
froddd authored Dec 2, 2024
2 parents 46c8116 + 292abab commit ff96e0a
Show file tree
Hide file tree
Showing 17 changed files with 243 additions and 176 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ context('Placements', () => {
// Then the linked placement
placementShowPage.shouldShowLinkedPlacements([
'Placement 10 Jun 2024 to 10 Sep 2024',
'Placement 02 Jan 2026 to 04 Mar 2027',
'Placement 2 Jan 2026 to 4 Mar 2027',
])
})

Expand Down
4 changes: 2 additions & 2 deletions integration_tests/tests/match/match.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import SearchPage from '../../pages/match/searchPage'
import UnableToMatchPage from '../../pages/match/unableToMatchPage'

import {
cas1SpaceBookingFactory,
personFactory,
placementRequestDetailFactory,
spaceBookingFactory,
spaceBookingRequirementsFactory,
spaceSearchParametersUiFactory,
spaceSearchResultsFactory,
Expand Down Expand Up @@ -134,7 +134,7 @@ context('Placement Requests', () => {

// And when I complete the form
const requirements = spaceBookingRequirementsFactory.build()
const spaceBooking = spaceBookingFactory.build({ requirements })
const spaceBooking = cas1SpaceBookingFactory.build({ requirements })
cy.task('stubSpaceBookingCreate', { placementRequestId: placementRequest.id, spaceBooking })
cy.task('stubPlacementRequestsDashboard', { placementRequests: [placementRequest], status: 'matched' })
page.clickSubmit()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { NextFunction, Request, Response } from 'express'
import type { ErrorsAndUserInput } from '@approved-premises/ui'
import { when } from 'jest-when'
import ArrivalsController from './arrivalsController'
import { spaceBookingFactory } from '../../../../testutils/factories'
import { cas1SpaceBookingFactory } from '../../../../testutils/factories'
import { PremisesService } from '../../../../services'
import * as validationUtils from '../../../../utils/validation'
import paths from '../../../../paths/manage'
Expand All @@ -22,7 +22,7 @@ describe('ArrivalsController', () => {
const arrivalsController = new ArrivalsController(premisesService, placementService)

const premisesId = 'premises-id'
const placement = spaceBookingFactory.build()
const placement = cas1SpaceBookingFactory.upcoming().build()

beforeEach(() => {
jest.clearAllMocks()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { when } from 'jest-when'
import type { NextFunction, Request, Response } from 'express'
import * as validationUtils from '../../../../utils/validation'
import { PlacementService, PremisesService } from '../../../../services'
import { departureReasonFactory, referenceDataFactory, spaceBookingFactory } from '../../../../testutils/factories'
import { cas1SpaceBookingFactory, departureReasonFactory, referenceDataFactory } from '../../../../testutils/factories'
import DeparturesController from './departuresController'
import paths from '../../../../paths/manage'
import { ValidationError } from '../../../../utils/errors'
Expand All @@ -29,7 +29,18 @@ describe('DeparturesController', () => {
})

const premisesId = 'premises-id'
const placement = spaceBookingFactory.build()
const TEST_DATE = new Date('2024-11-14T14:00:00.000Z')
const placement = cas1SpaceBookingFactory.current().build({
actualArrivalDate: '2024-10-05T11:30:00.000Z',
})
const departureFormData = {
departureDate: '2024-10-08',
'departureDate-day': '8',
'departureDate-month': '10',
'departureDate-year': '2024',
departureTime: '9:35',
reasonId: BREACH_OR_RECALL_REASON_ID,
}

const rootDepartureReason1 = departureReasonFactory.build({ parentReasonId: null })
const rootDepartureReason2 = departureReasonFactory.build({ id: BREACH_OR_RECALL_REASON_ID, parentReasonId: null })
Expand All @@ -43,17 +54,8 @@ describe('DeparturesController', () => {
childDepartureReason1,
childDepartureReason2,
]
const moveOnCategories = referenceDataFactory.buildList(5)

const TEST_DATE = new Date('2024-11-14T14:00:00.000Z')
const departureFormData = {
departureDate: '2024-10-08',
'departureDate-day': '8',
'departureDate-month': '10',
'departureDate-year': '2024',
departureTime: '9:35',
reasonId: BREACH_OR_RECALL_REASON_ID,
}
const moveOnCategories = referenceDataFactory.buildList(5)

beforeEach(() => {
jest.clearAllMocks()
Expand Down Expand Up @@ -165,48 +167,96 @@ describe('DeparturesController', () => {
expect(errorData).toEqual(expectedErrorData)
})

it('returns a date error for a date in the future', async () => {
const requestHandler = departuresController.saveNew()
describe('future date or time', () => {
it('returns a date error for a date in the future', async () => {
const requestHandler = departuresController.saveNew()

request.body = {
'departureDate-day': '15',
'departureDate-month': '11',
'departureDate-year': '2024',
departureTime: '10:00',
reasonId: rootDepartureReason1.id,
}
request.body = {
'departureDate-day': '15',
'departureDate-month': '11',
'departureDate-year': '2024',
departureTime: '10:00',
reasonId: rootDepartureReason1.id,
}

await requestHandler(request, response, next)
await requestHandler(request, response, next)

const expectedErrorData = {
departureDate: 'The date of departure must be today or in the past',
}
const expectedErrorData = {
departureDate: 'The date of departure must be today or in the past',
}

const errorData = (validationUtils.catchValidationErrorOrPropogate as jest.Mock).mock.lastCall[2].data
const errorData = (validationUtils.catchValidationErrorOrPropogate as jest.Mock).mock.lastCall[2].data

expect(errorData).toEqual(expectedErrorData)
expect(errorData).toEqual(expectedErrorData)
})

it('returns a time error for a date today but time in the future', async () => {
const requestHandler = departuresController.saveNew()

request.body = {
'departureDate-day': '14',
'departureDate-month': '11',
'departureDate-year': '2024',
departureTime: '17:00',
reasonId: rootDepartureReason1.id,
}

await requestHandler(request, response, next)

const expectedErrorData = {
departureTime: 'The time of departure must be in the past',
}

const errorData = (validationUtils.catchValidationErrorOrPropogate as jest.Mock).mock.lastCall[2].data

expect(errorData).toEqual(expectedErrorData)
})
})

it('returns a time error for a date today but time in the future', async () => {
const requestHandler = departuresController.saveNew()
describe('date or time before arrival date', () => {
it('returns a date error for a date before the arrival date', async () => {
const requestHandler = departuresController.saveNew()

request.body = {
'departureDate-day': '14',
'departureDate-month': '11',
'departureDate-year': '2024',
departureTime: '17:00',
reasonId: rootDepartureReason1.id,
}
request.body = {
'departureDate-day': '01',
'departureDate-month': '10',
'departureDate-year': '2024',
departureTime: '10:00',
reasonId: rootDepartureReason1.id,
}

await requestHandler(request, response, next)
await requestHandler(request, response, next)

const expectedErrorData = {
departureTime: 'The time of departure must be in the past',
}
const expectedErrorData = {
departureDate: 'The date of departure must be the same as or after 5 Oct 2024, when the person arrived',
}

const errorData = (validationUtils.catchValidationErrorOrPropogate as jest.Mock).mock.lastCall[2].data
const errorData = (validationUtils.catchValidationErrorOrPropogate as jest.Mock).mock.lastCall[2].data

expect(errorData).toEqual(expectedErrorData)
expect(errorData).toEqual(expectedErrorData)
})

it('returns a time error for a date on the same day but before the arrival time', async () => {
const requestHandler = departuresController.saveNew()

request.body = {
'departureDate-day': '05',
'departureDate-month': '10',
'departureDate-year': '2024',
departureTime: '11:00',
reasonId: rootDepartureReason1.id,
}

await requestHandler(request, response, next)

const expectedErrorData = {
departureTime: 'The time of departure must be after the time of arrival, 11:30 on 5 Oct 2024',
}

const errorData = (validationUtils.catchValidationErrorOrPropogate as jest.Mock).mock.lastCall[2].data

expect(errorData).toEqual(expectedErrorData)
})
})

describe('if the selected reason is not Breach or recall or Planned move-on', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { catchValidationErrorOrPropogate, fetchErrorsAndUserInput } from '../../
import {
DateFormats,
dateAndTimeInputsAreValidDates,
dateIsInThePast,
isToday,
dateIsToday,
datetimeIsInThePast,
timeIsValid24hrFormat,
} from '../../../../utils/dateUtils'
import { ValidationError } from '../../../../utils/errors'
Expand Down Expand Up @@ -82,7 +82,7 @@ export default class DeparturesController {
}
}

private newErrors(body: DepartureFormSessionData): DepartureFormErrors | null {
private newErrors(body: DepartureFormSessionData, placement: Cas1SpaceBooking): DepartureFormErrors | null {
const errors: DepartureFormErrors = {}

const { departureTime, reasonId } = body
Expand All @@ -95,22 +95,38 @@ export default class DeparturesController {
errors.departureDate = 'You must enter a date of departure'
} else if (!dateAndTimeInputsAreValidDates(body as ObjectWithDateParts<'departureDate'>, 'departureDate')) {
errors.departureDate = 'You must enter a valid date of departure'
} else if (!dateIsInThePast(departureDate)) {
} else if (!datetimeIsInThePast(departureDate)) {
errors.departureDate = 'The date of departure must be today or in the past'
} else if (
!dateIsToday(departureDate, placement.actualArrivalDate) &&
datetimeIsInThePast(departureDate, placement.actualArrivalDate)
) {
const actualArrivalDate = DateFormats.isoDateToUIDate(placement.actualArrivalDate, { format: 'short' })
errors.departureDate = `The date of departure must be the same as or after ${actualArrivalDate}, when the person arrived`
}

if (!departureTime) {
errors.departureTime = 'You must enter a time of departure'
} else if (!timeIsValid24hrFormat(departureTime)) {
errors.departureTime = 'You must enter a valid time of departure in 24-hour format'
} else if (isToday(departureDate)) {
} else {
const [hours, minutes] = departureTime.split(':').map(Number)
const now = new Date()

now.setHours(hours, minutes)
if (dateIsToday(departureDate)) {
const departureDateTime = DateFormats.isoToDateObj(departureDate)
departureDateTime.setHours(hours, minutes)

if (!dateIsInThePast(now.toISOString())) {
errors.departureTime = 'The time of departure must be in the past'
if (!datetimeIsInThePast(DateFormats.dateObjToIsoDateTime(departureDateTime))) {
errors.departureTime = 'The time of departure must be in the past'
}
} else if (dateIsToday(departureDate, placement.actualArrivalDate)) {
const departureDateObj = DateFormats.isoToDateObj(departureDate)
departureDateObj.setHours(hours, minutes)

if (datetimeIsInThePast(DateFormats.dateObjToIsoDateTime(departureDateObj), placement.actualArrivalDate)) {
const arrivalTime = placement.actualArrivalDate.substring(11, 16)
errors.departureTime = `The time of departure must be after the time of arrival, ${arrivalTime} on ${DateFormats.isoDateToUIDate(placement.actualArrivalDate, { format: 'short' })}`
}
}
}

Expand All @@ -123,10 +139,12 @@ export default class DeparturesController {

saveNew(): RequestHandler {
return async (req: Request, res: Response) => {
const { token } = req.user
const { premisesId, placementId } = req.params
const placement = await this.premisesService.getPlacement({ token, premisesId, placementId })

try {
const errors = this.newErrors(req.body)
const errors = this.newErrors(req.body, placement)

if (errors) {
throw new ValidationError(errors)
Expand Down Expand Up @@ -168,7 +186,7 @@ export default class DeparturesController {
} = await this.getFormPageData(req)

if (
this.newErrors(departureFormSessionData) ||
this.newErrors(departureFormSessionData, placement) ||
departureFormSessionData.reasonId !== BREACH_OR_RECALL_REASON_ID
) {
return res.redirect(departurePaths.new({ premisesId, placementId }))
Expand Down Expand Up @@ -225,7 +243,10 @@ export default class DeparturesController {
errorsAndUserInput: { userInput, ...errorsData },
} = await this.getFormPageData(req)

if (this.newErrors(departureFormSessionData) || departureFormSessionData.reasonId !== PLANNED_MOVE_ON_REASON_ID) {
if (
this.newErrors(departureFormSessionData, placement) ||
departureFormSessionData.reasonId !== PLANNED_MOVE_ON_REASON_ID
) {
return res.redirect(departurePaths.new({ premisesId, placementId }))
}

Expand Down Expand Up @@ -277,7 +298,7 @@ export default class DeparturesController {
errorsAndUserInput: { userInput, ...errorsData },
} = await this.getFormPageData(req)

if (this.newErrors(departureFormSessionData)) {
if (this.newErrors(departureFormSessionData, placement)) {
return res.redirect(departurePaths.new({ premisesId, placementId }))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { NextFunction, Request, Response } from 'express'
import type { ErrorsAndUserInput } from '@approved-premises/ui'
import { when } from 'jest-when'
import KeyworkerController from './keyworkerController'
import { spaceBookingFactory } from '../../../../testutils/factories'
import { cas1SpaceBookingFactory } from '../../../../testutils/factories'
import { PremisesService } from '../../../../services'
import * as validationUtils from '../../../../utils/validation'
import paths from '../../../../paths/manage'
Expand All @@ -22,7 +22,7 @@ describe('keyworkerController', () => {
const keyworkerController = new KeyworkerController(premisesService, placementService)

const premisesId = 'premises-id'
const placement = spaceBookingFactory.build()
const placement = cas1SpaceBookingFactory.build()
const testStaffCode = 'TestId'
const uiPlacementPagePath = paths.premises.placements.show({ premisesId, placementId: placement.id })
const uiKeyworkerPagePath = paths.premises.placements.keyworker({ premisesId, placementId: placement.id })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ErrorsAndUserInput } from '@approved-premises/ui'
import { when } from 'jest-when'
import { NonArrivalReason } from '@approved-premises/api'
import NonArrivalsController from './nonArrivalsController'
import { referenceDataFactory, spaceBookingFactory } from '../../../../testutils/factories'
import { cas1SpaceBookingFactory, referenceDataFactory } from '../../../../testutils/factories'
import { PremisesService } from '../../../../services'
import * as validationUtils from '../../../../utils/validation'
import managePaths from '../../../../paths/manage'
Expand All @@ -24,7 +24,7 @@ describe('nonArrivalsController', () => {

const nonArrivalsController = new NonArrivalsController(premisesService, placementService)

const placement = spaceBookingFactory.build()
const placement = cas1SpaceBookingFactory.upcoming().build()
const uiPlacementPagePath = managePaths.premises.placements.show({ premisesId, placementId: placement.id })
const uiNonArrivalsPagePath = managePaths.premises.placements.nonArrival({ premisesId, placementId: placement.id })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import SpaceBookingsController from './spaceBookingsController'

import { PlacementRequestService, SpaceService } from '../../../services'
import {
cas1SpaceBookingFactory,
newSpaceBookingFactory,
personFactory,
placementRequestDetailFactory,
spaceBookingFactory,
spaceBookingRequirementsFactory,
} from '../../../testutils/factories'
import { filterOutAPTypes, placementDates } from '../../../utils/match'
Expand Down Expand Up @@ -89,7 +89,7 @@ describe('SpaceBookingsController', () => {
const id = 'placement-request-id'
const requirements = spaceBookingRequirementsFactory.build(requirementsOverride)
const newSpaceBooking = newSpaceBookingFactory.build({ requirements })
const spaceBooking = spaceBookingFactory.build()
const spaceBooking = cas1SpaceBookingFactory.build()

const body = {
arrivalDate: newSpaceBooking.arrivalDate,
Expand Down
Loading

0 comments on commit ff96e0a

Please sign in to comment.