diff --git a/integration_tests/e2e/bookingJourneyDropOuts.cy.ts b/integration_tests/e2e/bookingJourneyDropOuts.cy.ts index 99c3ded..da2d1ed 100644 --- a/integration_tests/e2e/bookingJourneyDropOuts.cy.ts +++ b/integration_tests/e2e/bookingJourneyDropOuts.cy.ts @@ -13,14 +13,19 @@ context('Booking journey - drop-out points', () => { const today = new Date() const prison = TestData.prisonDto({ policyNoticeDaysMax: 36 }) // > 31 so always 2 months shown const prisoner = TestData.bookerPrisonerInfoDto() - const visitors = [ - TestData.visitorInfoDto({ - visitorId: 1000, - firstName: 'Adult', - lastName: 'One', - dateOfBirth: format(subYears(today, 25), DateFormats.ISO_DATE), // 25-year-old - }), - ] + + const adultVisitor = TestData.visitorInfoDto({ + visitorId: 1000, + firstName: 'Adult', + lastName: 'One', + dateOfBirth: format(subYears(today, 25), DateFormats.ISO_DATE), // 25-year-old + }) + const childVisitor = TestData.visitorInfoDto({ + visitorId: 1000, + firstName: 'Child', + lastName: 'One', + dateOfBirth: format(subYears(today, 5), DateFormats.ISO_DATE), // 5-year-old + }) const tomorrow = format(addDays(today, 1), DateFormats.ISO_DATE) const in10Days = format(addDays(today, 10), DateFormats.ISO_DATE) @@ -50,7 +55,7 @@ context('Booking journey - drop-out points', () => { // Start booking journey cy.task('stubGetPrison', prison) - cy.task('stubGetVisitors', { visitors }) + cy.task('stubGetVisitors', { visitors: [adultVisitor] }) cy.task('stubValidatePrisonerPass') homePage.startBooking() @@ -87,7 +92,7 @@ context('Booking journey - drop-out points', () => { // Start booking journey cy.task('stubGetPrison', prison) - cy.task('stubGetVisitors', { visitors }) + cy.task('stubGetVisitors', { visitors: [adultVisitor] }) cy.task('stubValidatePrisonerPass') homePage.startBooking() @@ -138,7 +143,7 @@ context('Booking journey - drop-out points', () => { // Start booking journey cy.task('stubGetPrison', prison) - cy.task('stubGetVisitors', { visitors }) + cy.task('stubGetVisitors', { visitors: [adultVisitor] }) cy.task('stubValidatePrisonerPass') homePage.startBooking() @@ -165,7 +170,7 @@ context('Booking journey - drop-out points', () => { // Start booking journey cy.task('stubGetPrison', prison) - cy.task('stubGetVisitors', { visitors }) + cy.task('stubGetVisitors', { visitors: [adultVisitor] }) cy.task('stubValidatePrisonerFail') homePage.startBooking() @@ -179,5 +184,28 @@ context('Booking journey - drop-out points', () => { cannotBookPage.backLink().click() Page.verifyOnPage(HomePage) }) + + it('should show drop-out page when no eligible visitors over 18', () => { + cy.task('stubGetBookerReference') + cy.task('stubGetPrisoners', { prisoners: [prisoner] }) + cy.signIn() + + // Home page - prisoner shown + const homePage = Page.verifyOnPage(HomePage) + + // Start booking journey + cy.task('stubGetPrison', prison) + cy.task('stubGetVisitors', { visitors: [childVisitor] }) + cy.task('stubValidatePrisonerPass') + homePage.startBooking() + + // Visit cannot be booked page + const cannotBookPage = Page.verifyOnPage(CannotBookPage) + cy.contains('One person on a visit must be 18 years old or older') + + // Back link back to Home page + cannotBookPage.backLink().click() + Page.verifyOnPage(HomePage) + }) }) }) diff --git a/server/@types/bapv.d.ts b/server/@types/bapv.d.ts index ba62614..86397d3 100644 --- a/server/@types/bapv.d.ts +++ b/server/@types/bapv.d.ts @@ -60,4 +60,8 @@ export type BookingCancelled = { hasMobile: boolean } -export type CannotBookReason = 'NO_VO_BALANCE' | 'TRANSFER_OR_RELEASE' | 'UNSUPPORTED_PRISON' +export type CannotBookReason = + | 'NO_VO_BALANCE' + | 'TRANSFER_OR_RELEASE' + | 'UNSUPPORTED_PRISON' + | 'NO_ELIGIBLE_ADULT_VISITOR' diff --git a/server/routes/bookVisit/cannotBookController.test.ts b/server/routes/bookVisit/cannotBookController.test.ts index bb89144..2488b92 100644 --- a/server/routes/bookVisit/cannotBookController.test.ts +++ b/server/routes/bookVisit/cannotBookController.test.ts @@ -109,5 +109,23 @@ describe('A visit cannot be booked', () => { expect(sessionData.bookingJourney).toBe(undefined) }) }) + + it('should render cannot book page and clear bookingJourney data - NO_ELIGIBLE_ADULT_VISITOR', () => { + sessionData.bookingJourney.cannotBookReason = 'NO_ELIGIBLE_ADULT_VISITOR' + + return request(app) + .get(paths.BOOK_VISIT.CANNOT_BOOK) + .expect('Content-Type', /html/) + .expect(res => { + const $ = cheerio.load(res.text) + expect($('title').text()).toMatch(/^A visit cannot be booked -/) + expect($('[data-test="back-link"]').attr('href')).toBe(paths.HOME) + expect($('h1').text()).toBe('A visit cannot be booked') + + expect($('main p').eq(0).text()).toContain('One person on a visit must be 18 years old or older') + + expect(sessionData.bookingJourney).toBe(undefined) + }) + }) }) }) diff --git a/server/routes/bookVisit/selectVisitorsController.test.ts b/server/routes/bookVisit/selectVisitorsController.test.ts index fb0ca60..c337cbd 100644 --- a/server/routes/bookVisit/selectVisitorsController.test.ts +++ b/server/routes/bookVisit/selectVisitorsController.test.ts @@ -49,6 +49,7 @@ const visitor3 = TestData.visitor({ firstName: 'Visitor', lastName: 'Age 17y', dateOfBirth: '2006-05-03', // 18 tomorrow + adult: false, }) const visitor4 = TestData.visitor({ visitorDisplayId: randomUUID(), @@ -56,6 +57,7 @@ const visitor4 = TestData.visitor({ firstName: 'Visitor', lastName: 'Age 16y', dateOfBirth: '2008-05-02', // 16 today + adult: false, }) const visitor5 = TestData.visitor({ visitorDisplayId: randomUUID(), @@ -63,6 +65,7 @@ const visitor5 = TestData.visitor({ firstName: 'Visitor', lastName: 'Age 15y', dateOfBirth: '2008-05-03', // 16 tomorrow + adult: false, }) const visitor6 = TestData.visitor({ visitorDisplayId: randomUUID(), @@ -70,6 +73,7 @@ const visitor6 = TestData.visitor({ firstName: 'Visitor', lastName: 'Age 10y', dateOfBirth: '2014-05-02', + adult: false, }) const visitor7 = TestData.visitor({ visitorDisplayId: randomUUID(), @@ -77,6 +81,7 @@ const visitor7 = TestData.visitor({ firstName: 'Visitor', lastName: 'Age 1y', dateOfBirth: '2023-05-02', + adult: false, }) const visitor8 = TestData.visitor({ visitorDisplayId: randomUUID(), @@ -84,6 +89,7 @@ const visitor8 = TestData.visitor({ firstName: 'Visitor', lastName: 'Age 4m', dateOfBirth: '2024-01-02', + adult: false, }) const visitors = [visitor1, visitor2, visitor3, visitor4, visitor5, visitor6, visitor7, visitor8] @@ -108,7 +114,6 @@ describe('Select visitors', () => { flashProvider.mockImplementation((key: keyof FlashData) => flashData[key]) sessionData = { - booker: { reference: bookerReference, prisoners: [prisoner] }, bookingJourney: { prisoner }, } as SessionData @@ -178,17 +183,11 @@ describe('Select visitors', () => { expect(bookerService.getEligibleVisitors).toHaveBeenCalledWith(bookerReference, prisoner.prisonerNumber) expect(prisonService.getPrison).toHaveBeenCalledWith(prisoner.prisonId) - expect(sessionData).toStrictEqual({ - booker: { - reference: bookerReference, - prisoners: [prisoner], - }, - bookingJourney: { - prisoner, - prison, - eligibleVisitors: visitors, - }, - } as SessionData) + expect(sessionData.bookingJourney).toStrictEqual({ + prisoner, + prison, + eligibleVisitors: visitors, + } as SessionData['bookingJourney']) }) }) @@ -271,17 +270,28 @@ describe('Select visitors', () => { expect(bookerService.getEligibleVisitors).toHaveBeenCalledWith(bookerReference, prisoner.prisonerNumber) expect(prisonService.getPrison).toHaveBeenCalledWith(prisoner.prisonId) - expect(sessionData).toStrictEqual({ - booker: { - reference: bookerReference, - prisoners: [prisoner], - }, - bookingJourney: { - prisoner, - prison, - eligibleVisitors: [], - }, - } as SessionData) + expect(sessionData.bookingJourney).toStrictEqual({ + prisoner, + prison, + eligibleVisitors: [], + } as SessionData['bookingJourney']) + }) + }) + + it('should handle booker having no eligible adult visitors for this prisoner and redirect to cannot book page with reason', () => { + bookerService.getEligibleVisitors.mockResolvedValue([visitor6]) // only a child visitor + + return request(app) + .get(paths.BOOK_VISIT.SELECT_VISITORS) + .expect(302) + .expect('location', paths.BOOK_VISIT.CANNOT_BOOK) + .expect(() => { + expect(sessionData.bookingJourney).toStrictEqual({ + prisoner, + prison, + eligibleVisitors: [visitor6], + cannotBookReason: 'NO_ELIGIBLE_ADULT_VISITOR', + } as SessionData['bookingJourney']) }) }) }) @@ -291,7 +301,6 @@ describe('Select visitors', () => { visitSessionsService.getSessionRestriction.mockResolvedValue('OPEN') sessionData = { - booker: { reference: bookerReference, prisoners: [prisoner] }, bookingJourney: { prisoner, prison, eligibleVisitors: visitors }, } as SessionData @@ -306,19 +315,13 @@ describe('Select visitors', () => { .expect('Location', paths.BOOK_VISIT.CHOOSE_TIME) .expect(() => { expect(flashProvider).not.toHaveBeenCalled() - expect(sessionData).toStrictEqual({ - booker: { - reference: bookerReference, - prisoners: [prisoner], - }, - bookingJourney: { - prisoner, - prison, - eligibleVisitors: visitors, - selectedVisitors: [visitor1, visitor3], - sessionRestriction: 'OPEN', - }, - } as SessionData) + expect(sessionData.bookingJourney).toStrictEqual({ + prisoner, + prison, + eligibleVisitors: visitors, + selectedVisitors: [visitor1, visitor3], + sessionRestriction: 'OPEN', + } as SessionData['bookingJourney']) expect(visitSessionsService.getSessionRestriction).toHaveBeenCalledWith({ prisonerId: prisoner.prisonerNumber, visitorIds: [visitor1.visitorId, visitor3.visitorId], @@ -336,19 +339,13 @@ describe('Select visitors', () => { .expect('Location', paths.BOOK_VISIT.CLOSED_VISIT) .expect(() => { expect(flashProvider).not.toHaveBeenCalled() - expect(sessionData).toStrictEqual({ - booker: { - reference: bookerReference, - prisoners: [prisoner], - }, - bookingJourney: { - prisoner, - prison, - eligibleVisitors: visitors, - selectedVisitors: [visitor1, visitor3], - sessionRestriction: 'CLOSED', - }, - } as SessionData) + expect(sessionData.bookingJourney).toStrictEqual({ + prisoner, + prison, + eligibleVisitors: visitors, + selectedVisitors: [visitor1, visitor3], + sessionRestriction: 'CLOSED', + } as SessionData['bookingJourney']) expect(visitSessionsService.getSessionRestriction).toHaveBeenCalledWith({ prisonerId: prisoner.prisonerNumber, visitorIds: [visitor1.visitorId, visitor3.visitorId], @@ -371,19 +368,13 @@ describe('Select visitors', () => { .expect('Location', paths.BOOK_VISIT.CHOOSE_TIME) .expect(() => { expect(flashProvider).not.toHaveBeenCalled() - expect(sessionData).toStrictEqual({ - booker: { - reference: bookerReference, - prisoners: [prisoner], - }, - bookingJourney: { - prisoner, - prison, - eligibleVisitors: visitors, - selectedVisitors: [visitors[0], visitors[2]], // duplicate '1' & unrecognised UUID filtered out - sessionRestriction: 'OPEN', - }, - } as SessionData) + expect(sessionData.bookingJourney).toStrictEqual({ + prisoner, + prison, + eligibleVisitors: visitors, + selectedVisitors: [visitors[0], visitors[2]], // duplicate '1' & unrecognised UUID filtered out + sessionRestriction: 'OPEN', + } as SessionData['bookingJourney']) expect(visitSessionsService.getSessionRestriction).toHaveBeenCalledWith({ prisonerId: prisoner.prisonerNumber, visitorIds: [visitor1.visitorId, visitor3.visitorId], diff --git a/server/routes/bookVisit/selectVisitorsController.ts b/server/routes/bookVisit/selectVisitorsController.ts index e23d37a..0a20833 100644 --- a/server/routes/bookVisit/selectVisitorsController.ts +++ b/server/routes/bookVisit/selectVisitorsController.ts @@ -23,6 +23,12 @@ export default class SelectVisitorsController { ]) } + const isAtLeastOneAdultVisitor = bookingJourney.eligibleVisitors.some(visitor => visitor.adult) + if (bookingJourney.eligibleVisitors.length && !isAtLeastOneAdultVisitor) { + req.session.bookingJourney.cannotBookReason = 'NO_ELIGIBLE_ADULT_VISITOR' + return res.redirect(paths.BOOK_VISIT.CANNOT_BOOK) + } + const selectedVisitorDisplayIds = { visitorDisplayIds: bookingJourney.selectedVisitors?.map(visitor => visitor.visitorDisplayId) ?? [], } @@ -31,7 +37,7 @@ export default class SelectVisitorsController { ...req.flash('formValues')?.[0], } - res.render('pages/bookVisit/selectVisitors', { + return res.render('pages/bookVisit/selectVisitors', { errors: req.flash('errors'), formValues, prison: bookingJourney.prison, diff --git a/server/views/pages/bookVisit/cannotBook.njk b/server/views/pages/bookVisit/cannotBook.njk index 394a9ed..1346abb 100644 --- a/server/views/pages/bookVisit/cannotBook.njk +++ b/server/views/pages/bookVisit/cannotBook.njk @@ -52,6 +52,22 @@ how to book a visit at this prison.
{% endif %} + + {% if cannotBookReason == 'NO_ELIGIBLE_ADULT_VISITOR' %} ++ One person on a visit must be 18 years old or older. None of your adult visitors can be selected. + This may be because of a new restriction or a ban. +
+ +The person you want to book for must be on the prisoner’s visitor list.
+ ++ To make a request to book for someone new, + complete the form (opens in a new tab). + We will respond within 5 working days. +
+ {% endif %} {% endblock %}