From 595e87abcffd3a2735a5ac803a9e4a59d364ab58 Mon Sep 17 00:00:00 2001 From: Bob Meredith Date: Wed, 25 Dec 2024 06:20:25 +0000 Subject: [PATCH 1/2] APS-1751 Occupancy view criteria tranferred to space booking --- .../pages/match/bookASpacePage.ts | 24 +++++++------ integration_tests/tests/match/match.cy.ts | 30 +++++++++++----- server/@types/ui/index.d.ts | 3 +- .../occupancyViewController.test.ts | 2 +- .../occupancyViewController.ts | 1 + .../spaceBookingsController.test.ts | 21 +++++++++-- .../spaceBookingsController.ts | 35 +++++++++++++++---- .../factories/spaceSearchParameters.ts | 4 +-- server/utils/match/index.test.ts | 21 +++++++---- server/utils/match/index.ts | 25 +++++++++---- .../placementRequests/spaceBookings/new.njk | 2 +- 11 files changed, 121 insertions(+), 47 deletions(-) diff --git a/integration_tests/pages/match/bookASpacePage.ts b/integration_tests/pages/match/bookASpacePage.ts index 6b98c28ac1..1e8e61deff 100644 --- a/integration_tests/pages/match/bookASpacePage.ts +++ b/integration_tests/pages/match/bookASpacePage.ts @@ -1,13 +1,16 @@ -import { ApType, PlacementDates, PlacementRequestDetail, Premises } from '@approved-premises/api' +import { + ApType, + Cas1SpaceBookingCharacteristic, + Cas1SpaceCharacteristic, + PlacementDates, + PlacementRequestDetail, + Premises, +} from '@approved-premises/api' import Page from '../page' import paths from '../../../server/paths/match' import { createQueryString, sentenceCase } from '../../../server/utils/utils' import { DateFormats } from '../../../server/utils/dateUtils' -import { - filterOutAPTypes, - placementDates, - placementLength as placementLengthInDaysAndWeeks, -} from '../../../server/utils/match' +import { placementDates, placementLength as placementLengthInDaysAndWeeks } from '../../../server/utils/match' import { placementCriteriaLabels } from '../../../server/utils/placementCriteriaUtils' import { apTypeLabels } from '../../../server/utils/apTypeLabels' @@ -23,8 +26,9 @@ export default class BookASpacePage extends Page { premisesName: Premises['name'], premisesId: Premises['id'], apType: ApType, + criteria: Array, ) { - const queryString = createQueryString({ startDate, durationDays, premisesName, premisesId, apType }) + const queryString = createQueryString({ startDate, durationDays, premisesName, premisesId, apType, criteria }) const path = `${paths.v2Match.placementRequests.spaceBookings.new({ id: placementRequest.id })}?${queryString}` cy.visit(path) return new BookASpacePage(premisesName) @@ -35,6 +39,7 @@ export default class BookASpacePage extends Page { startDate: string, duration: PlacementDates['duration'], apType: ApType, + criteria?: Array, ): void { const { endDate, placementLength } = placementDates(startDate, duration.toString()) cy.get('dd').contains(apTypeLabels[apType]) @@ -42,10 +47,7 @@ export default class BookASpacePage extends Page { cy.get('dd').contains(DateFormats.isoDateToUIDate(endDate)) cy.get('dd').contains(placementLengthInDaysAndWeeks(placementLength)) cy.get('dd').contains(sentenceCase(placementRequest.gender)) - filterOutAPTypes(placementRequest.essentialCriteria).forEach(requirement => { - cy.get('li').contains(placementCriteriaLabels[requirement]) - }) - filterOutAPTypes(placementRequest.desirableCriteria).forEach(requirement => { + ;(criteria || []).forEach(requirement => { cy.get('li').contains(placementCriteriaLabels[requirement]) }) } diff --git a/integration_tests/tests/match/match.cy.ts b/integration_tests/tests/match/match.cy.ts index d218e10a2e..c2ba75a9fe 100644 --- a/integration_tests/tests/match/match.cy.ts +++ b/integration_tests/tests/match/match.cy.ts @@ -22,7 +22,7 @@ import Page from '../../pages/page' import { signIn } from '../signIn' import ListPage from '../../pages/admin/placementApplications/listPage' -import { filterOutAPTypes, placementDates } from '../../../server/utils/match' +import { filterOutAPTypes, filterToSpaceBookingCharacteristics, placementDates } from '../../../server/utils/match' import BookASpacePage from '../../pages/match/bookASpacePage' import OccupancyViewPage from '../../pages/match/occupancyViewPage' import applicationFactory from '../../../server/testutils/factories/application' @@ -78,7 +78,7 @@ context('Placement Requests', () => { // And the results should have links with the correct AP type and criteria searchPage.shouldHaveSearchParametersInLinks(newSearchParameters) - // And the parameters should be submitted to the API + // // And the parameters should be submitted to the API cy.task('verifySearchSubmit').then(requests => { expect(requests).to.have.length(numberOfSearches) const initialSearchRequestBody = JSON.parse(requests[0].body) @@ -305,22 +305,36 @@ context('Placement Requests', () => { // And there is a placement request waiting for me to match const person = personFactory.build() - const essentialCharacteristics: Array = ['acceptsHateCrimeOffenders'] - const desirableCharacteristics: Array = ['isCatered', 'hasEnSuite'] + const essentialCharacteristics: Array = ['hasEnSuite'] + const desirableCharacteristics: Array = ['isCatered'] const placementRequest = placementRequestDetailFactory.build({ person, status: 'notMatched', duration: durationDays, - essentialCriteria: essentialCharacteristics, + essentialCriteria: [], desirableCriteria: desirableCharacteristics, }) // When I visit the 'Book a space' page cy.task('stubPlacementRequest', placementRequest) - const page = BookASpacePage.visit(placementRequest, startDate, durationDays, premisesName, premisesId, apType) + const page = BookASpacePage.visit( + placementRequest, + startDate, + durationDays, + premisesName, + premisesId, + apType, + filterToSpaceBookingCharacteristics(essentialCharacteristics), + ) // Then I should see the details of the space I am booking - page.shouldShowBookingDetails(placementRequest, startDate, durationDays, apType) + page.shouldShowBookingDetails( + placementRequest, + startDate, + durationDays, + apType, + filterToSpaceBookingCharacteristics(essentialCharacteristics), + ) // And when I complete the form const requirements = spaceBookingRequirementsFactory.build() @@ -346,7 +360,7 @@ context('Placement Requests', () => { premisesId, requirements: { ...spaceBooking.requirements, - essentialCharacteristics: placementRequest.essentialCriteria, + essentialCharacteristics, }, }) }) diff --git a/server/@types/ui/index.d.ts b/server/@types/ui/index.d.ts index 2fba252d2e..1ce5bb5060 100644 --- a/server/@types/ui/index.d.ts +++ b/server/@types/ui/index.d.ts @@ -25,7 +25,6 @@ import { RiskTier, RiskTierLevel, RoshRisks, - Cas1SpaceCharacteristic as SpaceCharacteristic, ApprovedPremisesUser as User, UserQualification, ApprovedPremisesUserRole as UserRole, @@ -386,7 +385,7 @@ export interface SpaceSearchParametersUi { requirements: { apType: ApType gender: Gender - spaceCharacteristics: Array + spaceCharacteristics: Array } } diff --git a/server/controllers/match/placementRequests/occupancyViewController.test.ts b/server/controllers/match/placementRequests/occupancyViewController.test.ts index 5e393a0ab2..5165e7a276 100644 --- a/server/controllers/match/placementRequests/occupancyViewController.test.ts +++ b/server/controllers/match/placementRequests/occupancyViewController.test.ts @@ -301,7 +301,7 @@ describe('OccupancyViewController', () => { const expectedDurationDays = 10 const expectedStartDate = `${arrivalYear}-0${arrivalMonth}-${arrivalDay}` - const expectedParams = `apType=${apType}&startDate=${expectedStartDate}&durationDays=${expectedDurationDays}` + const expectedParams = `apType=${apType}&startDate=${expectedStartDate}&durationDays=${expectedDurationDays}&criteria=` expect(response.redirect).toHaveBeenCalledWith( `${matchPaths.v2Match.placementRequests.spaceBookings.new({ id: placementRequestDetail.id })}?${expectedParams}`, ) diff --git a/server/controllers/match/placementRequests/occupancyViewController.ts b/server/controllers/match/placementRequests/occupancyViewController.ts index 4f06c60065..a9179885b1 100644 --- a/server/controllers/match/placementRequests/occupancyViewController.ts +++ b/server/controllers/match/placementRequests/occupancyViewController.ts @@ -213,6 +213,7 @@ export default class { DateFormats.isoToDateObj(departureDate), DateFormats.isoToDateObj(arrivalDate), ).toString(), + criteria: body.criteria, }) res.redirect(redirectUrl) } diff --git a/server/controllers/match/placementRequests/spaceBookingsController.test.ts b/server/controllers/match/placementRequests/spaceBookingsController.test.ts index 224312d400..f8751ae40f 100644 --- a/server/controllers/match/placementRequests/spaceBookingsController.test.ts +++ b/server/controllers/match/placementRequests/spaceBookingsController.test.ts @@ -1,6 +1,8 @@ import type { NextFunction, Request, Response } from 'express' import { DeepMocked, createMock } from '@golevelup/ts-jest' +import { faker } from '@faker-js/faker' +import { Cas1SpaceBookingCharacteristic } from '@approved-premises/api' import SpaceBookingsController from './spaceBookingsController' import { PlacementRequestService, SpaceService } from '../../../services' @@ -11,9 +13,10 @@ import { placementRequestDetailFactory, spaceBookingRequirementsFactory, } from '../../../testutils/factories' -import { filterOutAPTypes, placementDates } from '../../../utils/match' +import { filterOutAPTypes, occupancyViewLink, placementDates } from '../../../utils/match' import paths from '../../../paths/admin' import { fetchErrorsAndUserInput } from '../../../utils/validation' +import { occupancyCriteriaMap } from '../../../utils/match/occupancy' jest.mock('../../../utils/validation') describe('SpaceBookingsController', () => { @@ -42,6 +45,18 @@ describe('SpaceBookingsController', () => { const premisesName = 'Hope House' const premisesId = 'abc123' const apType = 'esap' + const criteria = faker.helpers.arrayElements(Object.keys(occupancyCriteriaMap), { + min: 0, + max: 3, + }) as Array + const backLink = occupancyViewLink({ + placementRequestId: placementRequestDetail.id, + premisesId, + apType, + startDate, + durationDays, + spaceCharacteristics: criteria, + }) ;(fetchErrorsAndUserInput as jest.Mock).mockReturnValue({ errors: [], errorSummary: {}, userInput: {} }) placementRequestService.getPlacementRequest.mockResolvedValue(placementRequestDetail) @@ -51,6 +66,7 @@ describe('SpaceBookingsController', () => { premisesName, premisesId, apType, + criteria: criteria.join(','), } const params = { id: placementRequestDetail.id } @@ -72,8 +88,9 @@ describe('SpaceBookingsController', () => { errorSummary: {}, errors: [], dates: placementDates(startDate, durationDays), - essentialCharacteristics: filterOutAPTypes(placementRequestDetail.essentialCriteria), + essentialCharacteristics: criteria, desirableCharacteristics: filterOutAPTypes(placementRequestDetail.desirableCriteria), + backLink, }) expect(placementRequestService.getPlacementRequest).toHaveBeenCalledWith(token, placementRequestDetail.id) }) diff --git a/server/controllers/match/placementRequests/spaceBookingsController.ts b/server/controllers/match/placementRequests/spaceBookingsController.ts index 416e87937a..55d45ade75 100644 --- a/server/controllers/match/placementRequests/spaceBookingsController.ts +++ b/server/controllers/match/placementRequests/spaceBookingsController.ts @@ -1,7 +1,12 @@ import type { Request, RequestHandler, Response, TypedRequestHandler } from 'express' -import type { ApType, Cas1NewSpaceBooking } from '@approved-premises/api' +import type { ApType, Cas1NewSpaceBooking, PlacementCriteria } from '@approved-premises/api' import { PlacementRequestService, SpaceService } from '../../../services' -import { filterOutAPTypes, placementDates } from '../../../utils/match' +import { + filterOutAPTypes, + filterToSpaceBookingCharacteristics, + occupancyViewLink, + placementDates, +} from '../../../utils/match' import { catchValidationErrorOrPropogate, fetchErrorsAndUserInput } from '../../../utils/validation' import paths from '../../../paths/admin' import matchPaths from '../../../paths/match' @@ -9,7 +14,14 @@ import { createQueryString } from '../../../utils/utils' interface NewRequest extends Request { params: { id: string } - query: { startDate: string; durationDays: string; premisesName: string; premisesId: string; apType: ApType } + query: { + startDate: string + durationDays: string + premisesName: string + premisesId: string + apType: ApType + criteria: string + } } export default class { @@ -21,9 +33,19 @@ export default class { new(): TypedRequestHandler { return async (req: NewRequest, res: Response) => { const placementRequest = await this.placementRequestService.getPlacementRequest(req.user.token, req.params.id) - const { startDate, durationDays, premisesName, premisesId, apType } = req.query + const { startDate, durationDays, premisesName, premisesId, apType, criteria } = req.query const { errors, errorSummary } = fetchErrorsAndUserInput(req) - + const essentialCharacteristics = filterToSpaceBookingCharacteristics( + (criteria ? criteria.split(',') : []) as Array, + ) + const backLink = occupancyViewLink({ + placementRequestId: placementRequest.id, + premisesId, + apType, + startDate, + durationDays, + spaceCharacteristics: essentialCharacteristics, + }) res.render('match/placementRequests/spaceBookings/new', { pageHeading: `Book space in ${premisesName}`, placementRequest, @@ -33,10 +55,11 @@ export default class { startDate, durationDays, dates: placementDates(startDate, durationDays), - essentialCharacteristics: filterOutAPTypes(placementRequest.essentialCriteria), + essentialCharacteristics, desirableCharacteristics: filterOutAPTypes(placementRequest.desirableCriteria), errors, errorSummary, + backLink, }) } } diff --git a/server/testutils/factories/spaceSearchParameters.ts b/server/testutils/factories/spaceSearchParameters.ts index 695a3b87e3..9f12ee4ecc 100644 --- a/server/testutils/factories/spaceSearchParameters.ts +++ b/server/testutils/factories/spaceSearchParameters.ts @@ -3,7 +3,7 @@ import { faker } from '@faker-js/faker/locale/en_GB' import type { Cas1SpaceSearchParameters, Cas1SpaceSearchRequirements } from '@approved-premises/api' import { DateFormats } from '../../utils/dateUtils' -import { filterOutAPTypes } from '../../utils/match' +import { filterOutAPTypes, filterToSpaceBookingCharacteristics } from '../../utils/match' import { placementCriteria } from './placementRequest' import postcodeAreas from '../../etc/postcodeAreas.json' import { SpaceSearchParametersUi } from '../../@types/ui' @@ -35,7 +35,7 @@ export const spaceSearchParametersUiFactory = Factory.define { const apType = 'pipe' const startDate = '2025-04-14' const durationDays = '84' - const spaceCharacteristics: Array = ['isWheelchairDesignated', 'isSingle'] + const spaceCharacteristics: Array = ['isWheelchairDesignated', 'isSingle'] const result = occupancyViewLink({ placementRequestId, @@ -285,14 +287,16 @@ describe('matchUtils', () => { const apType = 'pipe' const startDate = '2025-04-14' const durationDays = '84' - const spaceCharacteristics: Array = [ + const spaceCharacteristics: Array = [ + 'isPIPE', + 'isESAP', + 'isMHAPStJosephs', + 'isMHAPElliottHouse', + 'isSemiSpecialistMentalHealth', + 'isRecoveryFocussed', 'isWheelchairDesignated', - 'isIAP', - 'hasArsonInsuranceConditions', 'isSingle', - 'acceptsHateCrimeOffenders', 'hasEnSuite', - 'isArsonDesignated', 'isArsonSuitable', ] @@ -302,7 +306,7 @@ describe('matchUtils', () => { apType, startDate, durationDays, - spaceCharacteristics, + spaceCharacteristics: filterToSpaceBookingCharacteristics(spaceCharacteristics), }) expect(result).toEqual( @@ -322,6 +326,7 @@ describe('matchUtils', () => { const apType = 'pipe' const startDate = '2022-01-01' const durationDays = '1' + const criteria: Array = ['acceptsHateCrimeOffenders', 'hasEnSuite'] const result = redirectToSpaceBookingsNew({ placementRequestId, @@ -330,6 +335,7 @@ describe('matchUtils', () => { apType, startDate, durationDays, + criteria, }) expect(result).toEqual( @@ -340,6 +346,7 @@ describe('matchUtils', () => { apType, startDate, durationDays, + criteria, }, { addQueryPrefix: true, arrayFormat: 'repeat' }, )}`, diff --git a/server/utils/match/index.ts b/server/utils/match/index.ts index dcb6ce22ce..50623ddaa7 100644 --- a/server/utils/match/index.ts +++ b/server/utils/match/index.ts @@ -2,6 +2,7 @@ import { addDays } from 'date-fns' import type { ApType, ApprovedPremisesApplication, + Cas1SpaceBookingCharacteristic, Cas1SpaceCharacteristic, Gender, PlacementCriteria, @@ -97,10 +98,9 @@ export const occupancyViewLink = ({ apType: string startDate: string durationDays: string - spaceCharacteristics: Array -}): string => { - const criteria = spaceCharacteristics.filter(name => Object.keys(occupancyCriteriaMap).includes(name)) - return `${matchPaths.v2Match.placementRequests.search.occupancy({ + spaceCharacteristics: Array +}): string => + `${matchPaths.v2Match.placementRequests.search.occupancy({ id: placementRequestId, premisesId, })}${createQueryString( @@ -108,11 +108,10 @@ export const occupancyViewLink = ({ apType, startDate, durationDays, - criteria, + criteria: spaceCharacteristics, }, { addQueryPrefix: true, arrayFormat: 'repeat' }, )}` -} export const redirectToSpaceBookingsNew = ({ placementRequestId, @@ -121,6 +120,7 @@ export const redirectToSpaceBookingsNew = ({ apType, startDate, durationDays, + criteria, }: { placementRequestId: string premisesName: string @@ -128,6 +128,7 @@ export const redirectToSpaceBookingsNew = ({ apType: string startDate: string durationDays: string + criteria: Array }): string => { return `${matchPaths.v2Match.placementRequests.spaceBookings.new({ id: placementRequestId })}${createQueryString( { @@ -136,8 +137,9 @@ export const redirectToSpaceBookingsNew = ({ apType, startDate, durationDays, + criteria, }, - { addQueryPrefix: true }, + { addQueryPrefix: true, arrayFormat: 'repeat' }, )}` } @@ -208,6 +210,15 @@ export const filterOutAPTypes = (requirements: Array): Array< ) as Array } +export const filterToSpaceBookingCharacteristics = ( + requirements: Array, +): Array => { + const characteristics = Object.keys(occupancyCriteriaMap) + return requirements.filter(requirement => + characteristics.includes(requirement), + ) as Array +} + export const requirementsHtmlString = (requirements: Array): string => { let htmlString = '' requirements.forEach(requirement => { diff --git a/server/views/match/placementRequests/spaceBookings/new.njk b/server/views/match/placementRequests/spaceBookings/new.njk index 1cf3831ca5..e46d718eaf 100644 --- a/server/views/match/placementRequests/spaceBookings/new.njk +++ b/server/views/match/placementRequests/spaceBookings/new.njk @@ -10,7 +10,7 @@ {% block beforeContent %} {{ govukBackLink({ text: "Back", - href: MatchUtils.occupancyViewLink({ placementRequestId: placementRequest.id, premisesId: premisesId, apType: apType }) + href: backLink }) }} {% endblock %} From affc768f7ec1233503823d99fca0a04507aab67a Mon Sep 17 00:00:00 2001 From: Bob Meredith Date: Thu, 9 Jan 2025 12:22:16 +0000 Subject: [PATCH 2/2] Review responses --- .../occupancyViewController.test.ts | 2 +- .../placementRequests/occupancyViewController.ts | 2 +- server/utils/match/index.test.ts | 15 +++++++-------- server/utils/match/index.ts | 3 +-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/server/controllers/match/placementRequests/occupancyViewController.test.ts b/server/controllers/match/placementRequests/occupancyViewController.test.ts index 5165e7a276..5e393a0ab2 100644 --- a/server/controllers/match/placementRequests/occupancyViewController.test.ts +++ b/server/controllers/match/placementRequests/occupancyViewController.test.ts @@ -301,7 +301,7 @@ describe('OccupancyViewController', () => { const expectedDurationDays = 10 const expectedStartDate = `${arrivalYear}-0${arrivalMonth}-${arrivalDay}` - const expectedParams = `apType=${apType}&startDate=${expectedStartDate}&durationDays=${expectedDurationDays}&criteria=` + const expectedParams = `apType=${apType}&startDate=${expectedStartDate}&durationDays=${expectedDurationDays}` expect(response.redirect).toHaveBeenCalledWith( `${matchPaths.v2Match.placementRequests.spaceBookings.new({ id: placementRequestDetail.id })}?${expectedParams}`, ) diff --git a/server/controllers/match/placementRequests/occupancyViewController.ts b/server/controllers/match/placementRequests/occupancyViewController.ts index a9179885b1..6e894d50d6 100644 --- a/server/controllers/match/placementRequests/occupancyViewController.ts +++ b/server/controllers/match/placementRequests/occupancyViewController.ts @@ -213,7 +213,7 @@ export default class { DateFormats.isoToDateObj(departureDate), DateFormats.isoToDateObj(arrivalDate), ).toString(), - criteria: body.criteria, + criteria: body.criteria ? body.criteria : undefined, }) res.redirect(redirectUrl) } diff --git a/server/utils/match/index.test.ts b/server/utils/match/index.test.ts index c91b522ce4..287cabe1d2 100644 --- a/server/utils/match/index.test.ts +++ b/server/utils/match/index.test.ts @@ -2,7 +2,6 @@ import type { ApType, ApprovedPremisesApplication, Cas1SpaceBookingCharacteristic, - Cas1SpaceCharacteristic, FullPerson, PlacementCriteria, } from '@approved-premises/api' @@ -326,7 +325,7 @@ describe('matchUtils', () => { const apType = 'pipe' const startDate = '2022-01-01' const durationDays = '1' - const criteria: Array = ['acceptsHateCrimeOffenders', 'hasEnSuite'] + const criteria: Array = ['hasEnSuite', 'isArsonSuitable'] const result = redirectToSpaceBookingsNew({ placementRequestId, @@ -341,12 +340,12 @@ describe('matchUtils', () => { expect(result).toEqual( `${paths.v2Match.placementRequests.spaceBookings.new({ id: placementRequestId })}${createQueryString( { - premisesName, - premisesId, - apType, - startDate, - durationDays, - criteria, + premisesName: 'Hope House', + premisesId: 'abc', + apType: 'pipe', + startDate: '2022-01-01', + durationDays: '1', + criteria: ['hasEnSuite', 'isArsonSuitable'], }, { addQueryPrefix: true, arrayFormat: 'repeat' }, )}`, diff --git a/server/utils/match/index.ts b/server/utils/match/index.ts index 50623ddaa7..041cb385e2 100644 --- a/server/utils/match/index.ts +++ b/server/utils/match/index.ts @@ -3,7 +3,6 @@ import type { ApType, ApprovedPremisesApplication, Cas1SpaceBookingCharacteristic, - Cas1SpaceCharacteristic, Gender, PlacementCriteria, PlacementRequest, @@ -128,7 +127,7 @@ export const redirectToSpaceBookingsNew = ({ apType: string startDate: string durationDays: string - criteria: Array + criteria: Array }): string => { return `${matchPaths.v2Match.placementRequests.spaceBookings.new({ id: placementRequestId })}${createQueryString( {