Skip to content

Commit

Permalink
VB-4469, Booker cancel visit journey (#195)
Browse files Browse the repository at this point in the history
Co-authored-by: Thomas McGowan <tpmcgowan@users.noreply.github.com>
  • Loading branch information
hutcheonb-moj and tpmcgowan authored Oct 23, 2024
1 parent b0b10ba commit 27da41f
Show file tree
Hide file tree
Showing 21 changed files with 521 additions and 10 deletions.
2 changes: 1 addition & 1 deletion integration_tests/pages/bookings/cancelledVisitsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export default class CancelledVisitsPage extends Page {

visitEndTime = (index: number): PageElement => cy.get(`[data-test=visit-end-time-${index}]`)

visitLink = (index: number): PageElement => cy.get(`[data-test=visit-link-${index}]`)
visitLink = (index: number): PageElement => cy.get(`[data-test=visit-link-booking-${index}]`)
}
2 changes: 1 addition & 1 deletion integration_tests/pages/bookings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default class BookingsPage extends Page {

visitReference = (index: number): PageElement => cy.get(`[data-test=visit-reference-${index}]`)

visitLink = (index: number): PageElement => cy.get(`[data-test=visit-link-${index}]`)
visitLink = (index: number): PageElement => cy.get(`[data-test=visit-link-booking-${index}]`)

pastVisitsLink = (): PageElement => cy.get('[data-test="past-visits-link"]')

Expand Down
2 changes: 1 addition & 1 deletion integration_tests/pages/bookings/pastVisitsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export default class PastVisitsPage extends Page {

visitEndTime = (index: number): PageElement => cy.get(`[data-test=visit-end-time-${index}]`)

visitLink = (index: number): PageElement => cy.get(`[data-test=visit-link-${index}]`)
visitLink = (index: number): PageElement => cy.get(`[data-test=visit-link-booking-${index}]`)
}
7 changes: 6 additions & 1 deletion server/@types/orchestration-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,8 @@ export interface components {
| 'NOT_KNOWN'
| 'NOT_APPLICABLE'
| 'BY_PRISONER'
/** @description Username for user who actioned this request */
actionedBy: string
}
/**
* @description Contact Phone Number
Expand Down Expand Up @@ -800,6 +802,7 @@ export interface components {
| 'VISIT_ORDER_CANCELLED'
| 'SUPERSEDED_CANCELLATION'
| 'DETAILS_CHANGED_AFTER_BOOKING'
| 'BOOKER_CANCELLED'
/**
* @description Outcome text
* @example Because he got covid
Expand Down Expand Up @@ -902,6 +905,7 @@ export interface components {
| 'VISIT_ORDER_CANCELLED'
| 'SUPERSEDED_CANCELLATION'
| 'DETAILS_CHANGED_AFTER_BOOKING'
| 'BOOKER_CANCELLED'
/**
* @description Visit Restriction
* @example OPEN
Expand Down Expand Up @@ -1325,9 +1329,9 @@ export interface components {
sort?: components['schemas']['SortObject'][]
/** Format: int32 */
pageSize?: number
paged?: boolean
/** Format: int32 */
pageNumber?: number
paged?: boolean
unpaged?: boolean
}
SortObject: {
Expand Down Expand Up @@ -1616,6 +1620,7 @@ export interface components {
| 'VISIT_ORDER_CANCELLED'
| 'SUPERSEDED_CANCELLATION'
| 'DETAILS_CHANGED_AFTER_BOOKING'
| 'BOOKER_CANCELLED'
/**
* Format: date-time
* @description The date and time of the visit
Expand Down
4 changes: 4 additions & 0 deletions server/constants/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const paths = {
VISIT: '/bookings/details',
VISIT_PAST: '/bookings/past/details',
VISIT_CANCELLED: '/bookings/cancelled/details',

// Cancel journey
CANCEL_VISIT: '/bookings/cancel-booking',
CANCEL_CONFIRMATION: '/bookings/booking-cancelled',
},

VISITORS: '/visitors',
Expand Down
25 changes: 25 additions & 0 deletions server/data/orchestrationApiClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AvailableVisitSessionDto,
AvailableVisitSessionRestrictionDto,
BookingOrchestrationRequestDto,
CancelVisitOrchestrationDto,
ChangeApplicationDto,
CreateApplicationDto,
VisitDto,
Expand Down Expand Up @@ -53,6 +54,30 @@ describe('orchestrationApiClient', () => {
})
})

describe('cancelVisit', () => {
it('should cancel a visit for the booker', async () => {
const applicationReference = 'aaa-bbb-ccc'

fakeOrchestrationApi
.put(`/visits/${applicationReference}/cancel`, <CancelVisitOrchestrationDto>{
cancelOutcome: {
outcomeStatus: 'BOOKER_CANCELLED',
},
applicationMethodType: 'WEBSITE',
actionedBy: bookerReference.value,
})
.matchHeader('authorization', `Bearer ${token}`)
.reply(200)

await orchestrationApiClient.cancelVisit({
applicationReference,
actionedBy: bookerReference.value,
})

expect(fakeOrchestrationApi.isDone()).toBe(true)
})
})

describe('getFuturePublicVisits', () => {
it('should retrieve all future visits associated with a booker', async () => {
const visits = [TestData.orchestrationVisitDto({ outcomeStatus: null })]
Expand Down
20 changes: 20 additions & 0 deletions server/data/orchestrationApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
VisitDto,
VisitorInfoDto,
AvailableVisitSessionRestrictionDto,
CancelVisitOrchestrationDto,
} from './orchestrationApiTypes'

export type SessionRestriction = AvailableVisitSessionDto['sessionRestriction']
Expand Down Expand Up @@ -44,6 +45,25 @@ export default class OrchestrationApiClient {
})
}

async cancelVisit({
applicationReference,
actionedBy,
}: {
applicationReference: string
actionedBy: string
}): Promise<void> {
await this.restClient.put({
path: `/visits/${applicationReference}/cancel`,
data: <CancelVisitOrchestrationDto>{
cancelOutcome: {
outcomeStatus: 'BOOKER_CANCELLED',
},
applicationMethodType: 'WEBSITE',
actionedBy,
},
})
}

async getFuturePublicVisits(bookerReference: string): Promise<OrchestrationVisitDto[]> {
return this.restClient.get({ path: `/public/booker/${bookerReference}/visits/booked/future` })
}
Expand Down
2 changes: 2 additions & 0 deletions server/data/orchestrationApiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export type BookerPrisonerInfoDto = components['schemas']['BookerPrisonerInfoDto

export type BookerReference = components['schemas']['BookerReference']

export type CancelVisitOrchestrationDto = components['schemas']['CancelVisitOrchestrationDto']

export type ChangeApplicationDto = components['schemas']['ChangeApplicationDto']

export type CreateApplicationDto = components['schemas']['CreateApplicationDto']
Expand Down
5 changes: 5 additions & 0 deletions server/routes/bookings/bookingDetailsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,17 @@ export default class BookingDetailsController {

const prison = await this.prisonService.getPrison(visit.prisonId)

const nowTimestamp = new Date()
const visitStartTimestamp = new Date(visit.startTimestamp)
const showCancel = nowTimestamp < visitStartTimestamp

return res.render('pages/bookings/visit', {
booker,
prison,
prisoner,
type,
visit,
showCancel,
showServiceNav: true,
})
}
Expand Down
11 changes: 8 additions & 3 deletions server/routes/bookings/bookingsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,12 @@ describe('Bookings homepage (future visits)', () => {
expect($('[data-test="visit-start-time-1"]').text()).toBe('10am')
expect($('[data-test="visit-end-time-1"]').text()).toBe('11:30am')
expect($('[data-test="visit-reference-1"]').text()).toBe('ab-cd-ef-gh')
expect($('[data-test="visit-link-1"]').attr('href')).toBe(
expect($('[data-test="visit-link-booking-1"]').attr('href')).toBe(
`${paths.BOOKINGS.VISIT}/${futureVisitDetails.visitDisplayId}`,
)
expect($('[data-test="visit-link-cancel-1"]').attr('href')).toBe(
`${paths.BOOKINGS.CANCEL_VISIT}/${futureVisitDetails.visitDisplayId}`,
)

expect($('[data-test=change-booking-heading]').length).toBeTruthy()
expect($('[data-test="prison-name"]').text()).toBe(prison.prisonName)
Expand Down Expand Up @@ -122,9 +125,10 @@ describe('Past visits page', () => {
expect($('[data-test="visit-date-1"]').text()).toBe('Thursday 30 May 2024')
expect($('[data-test="visit-start-time-1"]').text()).toBe('10am')
expect($('[data-test="visit-end-time-1"]').text()).toBe('11:30am')
expect($('[data-test="visit-link-1"]').attr('href')).toBe(
expect($('[data-test="visit-link-booking-1"]').attr('href')).toBe(
`${paths.BOOKINGS.VISIT_PAST}/${pastVisitDetails.visitDisplayId}`,
)
expect($('[data-test="visit-link-cancel-1"]').attr('href')).toBe(undefined)

expect($('[data-test="no-visits"]').length).toBeFalsy()

Expand Down Expand Up @@ -189,9 +193,10 @@ describe('Cancelled visits page', () => {
expect($('[data-test="visit-date-1"]').text()).toBe('Thursday 30 May 2024')
expect($('[data-test="visit-start-time-1"]').text()).toBe('10am')
expect($('[data-test="visit-end-time-1"]').text()).toBe('11:30am')
expect($('[data-test="visit-link-1"]').attr('href')).toBe(
expect($('[data-test="visit-link-booking-1"]').attr('href')).toBe(
`${paths.BOOKINGS.VISIT_CANCELLED}/${cancelledVisitDetails.visitDisplayId}`,
)
expect($('[data-test="visit-link-cancel-1"]').attr('href')).toBe(undefined)

expect($('[data-test="no-visits"]').length).toBeFalsy()

Expand Down
164 changes: 164 additions & 0 deletions server/routes/bookings/cancelVisitController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type { Express } from 'express'
import request from 'supertest'
import * as cheerio from 'cheerio'
import { SessionData } from 'express-session'
import { randomUUID } from 'crypto'
import { FieldValidationError } from 'express-validator'
import { appWithAllRoutes, flashProvider } from '../testutils/appSetup'
import { createMockBookerService, createMockVisitService } from '../../services/testutils/mocks'
import TestData from '../testutils/testData'
import paths from '../../constants/paths'
import { VisitDetails } from '../../services/visitService'

let app: Express

const bookerService = createMockBookerService()
const visitService = createMockVisitService()

const bookerReference = TestData.bookerReference().value
const prisoner = TestData.prisoner()
const visitDisplayId = randomUUID()

let sessionData: SessionData
let bookings: SessionData['bookings']
let visitDetails: VisitDetails

beforeEach(() => {
visitDetails = TestData.visitDetails({ visitDisplayId })
bookings = { type: 'future', visits: [visitDetails] }

sessionData = {
booker: {
reference: bookerReference,
prisoners: [prisoner],
},
bookings,
} as SessionData

app = appWithAllRoutes({ services: { bookerService, visitService }, sessionData })
})

afterEach(() => {
jest.resetAllMocks()
})

describe('Cancel a booking', () => {
describe('GET - Display visit information on cancellation page', () => {
it('should render the cancel confirmation page', () => {
return request(app)
.get(`${paths.BOOKINGS.CANCEL_VISIT}/${visitDetails.visitDisplayId}`)
.expect('Content-Type', /html/)
.expect(res => {
const $ = cheerio.load(res.text)
expect($('title').text()).toMatch(/^Cancel your booking -/)
expect($('[data-test="back-link"]').attr('href')).toBe(
`${paths.BOOKINGS.VISIT}/${visitDetails.visitDisplayId}`,
)
expect($('h1').text()).toBe('Are you sure you want to cancel your booking?')

expect($('[data-test="visit-date"]').text()).toBe('Thursday 30 May 2024')
expect($('[data-test="visit-start-time"]').text()).toBe('10am')
expect($('[data-test="visit-end-time"]').text()).toBe('11:30am')
expect($('[data-test="prisoner-name"]').text()).toBe('John Smith')
expect($('[data-test="visitor-name-1"]').text()).toContain('Keith Phillips')
expect($('form[method=POST]').attr('action')).toBe(
`${paths.BOOKINGS.CANCEL_VISIT}/${visitDetails.visitDisplayId}`,
)
})
})

describe('GET - Display cancellation confirmed page', () => {
it('should render the page confirming the visit has been cancelled', () => {
return request(app)
.get(`${paths.BOOKINGS.CANCEL_CONFIRMATION}`)
.expect('Content-Type', /html/)
.expect(res => {
const $ = cheerio.load(res.text)
expect($('title').text()).toMatch(/^Booking cancelled -/)
expect($('[data-test="back-link"]').attr('href')).toBe(undefined)
expect($('h1').text()).toContain('Booking cancelled')
expect($('h2').text()).toContain('What happens next')
expect($('p').text()).toContain(
'The main contact for this booking will get a text message to confirm it has been cancelled.',
)
})
})
})
})

describe('POST - cancel booking', () => {
it('should have cancelled the visit and redirect to confirmation page', () => {
return request(app)
.post(`${paths.BOOKINGS.CANCEL_VISIT}/${visitDetails.visitDisplayId}`)
.send('cancelBooking=yes')
.expect(302)
.expect('location', paths.BOOKINGS.CANCEL_CONFIRMATION)
.expect(() => {
expect(visitService.cancelVisit).toHaveBeenCalledTimes(1)
expect(visitService.cancelVisit).toHaveBeenCalledWith({
actionedBy: 'aaaa-bbbb-cccc',
applicationReference: 'ab-cd-ef-gh',
})
})
})

it('should redirect to visit details page if "no" is selected', () => {
return request(app)
.post(`${paths.BOOKINGS.CANCEL_VISIT}/${visitDetails.visitDisplayId}`)
.send('cancelBooking=no')
.expect(302)
.expect('location', `${paths.BOOKINGS.VISIT}/${visitDetails.visitDisplayId}`)
.expect(() => {
expect(visitService.cancelVisit).toHaveBeenCalledTimes(0)
})
})

it('should NOT redirect when incorrect value posted', () => {
const expectedValidationError: FieldValidationError = {
location: 'body',
msg: 'No answer selected',
path: 'cancelBooking',
type: 'field',
value: 'test',
}
return request(app)
.post(`${paths.BOOKINGS.CANCEL_VISIT}/${visitDetails.visitDisplayId}`)
.send('cancelBooking=test')
.expect(302)
.expect('location', `${paths.BOOKINGS.CANCEL_VISIT}/${visitDetails.visitDisplayId}`)
.expect(() => {
expect(visitService.cancelVisit).toHaveBeenCalledTimes(0)
expect(flashProvider).toHaveBeenCalledWith('errors', [expectedValidationError])
})
})

it('should NOT redirect when no value posted', () => {
const expectedValidationError: FieldValidationError = {
location: 'body',
msg: 'No answer selected',
path: 'cancelBooking',
type: 'field',
value: undefined,
}
return request(app)
.post(`${paths.BOOKINGS.CANCEL_VISIT}/${visitDetails.visitDisplayId}`)
.expect(302)
.expect('location', `${paths.BOOKINGS.CANCEL_VISIT}/${visitDetails.visitDisplayId}`)
.expect(() => {
expect(visitService.cancelVisit).toHaveBeenCalledTimes(0)
expect(flashProvider).toHaveBeenCalledWith('errors', [expectedValidationError])
})
})

it('should NOT cancel the visit if invalid visit ID is posted', () => {
return request(app)
.post(`${paths.BOOKINGS.CANCEL_VISIT}/test`)
.send('cancelBooking=yes')
.expect(302)
.expect('location', paths.BOOKINGS.HOME)
.expect(() => {
expect(visitService.cancelVisit).toHaveBeenCalledTimes(0)
})
})
})
})
Loading

0 comments on commit 27da41f

Please sign in to comment.