diff --git a/app/api/applications.js b/app/api/applications.js
index 90ee399e..1c8a6962 100644
--- a/app/api/applications.js
+++ b/app/api/applications.js
@@ -36,7 +36,27 @@ async function getApplications (searchType, searchText, limit, offset, filterSta
return { applications: [], total: 0, applicationStatus: [] }
}
}
+
+async function withdrawApplication (reference, user, status) {
+ const url = `${applicationApiUri}/application/${reference}`
+ const options = {
+ payload: {
+ user,
+ status
+ },
+ json: true
+ }
+ try {
+ await Wreck.put(url, options)
+ return true
+ } catch (err) {
+ console.log(err)
+ return false
+ }
+}
+
module.exports = {
getApplications,
- getApplication
+ getApplication,
+ withdrawApplication
}
diff --git a/app/frontend/src/css/application.scss b/app/frontend/src/css/application.scss
index e4447da3..3d77f833 100644
--- a/app/frontend/src/css/application.scss
+++ b/app/frontend/src/css/application.scss
@@ -224,3 +224,16 @@ a.pagination__link:focus {
.govuk-summary-list {
margin-bottom: 60px !important;
}
+
+.govuk-link {
+ color: #d4351c;
+}
+
+.govuk-panel--confirmation {
+ background-color: #fafafa;
+ border-color: #000000;
+ border-width: medium;
+ h1 {
+ color: #000000;
+ }
+}
diff --git a/app/plugins/router.js b/app/plugins/router.js
index e95fce9a..f3b75d74 100644
--- a/app/plugins/router.js
+++ b/app/plugins/router.js
@@ -10,6 +10,7 @@ const routes = [].concat(
require('../routes/login'),
require('../routes/logout'),
require('../routes/privacy-policy'),
+ require('../routes/withdraw-application'),
require('../routes/view-application')
)
diff --git a/app/routes/view-application.js b/app/routes/view-application.js
index 37b2fa5a..a9030f8a 100644
--- a/app/routes/view-application.js
+++ b/app/routes/view-application.js
@@ -16,7 +16,8 @@ module.exports = {
reference: Joi.string().valid()
}),
query: Joi.object({
- page: Joi.number().greater(0).default(1)
+ page: Joi.number().greater(0).default(1),
+ withdraw: Joi.bool().default(false)
})
},
handler: async (request, h) => {
@@ -27,6 +28,9 @@ module.exports = {
const status = upperFirstLetter(application.status.status.toLowerCase())
const statusClass = getStyleClassByStatus(application.status.status)
+ const withdrawLinkStatus = ['IN CHECK', 'AGREED']
+ const withdrawConfirmationForm = application.status.status !== 'WITHDRAWN' && withdrawLinkStatus.includes(application.status.status) && request.query.withdraw
+
return h.view('view-application', {
applicationId: application.reference,
status,
@@ -34,6 +38,8 @@ module.exports = {
organisationName: application?.data?.organisation?.name,
vetVisit: application?.vetVisit,
claimed: application?.claimed,
+ withdrawLink: withdrawLinkStatus.includes(application.status.status),
+ withdrawConfirmationForm,
payment: application?.payment,
...new ViewModel(application),
page: request.query.page
diff --git a/app/routes/withdraw-application.js b/app/routes/withdraw-application.js
new file mode 100644
index 00000000..2f95ca9c
--- /dev/null
+++ b/app/routes/withdraw-application.js
@@ -0,0 +1,22 @@
+const Joi = require('joi')
+const { withdrawApplication } = require('../api/applications')
+
+module.exports = {
+ method: 'POST',
+ path: '/withdraw-application',
+ options: {
+ validate: {
+ payload: Joi.object({
+ withdrawConfirmation: Joi.string().valid('yes', 'no'),
+ reference: Joi.string().valid(),
+ page: Joi.number().greater(0).default(1)
+ })
+ },
+ handler: async (request, h) => {
+ if (request.payload.withdrawConfirmation === 'yes') {
+ await withdrawApplication(request.payload.reference, 'admin', 2)
+ }
+ return h.redirect(`/view-application/${request.payload.reference}?page=${request?.payload?.page || 1}`)
+ }
+ }
+}
diff --git a/app/views/macros/withdraw-confirmation-form.njk b/app/views/macros/withdraw-confirmation-form.njk
new file mode 100644
index 00000000..021d1f7e
--- /dev/null
+++ b/app/views/macros/withdraw-confirmation-form.njk
@@ -0,0 +1,12 @@
+{% macro viewWithdrawConfirmationForm(applicationId, page, crumb) %}
+
+
Are you sure you want to withdraw?
+
+
+{% endmacro %}
\ No newline at end of file
diff --git a/app/views/view-application.njk b/app/views/view-application.njk
index 361d973b..ca0d89ba 100644
--- a/app/views/view-application.njk
+++ b/app/views/view-application.njk
@@ -1,5 +1,6 @@
{% extends './layouts/layout.njk' %}
{% from "./macros/view-application-tabs.njk" import viewApplicationTabs %}
+{% from "./macros/withdraw-confirmation-form.njk" import viewWithdrawConfirmationForm %}
{% block pageTitle %}
{{ siteTitle }}: User Application
@@ -23,7 +24,13 @@
{{ govukSummaryList({
rows: model.listData.rows
}) }}
+ {% if withdrawConfirmationForm %}
+ {{ viewWithdrawConfirmationForm(applicationId, page, crumb) }}
+ {% endif %}
{{ viewApplicationTabs(model, vetVisit, claimed, payment, status) }}
+ {% if withdrawLink %}
+ Withdraw
+ {% endif %}
{% endblock %}
diff --git a/package.json b/package.json
index 3cd3ea52..1ee582e6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ffc-ahwr-backoffice",
- "version": "1.14.10",
+ "version": "1.15.0",
"description": "Back office of the health and welfare of your livestock",
"homepage": "https://github.com/DEFRA/ffc-ahwr-backoffice",
"main": "app/index.js",
diff --git a/test/integration/narrow/routes/withdraw-application.test.js b/test/integration/narrow/routes/withdraw-application.test.js
new file mode 100644
index 00000000..34883a55
--- /dev/null
+++ b/test/integration/narrow/routes/withdraw-application.test.js
@@ -0,0 +1,88 @@
+const cheerio = require('cheerio')
+const expectPhaseBanner = require('../../../utils/phase-banner-expect')
+const { administrator } = require('../../../../app/auth/permissions')
+const getCrumbs = require('../../../utils/get-crumbs')
+
+const applications = require('../../../../app/api/applications')
+jest.mock('../../../../app/api/applications')
+
+const reference = 'AHWR-555A-FD4C'
+
+applications.withdrawApplication = jest.fn().mockResolvedValue(true)
+
+describe('View Application test', () => {
+ let crumb
+ const url = '/withdraw-application/'
+ jest.mock('../../../../app/auth')
+ const auth = { strategy: 'session-auth', credentials: { scope: [administrator] } }
+
+ beforeEach(async () => {
+ crumb = await getCrumbs(global.__SERVER__)
+ jest.clearAllMocks()
+ })
+
+ describe(`POST ${url} route`, () => {
+ test('returns 302 no auth', async () => {
+ const options = {
+ method: 'POST',
+ url
+ }
+ const res = await global.__SERVER__.inject(options)
+ expect(res.statusCode).toBe(302)
+ })
+
+ test('returns 403', async () => {
+ const options = {
+ method: 'POST',
+ url,
+ auth,
+ payload: {
+ reference
+ }
+ }
+ const res = await global.__SERVER__.inject(options)
+ expect(res.statusCode).toBe(403)
+ const $ = cheerio.load(res.payload)
+ expect($('h1.govuk-heading-l').text()).toEqual('403 - Forbidden')
+ expectPhaseBanner.ok($)
+ })
+
+ test('Approve withdraw application', async () => {
+ const options = {
+ method: 'POST',
+ url,
+ auth,
+ headers: { cookie: `crumb=${crumb}` },
+ payload: {
+ reference,
+ withdrawConfirmation: 'yes',
+ page: 1,
+ crumb
+ }
+ }
+ const res = await global.__SERVER__.inject(options)
+ expect(applications.withdrawApplication).toHaveBeenCalledTimes(1)
+ expect(res.statusCode).toBe(302)
+ expect(res.headers.location).toEqual(`/view-application/${reference}?page=1`)
+ })
+
+ test('Cancel withdraw application', async () => {
+ const options = {
+ method: 'POST',
+ url,
+ auth,
+ headers: { cookie: `crumb=${crumb}` },
+ payload: {
+ reference,
+ withdrawConfirmation: 'no',
+ page: 1,
+ crumb
+ }
+ }
+ const res = await global.__SERVER__.inject(options)
+ expect(applications.withdrawApplication).toHaveBeenCalledTimes(0)
+ expect(res.statusCode).toBe(302)
+ expect(res.headers.location).toEqual(`/view-application/${reference}?page=1`)
+ })
+ })
+})
diff --git a/test/unit/api/applications.test.js b/test/unit/api/applications.test.js
index f9e47698..0011cb23 100644
--- a/test/unit/api/applications.test.js
+++ b/test/unit/api/applications.test.js
@@ -7,7 +7,7 @@ const limit = 20
const offset = 0
let searchText = ''
let searchType = ''
-const { getApplications, getApplication } = require('../../../app/api/applications')
+const { getApplications, getApplication, withdrawApplication } = require('../../../app/api/applications')
describe('Application API', () => {
it('GetApplications should return empty applications array', async () => {
jest.mock('@hapi/wreck')
@@ -144,4 +144,46 @@ describe('Application API', () => {
expect(Wreck.get).toHaveBeenCalledTimes(1)
expect(Wreck.get).toHaveBeenCalledWith(`${applicationApiUri}/application/get/${appRef}`, options)
})
+
+ it('WithdrawApplication should return false if api not available', async () => {
+ jest.mock('@hapi/wreck')
+ const options = {
+ payload: {
+ user: 'test',
+ status: 2
+ },
+ json: true
+ }
+ Wreck.put = jest.fn(async function (_url, _options) {
+ throw (new Error('fakeError'))
+ })
+ const response = await withdrawApplication(appRef, 'test', 2)
+ expect(response).toBe(false)
+ expect(Wreck.put).toHaveBeenCalledTimes(1)
+ expect(Wreck.put).toHaveBeenCalledWith(`${applicationApiUri}/application/${appRef}`, options)
+ })
+
+ it('WithdrawApplication should return true after successful API request', async () => {
+ jest.mock('@hapi/wreck')
+ const options = {
+ payload: {
+ user: 'test',
+ status: 2
+ },
+ json: true
+ }
+ const wreckResponse = {
+ res: {
+ statusCode: 200
+ }
+ }
+
+ Wreck.put = jest.fn(async function (_url, _options) {
+ return wreckResponse
+ })
+ const response = await withdrawApplication(appRef, 'test', 2)
+ expect(response).toBe(true)
+ expect(Wreck.put).toHaveBeenCalledTimes(1)
+ expect(Wreck.put).toHaveBeenCalledWith(`${applicationApiUri}/application/${appRef}`, options)
+ })
})