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) + }) })