Skip to content

Commit 8be893e

Browse files
feat: add withdraw link (#59)
* feat: add withdraw link * bump: version * feat: add form to confirm withdraw application * fix: linting * feat: integrate with withdraw application API * feat: add unit tests * fix: linting
1 parent 227e58b commit 8be893e

File tree

10 files changed

+215
-4
lines changed

10 files changed

+215
-4
lines changed

app/api/applications.js

+21-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,27 @@ async function getApplications (searchType, searchText, limit, offset, filterSta
3636
return { applications: [], total: 0, applicationStatus: [] }
3737
}
3838
}
39+
40+
async function withdrawApplication (reference, user, status) {
41+
const url = `${applicationApiUri}/application/${reference}`
42+
const options = {
43+
payload: {
44+
user,
45+
status
46+
},
47+
json: true
48+
}
49+
try {
50+
await Wreck.put(url, options)
51+
return true
52+
} catch (err) {
53+
console.log(err)
54+
return false
55+
}
56+
}
57+
3958
module.exports = {
4059
getApplications,
41-
getApplication
60+
getApplication,
61+
withdrawApplication
4262
}

app/frontend/src/css/application.scss

+13
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,16 @@ a.pagination__link:focus {
224224
.govuk-summary-list {
225225
margin-bottom: 60px !important;
226226
}
227+
228+
.govuk-link {
229+
color: #d4351c;
230+
}
231+
232+
.govuk-panel--confirmation {
233+
background-color: #fafafa;
234+
border-color: #000000;
235+
border-width: medium;
236+
h1 {
237+
color: #000000;
238+
}
239+
}

app/plugins/router.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const routes = [].concat(
1010
require('../routes/login'),
1111
require('../routes/logout'),
1212
require('../routes/privacy-policy'),
13+
require('../routes/withdraw-application'),
1314
require('../routes/view-application')
1415
)
1516

app/routes/view-application.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ module.exports = {
1616
reference: Joi.string().valid()
1717
}),
1818
query: Joi.object({
19-
page: Joi.number().greater(0).default(1)
19+
page: Joi.number().greater(0).default(1),
20+
withdraw: Joi.bool().default(false)
2021
})
2122
},
2223
handler: async (request, h) => {
@@ -27,13 +28,18 @@ module.exports = {
2728

2829
const status = upperFirstLetter(application.status.status.toLowerCase())
2930
const statusClass = getStyleClassByStatus(application.status.status)
31+
const withdrawLinkStatus = ['IN CHECK', 'AGREED']
32+
const withdrawConfirmationForm = application.status.status !== 'WITHDRAWN' && withdrawLinkStatus.includes(application.status.status) && request.query.withdraw
33+
3034
return h.view('view-application', {
3135
applicationId: application.reference,
3236
status,
3337
statusClass,
3438
organisationName: application?.data?.organisation?.name,
3539
vetVisit: application?.vetVisit,
3640
claimed: application?.claimed,
41+
withdrawLink: withdrawLinkStatus.includes(application.status.status),
42+
withdrawConfirmationForm,
3743
payment: application?.payment,
3844
...new ViewModel(application),
3945
page: request.query.page

app/routes/withdraw-application.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const Joi = require('joi')
2+
const { withdrawApplication } = require('../api/applications')
3+
4+
module.exports = {
5+
method: 'POST',
6+
path: '/withdraw-application',
7+
options: {
8+
validate: {
9+
payload: Joi.object({
10+
withdrawConfirmation: Joi.string().valid('yes', 'no'),
11+
reference: Joi.string().valid(),
12+
page: Joi.number().greater(0).default(1)
13+
})
14+
},
15+
handler: async (request, h) => {
16+
if (request.payload.withdrawConfirmation === 'yes') {
17+
await withdrawApplication(request.payload.reference, 'admin', 2)
18+
}
19+
return h.redirect(`/view-application/${request.payload.reference}?page=${request?.payload?.page || 1}`)
20+
}
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{% macro viewWithdrawConfirmationForm(applicationId, page, crumb) %}
2+
<div class="govuk-panel govuk-panel--confirmation govuk-!-text-align-left">
3+
<h1 class="govuk-panel__title-s govuk-!-font-size-36 govuk-!-margin-top-1">Are you sure you want to withdraw?</h1>
4+
<form method="POST" autocomplete="off" novalidate="novalidate" action="/withdraw-application">
5+
<button class="govuk-button govuk-button govuk-!-margin-bottom-3" name="withdrawConfirmation" value="yes">Yes</button>
6+
<button class="govuk-button govuk-button--secondary govuk-!-margin-bottom-3" name="withdrawConfirmation" value="no">No</button>
7+
<input type="hidden" name="reference" value="{{ applicationId }}" />
8+
<input type="hidden" name="page" value="{{ page }}" />
9+
<input type="hidden" name="crumb" value="{{crumb}}"/>
10+
</form>
11+
</div>
12+
{% endmacro %}

app/views/view-application.njk

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{% extends './layouts/layout.njk' %}
22
{% from "./macros/view-application-tabs.njk" import viewApplicationTabs %}
3+
{% from "./macros/withdraw-confirmation-form.njk" import viewWithdrawConfirmationForm %}
34

45
{% block pageTitle %}
56
{{ siteTitle }}: User Application
@@ -23,7 +24,13 @@
2324
{{ govukSummaryList({
2425
rows: model.listData.rows
2526
}) }}
27+
{% if withdrawConfirmationForm %}
28+
{{ viewWithdrawConfirmationForm(applicationId, page, crumb) }}
29+
{% endif %}
2630
{{ viewApplicationTabs(model, vetVisit, claimed, payment, status) }}
31+
{% if withdrawLink %}
32+
<a class="govuk-link govuk-body" href="/view-application/{{ applicationId }}?page={{page}}&withdraw=true">Withdraw</a>
33+
{% endif %}
2734
</div>
2835
</div>
2936
{% endblock %}

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ffc-ahwr-backoffice",
3-
"version": "1.14.10",
3+
"version": "1.15.0",
44
"description": "Back office of the health and welfare of your livestock",
55
"homepage": "https://github.com/DEFRA/ffc-ahwr-backoffice",
66
"main": "app/index.js",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const cheerio = require('cheerio')
2+
const expectPhaseBanner = require('../../../utils/phase-banner-expect')
3+
const { administrator } = require('../../../../app/auth/permissions')
4+
const getCrumbs = require('../../../utils/get-crumbs')
5+
6+
const applications = require('../../../../app/api/applications')
7+
jest.mock('../../../../app/api/applications')
8+
9+
const reference = 'AHWR-555A-FD4C'
10+
11+
applications.withdrawApplication = jest.fn().mockResolvedValue(true)
12+
13+
describe('View Application test', () => {
14+
let crumb
15+
const url = '/withdraw-application/'
16+
jest.mock('../../../../app/auth')
17+
const auth = { strategy: 'session-auth', credentials: { scope: [administrator] } }
18+
19+
beforeEach(async () => {
20+
crumb = await getCrumbs(global.__SERVER__)
21+
jest.clearAllMocks()
22+
})
23+
24+
describe(`POST ${url} route`, () => {
25+
test('returns 302 no auth', async () => {
26+
const options = {
27+
method: 'POST',
28+
url
29+
}
30+
const res = await global.__SERVER__.inject(options)
31+
expect(res.statusCode).toBe(302)
32+
})
33+
34+
test('returns 403', async () => {
35+
const options = {
36+
method: 'POST',
37+
url,
38+
auth,
39+
payload: {
40+
reference
41+
}
42+
}
43+
const res = await global.__SERVER__.inject(options)
44+
expect(res.statusCode).toBe(403)
45+
const $ = cheerio.load(res.payload)
46+
expect($('h1.govuk-heading-l').text()).toEqual('403 - Forbidden')
47+
expectPhaseBanner.ok($)
48+
})
49+
50+
test('Approve withdraw application', async () => {
51+
const options = {
52+
method: 'POST',
53+
url,
54+
auth,
55+
headers: { cookie: `crumb=${crumb}` },
56+
payload: {
57+
reference,
58+
withdrawConfirmation: 'yes',
59+
page: 1,
60+
crumb
61+
}
62+
}
63+
const res = await global.__SERVER__.inject(options)
64+
expect(applications.withdrawApplication).toHaveBeenCalledTimes(1)
65+
expect(res.statusCode).toBe(302)
66+
expect(res.headers.location).toEqual(`/view-application/${reference}?page=1`)
67+
})
68+
69+
test('Cancel withdraw application', async () => {
70+
const options = {
71+
method: 'POST',
72+
url,
73+
auth,
74+
headers: { cookie: `crumb=${crumb}` },
75+
payload: {
76+
reference,
77+
withdrawConfirmation: 'no',
78+
page: 1,
79+
crumb
80+
}
81+
}
82+
const res = await global.__SERVER__.inject(options)
83+
expect(applications.withdrawApplication).toHaveBeenCalledTimes(0)
84+
expect(res.statusCode).toBe(302)
85+
expect(res.headers.location).toEqual(`/view-application/${reference}?page=1`)
86+
})
87+
})
88+
})

test/unit/api/applications.test.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const limit = 20
77
const offset = 0
88
let searchText = ''
99
let searchType = ''
10-
const { getApplications, getApplication } = require('../../../app/api/applications')
10+
const { getApplications, getApplication, withdrawApplication } = require('../../../app/api/applications')
1111
describe('Application API', () => {
1212
it('GetApplications should return empty applications array', async () => {
1313
jest.mock('@hapi/wreck')
@@ -144,4 +144,46 @@ describe('Application API', () => {
144144
expect(Wreck.get).toHaveBeenCalledTimes(1)
145145
expect(Wreck.get).toHaveBeenCalledWith(`${applicationApiUri}/application/get/${appRef}`, options)
146146
})
147+
148+
it('WithdrawApplication should return false if api not available', async () => {
149+
jest.mock('@hapi/wreck')
150+
const options = {
151+
payload: {
152+
user: 'test',
153+
status: 2
154+
},
155+
json: true
156+
}
157+
Wreck.put = jest.fn(async function (_url, _options) {
158+
throw (new Error('fakeError'))
159+
})
160+
const response = await withdrawApplication(appRef, 'test', 2)
161+
expect(response).toBe(false)
162+
expect(Wreck.put).toHaveBeenCalledTimes(1)
163+
expect(Wreck.put).toHaveBeenCalledWith(`${applicationApiUri}/application/${appRef}`, options)
164+
})
165+
166+
it('WithdrawApplication should return true after successful API request', async () => {
167+
jest.mock('@hapi/wreck')
168+
const options = {
169+
payload: {
170+
user: 'test',
171+
status: 2
172+
},
173+
json: true
174+
}
175+
const wreckResponse = {
176+
res: {
177+
statusCode: 200
178+
}
179+
}
180+
181+
Wreck.put = jest.fn(async function (_url, _options) {
182+
return wreckResponse
183+
})
184+
const response = await withdrawApplication(appRef, 'test', 2)
185+
expect(response).toBe(true)
186+
expect(Wreck.put).toHaveBeenCalledTimes(1)
187+
expect(Wreck.put).toHaveBeenCalledWith(`${applicationApiUri}/application/${appRef}`, options)
188+
})
147189
})

0 commit comments

Comments
 (0)