Skip to content

Commit

Permalink
Update booking confirmation screen
Browse files Browse the repository at this point in the history
This does a few things:
- Ensures the booking confirmation flow uses the arrival date and
  departure date submitted, rather than relying on filter start date and
  duration
- Removes the `apType` parameter as it is not used
- Updates the booking confirmation template to show relevent information
- Refactors a lot of the flow to present occupancy view with retained
  filters where relevant
  • Loading branch information
froddd committed Jan 9, 2025
1 parent 669c2fe commit b6bf14f
Show file tree
Hide file tree
Showing 18 changed files with 388 additions and 311 deletions.
5 changes: 2 additions & 3 deletions e2e/pages/match/bookingPage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Page, expect } from '@playwright/test'
import { E2EDatesOfPlacement } from 'e2e/steps/assess'
import { BasePage } from '../basePage'
import { Premises } from '../../../server/@types/shared'
import { DateFormats } from '../../../server/utils/dateUtils'

export class BookingPage extends BasePage {
static async initialize(page: Page, premisesName: Premises['name']) {
await expect(page.locator('h1')).toContainText(`Book space in ${premisesName}`)
static async initialize(page: Page) {
await expect(page.locator('h1')).toContainText('Confirm booking')

return new BookingPage(page)
}
Expand Down
4 changes: 2 additions & 2 deletions e2e/steps/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const matchAndBookApplication = async ({
await occupancyViewPage.clickContinue()

// Then I should see the booking screen for that AP
const bookingPage = await BookingPage.initialize(page, premisesName)
const bookingPage = await BookingPage.initialize(page)

// Should show the booking details (inc. new dates)
const newDatesOfPlacement: E2EDatesOfPlacement = {
Expand All @@ -109,7 +109,7 @@ export const matchAndBookApplication = async ({
await bookingPage.shouldShowDatesOfPlacement(newDatesOfPlacement)

// And I confirm the booking
const premisesId = page.url().match(/premisesId=(.[^&]*)/)[1] // premisesId=338e22f3-70be-4519-97ab-f08c6c2dfb0b
const premisesId = page.url().match(/space-bookings\/(.[^/]*)/)[1] // Path: /match/placement-requests/:id/space-bookings/:premisesId/new
await bookingPage.clickConfirm()

// Then I should see the Matched tab on the CRU dashboard
Expand Down
9 changes: 0 additions & 9 deletions integration_tests/mockApis/spaceBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type {
Cas1SpaceBooking,
Cas1SpaceBookingResidency,
Cas1SpaceBookingSummary,
PlacementRequest,
TimelineEvent,
} from '@approved-premises/api'

Expand All @@ -26,14 +25,6 @@ export default {
},
}),

verifySpaceBookingCreate: async (placementRequest: PlacementRequest) =>
(
await getMatchingRequests({
method: 'POST',
url: paths.placementRequests.spaceBookings.create({ id: placementRequest.id }),
})
).body.requests,

stubSpaceBookingShow: (placement: Cas1SpaceBooking) =>
stubFor({
request: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ export default class ListPage extends Page {
return new ListPage()
}

shouldShowSpaceBookingConfirmation(premisesName: string, personName: string) {
this.shouldShowBanner(`Space booked for ${personName} in ${premisesName}`)
shouldShowSpaceBookingConfirmation() {
this.shouldShowBanner(
'You have now booked a place in this AP for this person. An email will be sent to the AP, to inform them of the booking.',
)
}

shouldShowPlacementRequests(placementRequests: Array<PlacementRequest>, status?: PlacementRequestStatus): void {
Expand Down
74 changes: 40 additions & 34 deletions integration_tests/pages/match/bookASpacePage.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,58 @@
import { ApType, PlacementDates, PlacementRequestDetail, Premises } from '@approved-premises/api'
import {
Cas1PremisesSummary,
Cas1SpaceBookingCharacteristic,
PlacementRequestDetail,
Premises,
} from '@approved-premises/api'
import { differenceInDays } from 'date-fns'
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 { placementCriteriaLabels } from '../../../server/utils/placementCriteriaUtils'
import { apTypeLabels } from '../../../server/utils/apTypeLabels'
import { createQueryString } from '../../../server/utils/utils'
import { DateFormats, daysToWeeksAndDays } from '../../../server/utils/dateUtils'
import { requirementsHtmlString } from '../../../server/utils/match'
import { allReleaseTypes } from '../../../server/utils/applications/releaseTypeUtils'

export default class BookASpacePage extends Page {
constructor(premisesName: string) {
super(`Book space in ${premisesName}`)
constructor() {
super(`Confirm booking`)
}

static visit(
placementRequest: PlacementRequestDetail,
startDate: string,
durationDays: PlacementDates['duration'],
premisesName: Premises['name'],
premisesId: Premises['id'],
apType: ApType,
arrivalDate: string,
departureDate: string,
criteria: Array<Cas1SpaceBookingCharacteristic>,
) {
const queryString = createQueryString({ startDate, durationDays, premisesName, premisesId, apType })
const path = `${paths.v2Match.placementRequests.spaceBookings.new({ id: placementRequest.id })}?${queryString}`
const queryString = createQueryString({ arrivalDate, departureDate, criteria })
const path = `${paths.v2Match.placementRequests.spaceBookings.new({
id: placementRequest.id,
premisesId,
})}?${queryString}`

cy.visit(path)
return new BookASpacePage(premisesName)

return new BookASpacePage()
}

shouldShowBookingDetails(
placementRequest: PlacementRequestDetail,
startDate: string,
duration: PlacementDates['duration'],
apType: ApType,
premises: Cas1PremisesSummary,
arrivalDate: string,
departureDate: string,
criteria: Array<Cas1SpaceBookingCharacteristic>,
): void {
const { endDate, placementLength } = placementDates(startDate, duration.toString())
cy.get('dd').contains(apTypeLabels[apType])
cy.get('dd').contains(DateFormats.isoDateToUIDate(startDate))
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 => {
cy.get('li').contains(placementCriteriaLabels[requirement])
})
this.shouldContainSummaryListItems([
{ key: { text: 'Approved Premises' }, value: { text: premises.name } },
// { key: { text: 'Address' }, value: { text: premises.fullAddress } },
{ key: { text: 'Space type' }, value: { html: requirementsHtmlString(criteria) } },
{ key: { text: 'Arrival date' }, value: { text: DateFormats.isoDateToUIDate(arrivalDate) } },
{ key: { text: 'Departure date' }, value: { text: DateFormats.isoDateToUIDate(departureDate) } },
{
key: { text: 'Length of stay' },
value: { text: DateFormats.formatDuration(daysToWeeksAndDays(differenceInDays(departureDate, arrivalDate))) },
},
{ key: { text: 'Release type' }, value: { text: allReleaseTypes[placementRequest.releaseType] } },
])
}
}
84 changes: 36 additions & 48 deletions integration_tests/tests/match/match.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Cas1SpaceSearchParameters, PlacementCriteria } from '@approved-premises/api'
import { Cas1SpaceBookingCharacteristic, Cas1SpaceSearchParameters, FullPerson } from '@approved-premises/api'
import SearchPage from '../../pages/match/searchPage'
import UnableToMatchPage from '../../pages/match/unableToMatchPage'

Expand All @@ -16,10 +16,11 @@ 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 } from '../../../server/utils/match'
import BookASpacePage from '../../pages/match/bookASpacePage'
import OccupancyViewPage from '../../pages/match/occupancyViewPage'
import applicationFactory from '../../../server/testutils/factories/application'
import apiPaths from '../../../server/paths/api'

context('Placement Requests', () => {
beforeEach(() => {
Expand Down Expand Up @@ -143,20 +144,6 @@ context('Placement Requests', () => {
occupancyViewPage.shouldShowErrorSummaryAndErrorMessage('The departure date must be after the arrival date')
})

it('allows me to submit valid dates in the book your placement form on occupancy view page and redirects to book a space', () => {
const { occupancyViewPage, placementRequest, premises } =
shouldVisitOccupancyViewPageAndShowMatchingDetails(defaultLicenceExpiryDate)

// When I submit valid dates
const arrivalDate = '2024-11-25'
occupancyViewPage.shouldFillBookYourPlacementFormDates(arrivalDate, '2024-11-26')
occupancyViewPage.clickContinue()

// Then I should land on the Book a space page (with the new dates overriding the original ones)
const bookASpacePage = Page.verifyOnPage(BookASpacePage, premises.name)
bookASpacePage.shouldShowBookingDetails(placementRequest, arrivalDate, 1, 'normal')
})

const shouldVisitOccupancyViewPageAndShowMatchingDetails = (licenceExpiryDate: string | undefined) => {
const apType = 'normal'
const durationDays = 15
Expand Down Expand Up @@ -287,35 +274,38 @@ context('Placement Requests', () => {
// Given I am signed in as a cru_member
signIn(['cru_member'], ['cas1_space_booking_create'])

const premisesName = 'Hope House'
const premisesId = 'abc123'
const apType = 'normal'
const durationDays = 15
const startDate = '2024-07-23'
const { endDate } = placementDates(startDate, durationDays.toString())
const premises = cas1PremisesSummaryFactory.build()
cy.task('stubSinglePremises', premises)

const arrivalDate = '2024-07-23'
const departureDate = '2024-08-08'
const criteria: Array<Cas1SpaceBookingCharacteristic> = ['isWheelchairDesignated', 'hasEnSuite']

// And there is a placement request waiting for me to match
const person = personFactory.build()
const essentialCharacteristics: Array<PlacementCriteria> = ['acceptsHateCrimeOffenders']
const desirableCharacteristics: Array<PlacementCriteria> = ['isCatered', 'hasEnSuite']
const placementRequest = placementRequestDetailFactory.build({
person,
status: 'notMatched',
duration: durationDays,
essentialCriteria: essentialCharacteristics,
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, premises.id, arrivalDate, departureDate, criteria)

// Then I should see the details of the case I am matching
page.shouldShowPersonHeader(placementRequest.person as FullPerson)

// Then I should see the details of the space I am booking
page.shouldShowBookingDetails(placementRequest, startDate, durationDays, apType)
// And I should see the details of the space I am booking
page.shouldShowBookingDetails(placementRequest, premises, arrivalDate, departureDate, criteria)

// And when I complete the form
const requirements = spaceBookingRequirementsFactory.build()
const spaceBooking = cas1SpaceBookingFactory.build({ requirements })
const requirements = spaceBookingRequirementsFactory.build({
essentialCharacteristics: criteria,
})
const spaceBooking = cas1SpaceBookingFactory.upcoming().build({
expectedArrivalDate: arrivalDate,
expectedDepartureDate: departureDate,
requirements,
})
cy.task('stubSpaceBookingCreate', { placementRequestId: placementRequest.id, spaceBooking })
cy.task('stubPlacementRequestsDashboard', { placementRequests: [placementRequest], status: 'matched' })
page.clickSubmit()
Expand All @@ -324,23 +314,21 @@ context('Placement Requests', () => {
const cruDashboard = Page.verifyOnPage(ListPage)

// And I should see a success message
cruDashboard.shouldShowSpaceBookingConfirmation(premisesName, person.name)
cruDashboard.shouldShowSpaceBookingConfirmation()

// And the booking details should have been sent to the API
cy.task('verifySpaceBookingCreate', placementRequest).then(requests => {
expect(requests).to.have.length(1)
const body = JSON.parse(requests[0].body)

expect(body).to.deep.equal({
arrivalDate: startDate,
departureDate: endDate,
premisesId,
requirements: {
...spaceBooking.requirements,
essentialCharacteristics: placementRequest.essentialCriteria,
},
})
})
cy.task('verifyApiPost', apiPaths.placementRequests.spaceBookings.create({ id: placementRequest.id })).then(
body => {
expect(body).to.deep.equal({
arrivalDate,
departureDate,
premisesId: premises.id,
requirements: {
essentialCharacteristics: criteria,
},
})
},
)
})

it('allows me to mark a placement request as unable to match', () => {
Expand Down
2 changes: 1 addition & 1 deletion server/controllers/match/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const controllers = (services: Services) => {
)
const spaceSearchController = new SpaceSearchController(spaceService, placementRequestService)
const placementRequestBookingsController = new BookingsController(placementRequestService)
const spaceBookingsController = new SpaceBookingsController(placementRequestService, spaceService)
const spaceBookingsController = new SpaceBookingsController(placementRequestService, premisesService, spaceService)
const occupancyViewController = new OccupancyViewController(placementRequestService, premisesService)

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('OccupancyViewController', () => {
occupancyViewController = new OccupancyViewController(placementRequestService, premisesService)
request = createMock<Request>({
user: { token },
query: {},
flash: flashSpy,
})

Expand Down Expand Up @@ -258,18 +259,12 @@ describe('OccupancyViewController', () => {
describe('bookSpace', () => {
const startDate = '2025-08-15'
const durationDays = '22'
const arrivalDay = '11'
const arrivalMonth = '2'
const arrivalYear = '2026'

const validBookingBody = {
apType,
startDate,
durationDays,
criteria: '',
'arrivalDate-day': arrivalDay,
'arrivalDate-month': arrivalMonth,
'arrivalDate-year': arrivalYear,
criteria: 'hasEnSuite,isStepFreeDesignated',
'arrivalDate-day': '11',
'arrivalDate-month': '2',
'arrivalDate-year': '2026',
'departureDate-day': '21',
'departureDate-month': '2',
'departureDate-year': '2026',
Expand All @@ -281,41 +276,44 @@ describe('OccupancyViewController', () => {
const requestHandler = occupancyViewController.bookSpace()
await requestHandler({ ...request, params, body: validBookingBody }, response, next)

const expectedDurationDays = 10
const expectedStartDate = `${arrivalYear}-0${arrivalMonth}-${arrivalDay}`
const expectedParams = `apType=${apType}&startDate=${expectedStartDate}&durationDays=${expectedDurationDays}`
const expectedQueryString =
'arrivalDate=2026-02-11&departureDate=2026-02-21&criteria=hasEnSuite&criteria=isStepFreeDesignated'

expect(response.redirect).toHaveBeenCalledWith(
`${matchPaths.v2Match.placementRequests.spaceBookings.new({ id: placementRequestDetail.id })}?${expectedParams}`,
`${matchPaths.v2Match.placementRequests.spaceBookings.new(params)}?${expectedQueryString}`,
)
})

it(`should redirect to occupancy view and add errors messages when date validation fails`, async () => {
it(`should redirect to occupancy view with existing query string and add errors messages when date validation fails`, async () => {
jest.spyOn(validationUtils, 'addErrorMessageToFlash')
const emptyDay = ''
const body = {
...validBookingBody,
'arrivalDate-day': emptyDay,
'arrivalDate-day': '',
criteria: 'isWheelchairDesignated,isSuitedForSexOffenders',
}
const query = {
startDate,
durationDays,
}
const params = { id: placementRequestDetail.id, premisesId: premises.id }

const requestHandler = occupancyViewController.bookSpace()

await requestHandler({ ...request, params, body }, response, next)
await requestHandler({ ...request, params, query, body }, response, next)

expect(validationUtils.addErrorMessageToFlash).toHaveBeenCalledWith(
request,
'You must enter an arrival date',
'arrivalDate',
)

const expectedParams = `apType=${apType}&startDate=${startDate}&durationDays=${durationDays}&criteria=isWheelchairDesignated&criteria=isSuitedForSexOffenders`
const expectedQueryString = `startDate=${startDate}&durationDays=${durationDays}&criteria=isWheelchairDesignated&criteria=isSuitedForSexOffenders`

expect(response.redirect).toHaveBeenCalledWith(
`${matchPaths.v2Match.placementRequests.search.occupancy({
id: placementRequestDetail.id,
premisesId: premises.id,
})}?${expectedParams}`,
})}?${expectedQueryString}`,
)
})
})
Expand Down
Loading

0 comments on commit b6bf14f

Please sign in to comment.