Skip to content

Commit

Permalink
Merge pull request #2218 from ministryofjustice/feature/APS-1384_with…
Browse files Browse the repository at this point in the history
…draw_space_booking

APS-1384 Withdraw a space booking
  • Loading branch information
bobmeredith authored Dec 2, 2024
2 parents 7ae0bda + 6577788 commit 46c8116
Show file tree
Hide file tree
Showing 58 changed files with 857 additions and 450 deletions.
9 changes: 7 additions & 2 deletions integration_tests/mockApis/cancellation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ import { cancellationReasons } from '../../server/testutils/referenceData/stubs/

export default {
stubCancellationReferenceData: (): SuperAgentRequest => stubFor(cancellationReasons),

stubCancellationCreate: (args: {
premisesId: string
bookingId: string
bookingId?: string
placementId?: string
cancellation: Cancellation
}): SuperAgentRequest =>
stubFor({
request: {
method: 'POST',
url: `/premises/${args.premisesId}/bookings/${args.bookingId}/cancellations`,
url: args.bookingId
? `/premises/${args.premisesId}/bookings/${args.bookingId}/cancellations`
: `/cas1/premises/${args.premisesId}/space-bookings/${args.placementId}/cancellations`,
},
response: {
status: 201,
Expand All @@ -26,6 +30,7 @@ export default {
}),
stubCancellationErrors: (args: { premisesId: string; bookingId: string; params: Array<string> }) =>
stubFor(errorStub(args.params, `/premises/${args.premisesId}/bookings/${args.bookingId}/cancellations`)),

verifyCancellationCreate: async (args: { premisesId: string; bookingId: string; cancellation: Cancellation }) =>
(
await getMatchingRequests({
Expand Down
18 changes: 18 additions & 0 deletions integration_tests/mockApis/spaceBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,24 @@ export default {
status: 200,
},
}),

stubSpaceBookingGetWithoutPremises: (placement: Cas1SpaceBooking) =>
stubFor({
request: {
method: 'GET',
urlPattern: paths.placements.placementWithoutPremises({
placementId: placement.id,
}),
},
response: {
status: 200,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
jsonBody: placement,
},
}),

verifySpaceBookingDepartureCreate: async (placement: Cas1SpaceBooking) =>
(
await getMatchingRequests({
Expand Down
12 changes: 12 additions & 0 deletions integration_tests/pages/manage/cancellationCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export default class CancellationCreatePage extends Page {
return new CancellationCreatePage(premisesId, bookingId)
}

static visitWithSpaceBooking(premisesId: string, placementId: string): CancellationCreatePage {
cy.visit(paths.premises.placements.cancellations.new({ premisesId, placementId }))

return new CancellationCreatePage(premisesId, placementId)
}

completeForm(cancellation: NewCancellation): void {
this.getLegend('Why is this placement being withdrawn?')
this.checkRadioByNameAndValue('cancellation[reason]', cancellation.reason)
Expand All @@ -29,6 +35,12 @@ export default class CancellationCreatePage extends Page {
this.clickSubmit()
}

shouldShowBacklinkToSpaceBooking(): void {
cy.get('.govuk-back-link')
.should('have.attr', 'href')
.and('include', paths.premises.placements.show({ premisesId: this.premisesId, placementId: this.bookingId }))
}

shouldShowBacklinkToBooking(): void {
cy.get('.govuk-back-link')
.should('have.attr', 'href')
Expand Down
84 changes: 74 additions & 10 deletions integration_tests/tests/manage/cancellation.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
applicationFactory,
bookingFactory,
cancellationFactory,
cas1SpaceBookingFactory,
extendedPremisesSummaryFactory,
newCancellationFactory,
premisesFactory,
Expand All @@ -11,14 +12,15 @@ import {
import { CancellationCreatePage } from '../../pages/manage'
import { signIn } from '../signIn'
import BookingCancellationConfirmPage from '../../pages/manage/bookingCancellationConfirmation'
import apiPaths from '../../../server/paths/api'

context('Cancellation', () => {
beforeEach(() => {
cy.task('reset')
cy.task('stubCancellationReferenceData')

// Given I am signed in
signIn(['workflow_manager'], ['cas1_booking_withdraw'])
signIn(['workflow_manager'], ['cas1_booking_withdraw', 'cas1_space_booking_withdraw'])
})

it('should allow me to create a cancellation with a reason of "other" ', () => {
Expand Down Expand Up @@ -95,15 +97,8 @@ context('Cancellation', () => {
page.completeForm(cancellation)

// Then a cancellation should have been created in the API
cy.task('verifyCancellationCreate', {
premisesId: premises.id,
bookingId: booking.id,
cancellation,
}).then(requests => {
expect(requests).to.have.length(1)
const requestBody = JSON.parse(requests[0].body)

expect(requestBody.reason).equal(cancellation.reason)
cy.task('verifyApiPost', `/premises/${premises.id}/bookings/${booking.id}/cancellations`).then(({ reason }) => {
expect(reason).equal(cancellation.reason)
})

// And I should see a confirmation message
Expand Down Expand Up @@ -138,4 +133,73 @@ context('Cancellation', () => {
// And the back link should be populated correctly
page.shouldShowBackLinkToApplicationWithdraw(booking.applicationId)
})

it('should allow me to create a cancellation for a space booking ', () => {
// Given a booking is available
const application = applicationFactory.build()
const placement = cas1SpaceBookingFactory.upcoming().build({ applicationId: application.id })
const premises = extendedPremisesSummaryFactory.build({ bookings: [placement], id: placement.premises.id })

const placementId = placement.id
const premisesId = premises.id

cy.task('stubSpaceBookingShow', placement)

const cancellation = newCancellationFactory.withOtherReason().build()
const withdrawable = withdrawableFactory.build({ id: placementId, type: 'space_booking' })
cy.task('stubPremisesSummary', premises)
cy.task('stubWithdrawablesWithNotes', { applicationId: application.id, withdrawables: [withdrawable] })
cy.task('stubSpaceBookingGetWithoutPremises', placement)
cy.task('stubCancellationCreate', { premisesId, placementId, cancellation })

// When I navigate to the booking's cancellation page
const cancellationPage = CancellationCreatePage.visitWithSpaceBooking(premisesId, placementId)

cancellationPage.shouldShowBackLinkToApplicationWithdraw(application.id)

// And I complete the reason and notes
cancellationPage.completeForm(cancellation)

// Then a cancellation should have been created in the API
cy.task('verifyApiPost', apiPaths.premises.placements.cancel({ premisesId, placementId })).then(
({ reasonNotes, reasonId }) => {
expect(reasonNotes).equal(cancellation.otherReason)
expect(reasonId).equal(cancellation.reason)
},
)

// And I should see a confirmation message
const confirmationPage = new BookingCancellationConfirmPage()
confirmationPage.shouldShowPanel()
})

it('should allow me to create a cancellation for a space-booking without an applicationId', () => {
// Given a placement is available
const premises = premisesFactory.build()
const placement = cas1SpaceBookingFactory.build({ applicationId: undefined })
const placementId = placement.id
const premisesId = premises.id
cy.task('stubSpaceBookingGetWithoutPremises', placement)

// When I navigate to the placements's cancellation page
const cancellation = newCancellationFactory.build()
cy.task('stubCancellationCreate', { premisesId, placementId, cancellation })

const page = CancellationCreatePage.visitWithSpaceBooking(premisesId, placementId)

// Then the backlink should be populated correctly
page.shouldShowBacklinkToSpaceBooking()

// When I fill out the cancellation form
page.completeForm(cancellation)

// Then a cancellation should have been created in the API
cy.task('verifyApiPost', apiPaths.premises.placements.cancel({ premisesId, placementId })).then(({ reasonId }) => {
expect(reasonId).equal(cancellation.reason)
})

// And I should see a confirmation message
const confirmationPage = new BookingCancellationConfirmPage()
confirmationPage.shouldShowPanel()
})
})
3 changes: 2 additions & 1 deletion server/controllers/apply/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const controllers = (services: Services) => {
apAreaService,
bookingService,
appealService,
placementService,
} = services
const applicationsController = new ApplicationsController(applicationService, personService)
const pagesController = new PagesController(applicationService, {
Expand All @@ -33,7 +34,7 @@ export const controllers = (services: Services) => {
const documentsController = new DocumentsController(personService)
const withdrawalsController = new WithdrawalsController(applicationService)
const notesController = new NotesController(applicationService)
const withdrawablesController = new WithdrawablesController(applicationService, bookingService)
const withdrawablesController = new WithdrawablesController(applicationService, bookingService, placementService)
const appealsController = new AppealsController(appealService, applicationService)

return {
Expand Down
63 changes: 51 additions & 12 deletions server/controllers/apply/withdrawablesController.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Request, Response } from 'express'
import { DeepMocked, createMock } from '@golevelup/ts-jest'
import { NextFunction } from 'express'
import { ApplicationService, BookingService } from '../../services'
import { ApplicationService, BookingService, PlacementService } from '../../services'
import WithdrawablesController from './withdrawablesController'
import { bookingFactory, withdrawableFactory } from '../../testutils/factories'
import { bookingFactory, cas1SpaceBookingFactory, withdrawableFactory } from '../../testutils/factories'
import adminPaths from '../../paths/admin'
import managePaths from '../../paths/manage'
import placementAppPaths from '../../paths/placementApplications'
Expand All @@ -25,11 +25,12 @@ describe('withdrawablesController', () => {

const applicationService = createMock<ApplicationService>({})
const bookingService = createMock<BookingService>({})
const placementService = createMock<PlacementService>({})

let withdrawablesController: WithdrawablesController

beforeEach(() => {
withdrawablesController = new WithdrawablesController(applicationService, bookingService)
withdrawablesController = new WithdrawablesController(applicationService, bookingService, placementService)
request = createMock<Request>({ user: { token }, flash })
response = createMock<Response>({})
jest.clearAllMocks()
Expand Down Expand Up @@ -77,19 +78,25 @@ describe('withdrawablesController', () => {
describe('Bookings', () => {
it(`renders the view, calling the booking service to retrieve bookings`, async () => {
const selectedWithdrawableType = 'placement'
const placementWithdrawables = withdrawableFactory.buildList(2, { type: 'booking' })
const bookingWithdrawables = withdrawableFactory.buildList(2, { type: 'booking' })
const spaceBookingWithdrawables = withdrawableFactory.buildList(2, { type: 'space_booking' })
const allPlacementWithdrawables = [...bookingWithdrawables, ...spaceBookingWithdrawables]
const applicationWithdrawable = withdrawableFactory.build({ type: 'application' })
const bookings = bookingFactory.buildList(2).map((b, i) => {
return { ...b, id: placementWithdrawables[i].id }
return { ...b, id: bookingWithdrawables[i].id }
})
const withdrawable = [applicationWithdrawable, ...placementWithdrawables]
const spaceBookings = cas1SpaceBookingFactory.buildList(2).map((b, i) => {
return { ...b, id: spaceBookingWithdrawables[i].id }
})
const withdrawable = [applicationWithdrawable, ...allPlacementWithdrawables]
const withdrawables = withdrawablesFactory.build({ withdrawables: withdrawable })

applicationService.getWithdrawablesWithNotes.mockResolvedValue(withdrawables)
;(sortAndFilterWithdrawables as jest.MockedFunction<typeof sortAndFilterWithdrawables>).mockReturnValue(
placementWithdrawables,
allPlacementWithdrawables,
)
bookings.forEach(b => bookingService.findWithoutPremises.mockResolvedValueOnce(b))
spaceBookings.forEach(b => placementService.getPlacement.mockResolvedValueOnce(b))

const requestHandler = withdrawablesController.show()

Expand All @@ -98,20 +105,23 @@ describe('withdrawablesController', () => {
response,
next,
)
expect(sortAndFilterWithdrawables).toHaveBeenCalledWith(withdrawable, ['booking'])
expect(sortAndFilterWithdrawables).toHaveBeenCalledWith(withdrawable, ['booking', 'space_booking'])
expect(applicationService.getWithdrawablesWithNotes).toHaveBeenCalledWith(token, applicationId)
expect(response.render).toHaveBeenCalledWith('applications/withdrawables/show', {
pageHeading: 'Select your placement',
id: applicationId,
withdrawables: placementWithdrawables,
bookings,
withdrawables: allPlacementWithdrawables,
allBookings: [...bookings, ...spaceBookings],
withdrawableType: 'placement',
notes: withdrawables.notes,
})
expect(bookingService.findWithoutPremises).toHaveBeenCalledTimes(2)
expect(bookingService.findWithoutPremises).toHaveBeenCalledWith(token, placementWithdrawables[0].id)
expect(bookingService.findWithoutPremises).toHaveBeenCalledWith(token, placementWithdrawables[1].id)
expect(bookingService.findWithoutPremises).toHaveBeenCalledWith(token, bookingWithdrawables[0].id)
expect(bookingService.findWithoutPremises).toHaveBeenCalledWith(token, bookingWithdrawables[1].id)
expect(bookingService.findWithoutPremises).not.toHaveBeenCalledWith(token, applicationWithdrawable.id)
expect(placementService.getPlacement).toHaveBeenCalledTimes(2)
expect(placementService.getPlacement).toHaveBeenCalledWith(token, spaceBookingWithdrawables[0].id)
expect(placementService.getPlacement).toHaveBeenCalledWith(token, spaceBookingWithdrawables[1].id)
})
})
})
Expand Down Expand Up @@ -182,5 +192,34 @@ describe('withdrawablesController', () => {
managePaths.bookings.cancellations.new({ bookingId: selectedWithdrawable, premisesId: booking.premises.id }),
)
})

it('redirects to the booking withdrawal page if the withdrawable is a space_booking (placement)', async () => {
const placementId = 'some-id'
const withdrawable = withdrawableFactory.build({
type: 'space_booking',
id: placementId,
})
const withdrawables = withdrawablesFactory.build({ withdrawables: [withdrawable] })

const placement = cas1SpaceBookingFactory.build({ id: placementId })

applicationService.getWithdrawablesWithNotes.mockResolvedValue(withdrawables)
placementService.getPlacement.mockResolvedValue(placement)

const requestHandler = withdrawablesController.create()

await requestHandler(
{ ...request, params: { id: applicationId }, body: { selectedWithdrawable: placementId } },
response,
next,
)

expect(applicationService.getWithdrawablesWithNotes).toHaveBeenCalledWith(token, applicationId)
expect(placementService.getPlacement).toHaveBeenCalledWith(token, placementId)
expect(response.redirect).toHaveBeenCalledWith(
302,
managePaths.premises.placements.cancellations.new({ placementId, premisesId: placement.premises.id }),
)
})
})
})
Loading

0 comments on commit 46c8116

Please sign in to comment.