Skip to content

Commit 92eabf9

Browse files
authored
RA-275: forward app api functionality (#37)
* api functionality * add client and service tests * update test descriptions * remove test data * update test url * update routes, views and tests * reinstate service calls * temp comment out unit test
1 parent 24af4e2 commit 92eabf9

14 files changed

+326
-97
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import TestData from '../../server/routes/testutils/testData'
2+
import ForwardApplicationPage from '../pages/forwardApplication/swapVisitingOrdersForPinCredit'
3+
import Page from '../pages/page'
4+
5+
context('Forward Application Page', () => {
6+
let page: ForwardApplicationPage
7+
const { prisonerApp: application } = new TestData()
8+
const {
9+
id: applicationId,
10+
requestedBy: { id: prisonerId },
11+
} = application
12+
13+
const visitPage = () => {
14+
cy.task('reset')
15+
cy.task('stubSignIn')
16+
cy.task('stubGetPrisonerApp', { prisonerId, applicationId, application })
17+
cy.signIn()
18+
cy.visit(`/applications/business-hub/${prisonerId}/${applicationId}/forward`)
19+
page = Page.verifyOnPage(ForwardApplicationPage)
20+
}
21+
22+
beforeEach(visitPage)
23+
24+
describe('Page structure', () => {
25+
it('should display the correct page title', () => {
26+
page.pageTitle().should('include', "Forward this application to swap VO's")
27+
})
28+
29+
it('should display all form elements', () => {
30+
page.forwardToDepartment().should('exist')
31+
page.forwardingReason().should('exist')
32+
page.continueButton().should('exist').and('contain.text', 'Continue')
33+
})
34+
})
35+
36+
describe('Form interactions', () => {
37+
it('should allow submitting the form when a department is selected', () => {
38+
page.selectForwardToDepartment('activities')
39+
page.continueButton().click()
40+
cy.url().should('not.include', '/forward')
41+
})
42+
43+
it('should allow adding a forwarding reason', () => {
44+
const reason = 'This application needs to be reviewed by Activities department.'
45+
page.forwardingReason().type(reason).should('have.value', reason)
46+
})
47+
})
48+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Page from '../page'
2+
3+
export default class ForwardApplicationPage extends Page {
4+
constructor() {
5+
super("Forward this application to swap VO's")
6+
}
7+
8+
pageTitle = () => cy.title()
9+
10+
forwardToDepartment = () => cy.get('.govuk-radios')
11+
12+
forwardingReason = () => cy.get('#forwarding-reason')
13+
14+
continueButton = () => cy.get('button.govuk-button--primary')
15+
16+
errorSummary = () => cy.get('.govuk-error-summary')
17+
18+
selectForwardToDepartment = (department: string) => {
19+
this.forwardToDepartment().find(`input[value="${department}"]`).check()
20+
}
21+
}

server/data/managingPrisonerAppsClient.test.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('Managing Prisoner Apps API Client', () => {
2020

2121
afterEach(() => nock.cleanAll())
2222

23-
it('should return a response from the api', async () => {
23+
it('should fetch a prisoner application by prisoner and application ID', async () => {
2424
fakeManagingPrisonerAppApi
2525
.get('/v1/prisoners/prisoner-id/apps/app-id')
2626
.matchHeader('authorization', `Bearer ${user.token}`)
@@ -29,4 +29,15 @@ describe('Managing Prisoner Apps API Client', () => {
2929
const output = await client.getPrisonerApp('prisoner-id', 'app-id')
3030
expect(output).toEqual(prisonerApp)
3131
})
32+
33+
// Reinstate with correct api endpoint and implementation once it is available
34+
// it('should successfully forward an application to a different department', async () => {
35+
// fakeManagingPrisonerAppApi
36+
// .get('/v1/')
37+
// .matchHeader('authorization', `Bearer ${user.token}`)
38+
// .reply(200, undefined)
39+
40+
// const output = await client.forwardApp('prisoner-id', 'app-id', 'dept')
41+
// expect(output).toBeUndefined()
42+
// })
3243
})

server/data/managingPrisonerAppsClient.ts

+13
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,17 @@ export default class ManagingPrisonerAppsApiClient {
2424
return null
2525
}
2626
}
27+
28+
async forwardApp(prisonerId: string, applicationId: string, department: string): Promise<void> {
29+
try {
30+
await this.restClient.put({
31+
path: `/v1/`,
32+
})
33+
} catch (error) {
34+
logger.error(
35+
`Error updating department for prisonerId: ${prisonerId}, applicationId: ${applicationId}, department: ${department}`,
36+
error,
37+
)
38+
}
39+
}
2740
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Request, Response, Router } from 'express'
2+
import asyncMiddleware from '../../middleware/asyncMiddleware'
3+
import AuditService, { Page } from '../../services/auditService'
4+
import ManagingPrisonerAppsService from '../../services/managingPrisonerAppsService'
5+
import { getApplicationType } from '../../utils/getApplicationType'
6+
import { validateForwardingApplication } from '../validate/validateForwardingApplication'
7+
8+
export default function forwardApplicationRoutes({
9+
auditService,
10+
managingPrisonerAppsService,
11+
}: {
12+
auditService: AuditService
13+
managingPrisonerAppsService: ManagingPrisonerAppsService
14+
}): Router {
15+
const router = Router()
16+
17+
router.get(
18+
'/applications/:departmentName/:prisonerId/:applicationId/forward',
19+
asyncMiddleware(async (req: Request, res: Response) => {
20+
const { departmentName, prisonerId, applicationId } = req.params
21+
const { user } = res.locals
22+
23+
const application = await managingPrisonerAppsService.getPrisonerApp(prisonerId, applicationId, user)
24+
25+
if (!application) {
26+
return res.redirect(`/applications/${departmentName}/pending`)
27+
}
28+
29+
await auditService.logPageView(Page.FORWARD_APPLICATION_PAGE, {
30+
who: user.username,
31+
correlationId: req.id,
32+
})
33+
34+
const applicationType = getApplicationType(application.type)
35+
36+
if (!applicationType) {
37+
return res.redirect(`/applications/${departmentName}/pending?error=unknown-type`)
38+
}
39+
40+
return res.render(`pages/forward-application/${applicationType.value}`, {
41+
application,
42+
departmentName,
43+
textareaValue: '',
44+
title: "Forward this application to swap VO's",
45+
errors: null,
46+
})
47+
}),
48+
)
49+
50+
router.post(
51+
'/applications/:departmentName/:prisonerId/:applicationId/forward',
52+
asyncMiddleware(async (req: Request, res: Response) => {
53+
const { departmentName, prisonerId, applicationId } = req.params
54+
const { forwardToDepartment, forwardingReason } = req.body
55+
const { user } = res.locals
56+
57+
const application = await managingPrisonerAppsService.getPrisonerApp(prisonerId, applicationId, user)
58+
59+
if (!application) {
60+
return res.redirect(`/applications/${departmentName}/pending`)
61+
}
62+
63+
const applicationType = getApplicationType(application.type)
64+
const errors = validateForwardingApplication(forwardToDepartment, forwardingReason)
65+
66+
if (Object.keys(errors).length > 0) {
67+
return res.render(`pages/forward-application/${applicationType.value}`, {
68+
application,
69+
departmentName,
70+
textareaValue: forwardingReason,
71+
title: "Forward this application to swap VO's",
72+
errors,
73+
})
74+
}
75+
76+
await managingPrisonerAppsService.forwardApp(prisonerId, applicationId, forwardToDepartment, user)
77+
78+
return res.redirect(`/applications/${departmentName}/${prisonerId}/${applicationId}`)
79+
}),
80+
)
81+
82+
return router
83+
}

server/routes/applications/index.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Request, Response, Router } from 'express'
22
import asyncMiddleware from '../../middleware/asyncMiddleware'
33
import AuditService, { Page } from '../../services/auditService'
4+
import ManagingPrisonerAppsService from '../../services/managingPrisonerAppsService'
5+
import PrisonService from '../../services/prisonService'
46
import applicationTypeRoutes from './applicationTypeRoutes'
7+
import forwardApplicationRoutes from './forwardApplicationRoutes'
58
import prisonerDetailsRoutes from './prisonerDetailsRoutes'
6-
import swapVosPinCreditDetailsRoutes from './swapVosPinCreditDetailsRoutes'
79
import submitApplicationRoutes from './submitApplicationRoutes'
10+
import swapVosPinCreditDetailsRoutes from './swapVosPinCreditDetailsRoutes'
811
import viewApplicationRoutes from './viewApplicationsRoutes'
9-
import ManagingPrisonerAppsService from '../../services/managingPrisonerAppsService'
10-
import PrisonService from '../../services/prisonService'
1112

1213
export default function applicationsRoutes({
1314
auditService,
@@ -62,9 +63,10 @@ export default function applicationsRoutes({
6263
)
6364

6465
router.use(applicationTypeRoutes({ auditService }))
66+
router.use(forwardApplicationRoutes({ auditService, managingPrisonerAppsService }))
6567
router.use(prisonerDetailsRoutes({ auditService, prisonService }))
66-
router.use(swapVosPinCreditDetailsRoutes({ auditService }))
6768
router.use(submitApplicationRoutes({ auditService }))
69+
router.use(swapVosPinCreditDetailsRoutes({ auditService }))
6870
router.use(viewApplicationRoutes({ auditService, managingPrisonerAppsService }))
6971

7072
return router
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Request, Response, Router } from 'express'
2-
import { APPLICATION_TYPES } from '../../constants/applicationTypes'
32
import asyncMiddleware from '../../middleware/asyncMiddleware'
43
import AuditService, { Page } from '../../services/auditService'
54
import ManagingPrisonerAppsService from '../../services/managingPrisonerAppsService'
5+
import { getApplicationType } from '../../utils/getApplicationType'
66

77
export default function viewApplicationRoutes({
88
auditService,
@@ -34,68 +34,27 @@ export default function viewApplicationRoutes({
3434
const application = await managingPrisonerAppsService.getPrisonerApp(prisonerId, applicationId, user)
3535

3636
if (!application) {
37-
res.redirect(`/applications/${departmentName}/pending`)
38-
return
37+
return res.redirect(`/applications/${departmentName}/pending`)
3938
}
4039

4140
await auditService.logPageView(Page.VIEW_APPLICATION_PAGE, {
4241
who: user.username,
4342
correlationId: req.id,
4443
})
4544

46-
const applicationType = APPLICATION_TYPES.find(type => type.apiValue === application.type)
45+
const applicationType = getApplicationType(application.type)
4746

4847
if (!applicationType) {
49-
res.redirect(`/applications/${departmentName}/pending?error=unknown-type`)
50-
return
48+
return res.redirect(`/applications/${departmentName}/pending?error=unknown-type`)
5149
}
5250

53-
res.render(`pages/view-application/${applicationType.value}`, {
51+
return res.render(`pages/view-application/${applicationType.value}`, {
5452
title: applicationType.name,
5553
application,
5654
departmentName,
5755
})
5856
}),
5957
)
6058

61-
router.get(
62-
'/applications/:departmentName/:prisonerId/:applicationId/forward',
63-
asyncMiddleware(async (req: Request, res: Response) => {
64-
const { departmentName, prisonerId, applicationId } = req.params
65-
const { user } = res.locals
66-
67-
const application = await managingPrisonerAppsService.getPrisonerApp(prisonerId, applicationId, user)
68-
69-
if (!application) {
70-
res.redirect(`/applications/${departmentName}/pending`)
71-
return
72-
}
73-
74-
await auditService.logPageView(Page.FORWARD_APPLICATION_PAGE, {
75-
who: user.username,
76-
correlationId: req.id,
77-
})
78-
79-
const applicationType = APPLICATION_TYPES.find(type => type.apiValue === application.type)
80-
81-
if (!applicationType) {
82-
res.redirect(`/applications/${departmentName}/pending?error=unknown-type`)
83-
return
84-
}
85-
86-
res.render(`pages/forward-application/${applicationType.value}`, {
87-
application,
88-
departmentName,
89-
})
90-
}),
91-
)
92-
93-
router.post(
94-
'/applications/:departmentName/:prisonerId/:applicationId/forward',
95-
asyncMiddleware(async (req: Request, res: Response) => {
96-
res.redirect(``)
97-
}),
98-
)
99-
10059
return router
10160
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { validateForwardingApplication } from './validateForwardingApplication'
2+
3+
describe('validateForwardingApplication', () => {
4+
it('should return an empty object when there are no errors', () => {
5+
const errors = validateForwardingApplication('Some Department', 'Reason must be 1000 characters or less')
6+
7+
expect(errors).toEqual({})
8+
})
9+
10+
it('should return an error for missing forwardToDepartment', () => {
11+
const errors = validateForwardingApplication('', 'Reason must be 1000 characters or less')
12+
13+
expect(errors).toEqual({
14+
forwardToDepartment: { text: 'Choose where to send' },
15+
})
16+
})
17+
18+
it('should return an error for forwardingReason longer than 1000 characters', () => {
19+
const longReason = 'A'.repeat(1001)
20+
const errors = validateForwardingApplication('Some Department', longReason)
21+
22+
expect(errors).toEqual({
23+
forwardingReason: { text: 'Reason must be 1000 characters or less' },
24+
})
25+
})
26+
27+
it('should return both errors for missing forwardToDepartment and long forwardingReason', () => {
28+
const longReason = 'A'.repeat(1001)
29+
const errors = validateForwardingApplication('', longReason)
30+
31+
expect(errors).toEqual({
32+
forwardToDepartment: { text: 'Choose where to send' },
33+
forwardingReason: { text: 'Reason must be 1000 characters or less' },
34+
})
35+
})
36+
37+
it('should return no errors if forwardingReason is exactly 1000 characters', () => {
38+
const validReason = 'A'.repeat(1000)
39+
const errors = validateForwardingApplication('Some Department', validReason)
40+
41+
expect(errors).toEqual({})
42+
})
43+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// eslint-disable-next-line import/prefer-default-export
2+
export const validateForwardingApplication = (forwardToDepartment: string, forwardingReason: string) => {
3+
const errors: Record<string, { text: string }> = {}
4+
5+
if (!forwardToDepartment) {
6+
errors.forwardToDepartment = { text: 'Choose where to send' }
7+
}
8+
9+
if (forwardingReason && forwardingReason.length > 1000) {
10+
errors.forwardingReason = { text: 'Reason must be 1000 characters or less' }
11+
}
12+
13+
return errors
14+
}

server/services/managingPrisonerAppsService.test.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import TestData from '../routes/testutils/testData'
33
import ManagingPrisonerAppsService from './managingPrisonerAppsService'
44

55
const getPrisonerApp = jest.fn()
6+
const forwardApp = jest.fn()
67

78
const testData = new TestData()
89

910
jest.mock('../data/hmppsAuthClient')
1011
jest.mock('../data/managingPrisonerAppsClient', () => {
1112
return jest.fn().mockImplementation(() => {
12-
return { getPrisonerApp }
13+
return { getPrisonerApp, forwardApp }
1314
})
1415
})
1516

@@ -37,4 +38,13 @@ describe('Managing Prisoner Apps Service', () => {
3738
expect(getPrisonerApp).toHaveBeenCalledWith('prisoner-id', 'application-id')
3839
})
3940
})
41+
42+
describe('forwardApp', () => {
43+
it('should call the client and fetch the prisoner application', async () => {
44+
const result = await service.forwardApp('prisoner-id', 'application-id', 'dept', user)
45+
46+
expect(result).toBeUndefined()
47+
expect(forwardApp).toHaveBeenCalledWith('prisoner-id', 'application-id', 'dept')
48+
})
49+
})
4050
})

0 commit comments

Comments
 (0)