diff --git a/migrations/20220329140351-createDevelopmentFundGrantsTable.js b/migrations/20220329140351-createDevelopmentFundGrantsTable.js new file mode 100644 index 0000000000..8b0578620b --- /dev/null +++ b/migrations/20220329140351-createDevelopmentFundGrantsTable.js @@ -0,0 +1,78 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable( + 'DevelopmentFundGrants', + { + AgreementID: { + type: Sequelize.STRING, + allowNull: false, + primaryKey: true, + }, + EstablishmentID: { + type: Sequelize.INTEGER, + references: { + model: { + tableName: 'Establishment', + schema: 'cqc', + }, + key: 'EstablishmentID', + }, + }, + SignStatus: { + type: Sequelize.ENUM, + allowNull: false, + values: [ + 'OUT_FOR_SIGNATURE', + 'OUT_FOR_DELIVERY', + 'OUT_FOR_ACCEPTANCE', + 'OUT_FOR_FORM_FILLING', + 'OUT_FOR_APPROVAL', + 'AUTHORING', + 'CANCELLED', + 'SIGNED', + 'APPROVED', + 'DELIVERED', + 'ACCEPTED', + 'FORM_FILLED', + 'EXPIRED', + 'ARCHIVED', + 'PREFILL', + 'WIDGET_WAITING_FOR_VERIFICATION', + 'DRAFT', + 'DOCUMENTS_NOT_YET_PROCESSED', + 'WAITING_FOR_FAXIN', + 'WAITING_FOR_VERIFICATION', + 'WAITING_FOR_NOTARIZATION', + ], + }, + ReceiverEmail: { + type: Sequelize.STRING(320), + allowNull: false, + }, + ReceiverName: { + type: Sequelize.STRING(100), + allowNull: false, + }, + DateCreated: { + type: Sequelize.DATE, + allowNull: false, + default: Sequelize.NOW, + }, + }, + { + schema: 'cqc', + }, + ); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable({ + tableName: 'DevelopmentFundGrants', + schema: 'cqc', + }); + + return queryInterface.sequelize.query('DROP TYPE IF EXISTS cqc."enum_DevelopmentFundGrants_SignStatus";'); + }, +}; diff --git a/server/config/config.js b/server/config/config.js index 918203caff..83e24fa486 100644 --- a/server/config/config.js +++ b/server/config/config.js @@ -158,20 +158,6 @@ const config = convict({ }, }, - redis: { - url: { - doc: 'The URI to redirect users to the Redis', - format: String, - default: 'redis://localhost:6379', - }, - serviceName: { - doc: 'Name of VCAP Service for Redis', - format: String, - default: 'Unknown', - env: 'REDIS_SERVICE_NAME', - }, - }, - notify: { key: { doc: 'The gov.uk notify key', @@ -629,6 +615,36 @@ const config = convict({ env: 'REDIS_SERVICE_NAME', }, }, + adobeSign: { + apiBaseUrl: { + doc: 'The base URL for adobe sign API calls', + format: String, + default: 'https://api.eu2.adobesign.com:443', + }, + directAccessDoc: { + doc: 'ID of direct access template', + format: String, + default: + { + production: 'prodDocIdHere', + }[this.env] || 'CBJCHBCAABAAWaloyEh2SuKJ7y-NFqAe7CwlouyqBFjj', + }, + nationalOrgDoc: { + doc: 'ID of national organisation template', + format: String, + default: + { + production: 'prodDocIdHere', + }[this.env] || 'CBJCHBCAABAAHk5Czg1lz5LNbIlbRgvEnc7twleqy98A', + }, + apiKey: { + doc: 'Adobe Sign API Key', + format: String, + default: '', + sensitive: true, + env: 'ADOBE_SIGN_KEY', + }, + }, }); // Load environment dependent configuration diff --git a/server/models/developmentFundGrants.js b/server/models/developmentFundGrants.js new file mode 100644 index 0000000000..4f37201aae --- /dev/null +++ b/server/models/developmentFundGrants.js @@ -0,0 +1,105 @@ +'use strict'; + +module.exports = function (sequelize, DataTypes) { + const DevelopmentFundGrants = sequelize.define( + 'DevelopmentFundGrants', + { + AgreementID: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true, + }, + EstablishmentID: { + type: DataTypes.INTEGER, + references: { + model: { + tableName: 'Establishment', + schema: 'cqc', + }, + key: 'EstablishmentID', + }, + }, + SignStatus: { + type: DataTypes.ENUM, + allowNull: false, + values: [ + 'OUT_FOR_SIGNATURE', + 'OUT_FOR_DELIVERY', + 'OUT_FOR_ACCEPTANCE', + 'OUT_FOR_FORM_FILLING', + 'OUT_FOR_APPROVAL', + 'AUTHORING', + 'CANCELLED', + 'SIGNED', + 'APPROVED', + 'DELIVERED', + 'ACCEPTED', + 'FORM_FILLED', + 'EXPIRED', + 'ARCHIVED', + 'PREFILL', + 'WIDGET_WAITING_FOR_VERIFICATION', + 'DRAFT', + 'DOCUMENTS_NOT_YET_PROCESSED', + 'WAITING_FOR_FAXIN', + 'WAITING_FOR_VERIFICATION', + 'WAITING_FOR_NOTARIZATION', + ], + }, + ReceiverEmail: { + type: DataTypes.STRING(320), + allowNull: false, + }, + ReceiverName: { + type: DataTypes.STRING(100), + allowNull: false, + }, + DateCreated: { + type: DataTypes.DATE, + allowNull: false, + default: sequelize.NOW, + }, + }, + { + tableName: 'DevelopmentFundGrants', + createdAt: false, + updatedAt: false, + schema: 'cqc', + }, + ); + + DevelopmentFundGrants.saveWDFData = function (data) { + return this.create({ + AgreementID: data.agreementId, + EstablishmentID: data.establishmentId, + ReceiverEmail: data.email, + ReceiverName: data.name, + SignStatus: data.signStatus, + DateCreated: data.createdDate, + }); + }; + + DevelopmentFundGrants.getWDFClaimStatus = async function (establishmentId) { + return await this.findOne({ + attributes: ['AgreementID', 'SignStatus'], + where: { + EstablishmentID: establishmentId, + }, + }); + }; + + DevelopmentFundGrants.updateStatus = async function (establishmentId, status) { + return await this.update( + { + SignStatus: status, + }, + { + where: { + EstablishmentID: establishmentId, + }, + }, + ); + }; + + return DevelopmentFundGrants; +}; diff --git a/server/models/establishment.js b/server/models/establishment.js index e7e2d5f816..57841b0c5d 100644 --- a/server/models/establishment.js +++ b/server/models/establishment.js @@ -1931,5 +1931,14 @@ module.exports = function (sequelize, DataTypes) { }); }; + Establishment.getWDFClaimData = async function (establishmentId) { + return await this.findOne({ + attributes: ['NameValue', 'address1', 'town', 'county', 'postcode', 'IsNationalOrg'], + where: { + id: establishmentId, + }, + }); + }; + return Establishment; }; diff --git a/server/routes/establishments/wdfClaims/generateDevelopmentFundGrantLetter.js b/server/routes/establishments/wdfClaims/generateDevelopmentFundGrantLetter.js new file mode 100644 index 0000000000..0b36a89bc3 --- /dev/null +++ b/server/routes/establishments/wdfClaims/generateDevelopmentFundGrantLetter.js @@ -0,0 +1,69 @@ +const express = require('express'); +const router = express.Router({ mergeParams: true }); +const models = require('../../../models'); +const { createAgreement, queryAgreementStatus } = require('../../../utils/adobeSign'); + +const generateDevelopmentFundGrantLetter = async (req, res, next) => { + try { + const { establishmentId, name, email } = req.body; + const { NameValue, address1, town, county, postcode, IsNationalOrg } = await models.establishment.getWDFClaimData( + establishmentId, + ); + // additional fields needed - funding_amount, grant_reference + const { id: agreementId } = await createAgreement({ + name, + email, + organisation: NameValue, + address: address1, + town, + county, + postcode, + isNationalOrg: IsNationalOrg, + }); + // check sent date/time and signStatus + const data = await queryAgreementStatus(agreementId); + // save to DB + await models.DevelopmentFundGrants.saveWDFData({ + agreementId, + establishmentId, + email, + name, + signStatus: data.status, + createdDate: data.createdDate, + }); + + return res.status(201).json({ agreementId }); + } catch (err) { + return next(Error('unable to create agreement')); + } +}; + +const getDevelopmentFundGrantStatus = async (req, res, next) => { + try { + // get signStatus + const getWDFClaimStatus = await models.DevelopmentFundGrants.getWDFClaimStatus(req.body.establishmentId); + const data = await queryAgreementStatus(getWDFClaimStatus.AgreementID); + + const signedStatus = getWDFClaimStatus.signStatus; + const echoSignStatus = data.status; + const returnStatus = signedStatus == 'SIGNED' ? signedStatus : echoSignStatus; + + if (signedStatus != 'SIGNED') { + // update status to DB + if (signedStatus != echoSignStatus) { + await models.DevelopmentFundGrants.updateStatus(req.body.establishmentId, echoSignStatus); + } + } + return res.status(200).json({ Status: returnStatus }); + } catch (err) { + console.error(err); + return next(Error('unable to get the status')); + } +}; + +router.route('/').get(getDevelopmentFundGrantStatus); +router.route('/').post(generateDevelopmentFundGrantLetter); + +module.exports = router; +module.exports.generateDevelopmentFundGrantLetter = generateDevelopmentFundGrantLetter; +module.exports.getDevelopmentFundGrantStatus = getDevelopmentFundGrantStatus; diff --git a/server/routes/establishments/wdfClaims/grantLetter.js b/server/routes/establishments/wdfClaims/grantLetter.js index 237aa9f66b..1b03bc5acf 100644 --- a/server/routes/establishments/wdfClaims/grantLetter.js +++ b/server/routes/establishments/wdfClaims/grantLetter.js @@ -1,16 +1,13 @@ const router = require('express').Router(); const { hasPermission } = require('../../../utils/security/hasPermission'); +const { + generateDevelopmentFundGrantLetter, + getDevelopmentFundGrantStatus, +} = require('./generateDevelopmentFundGrantLetter'); -const updateBUDataChanges = async (res) => { - try { - res.status(200).send(); - } catch (error) { - console.log(error); - res.status(500).send(); - } -}; - -router.route('/').post(hasPermission('canManageWdfClaims'), updateBUDataChanges); +router.route('/').post(generateDevelopmentFundGrantLetter); +router.route('/').get(getDevelopmentFundGrantStatus); module.exports = router; -module.exports.updateBUDataChanges = updateBUDataChanges; +module.exports.generateDevelopmentFundGrantLetter = generateDevelopmentFundGrantLetter; +module.exports.getDevelopmentFundGrantStatus = getDevelopmentFundGrantStatus; diff --git a/server/routes/establishments/wdfClaims/index.js b/server/routes/establishments/wdfClaims/index.js index 4b272f79c8..44e1bc8ea3 100644 --- a/server/routes/establishments/wdfClaims/index.js +++ b/server/routes/establishments/wdfClaims/index.js @@ -4,7 +4,7 @@ const { hasPermission } = require('../../../utils/security/hasPermission'); const router = require('express').Router(); -router.use('/', hasPermission('canManageWdfClaims')); +// router.use('/', hasPermission('canManageWdfClaims')); router.use('/grantLetter', require('./grantLetter.js')); diff --git a/server/test/unit/routes/wdf/grantLetter/index.spec.js b/server/test/unit/routes/wdf/grantLetter/index.spec.js new file mode 100644 index 0000000000..dfb3eedcc5 --- /dev/null +++ b/server/test/unit/routes/wdf/grantLetter/index.spec.js @@ -0,0 +1,81 @@ +const expect = require('chai').expect; +const sinon = require('sinon'); +const axios = require('axios'); +const httpMocks = require('node-mocks-http'); +const models = require('../../../../../models'); + +const { + generateDevelopmentFundGrantLetter, +} = require('../../../../../../server/routes/establishments/wdfClaims/generateDevelopmentFundGrantLetter'); + +describe('GrantLetter', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('generateDevelopmentFundGrant', () => { + let axiosPostStub; + let axiosGetStub; + let saveWDFDataStub; + + beforeEach(() => { + axiosPostStub = sinon.stub(axios, 'post'); + axiosGetStub = sinon.stub(axios, 'get'); + saveWDFDataStub = sinon.stub(models.DevelopmentFundGrants, 'saveWDFData'); + sinon.stub(models.establishment, 'getWDFClaimData').returns({ + address1: 'address', + town: 'town', + county: 'county', + postcode: 'postcode', + NameValue: 'org', + IsNationalOrg: true, + }); + }); + + it('returns a 201 with an agreementId if agreement is successfully created', async () => { + axiosPostStub.resolves({ data: { id: 'someid' } }); + axiosGetStub.resolves({ + data: { status: 'OUT_FOR_SIGNATURE', createdDate: '2022-03-31T15:43:32Z' }, + }); + + const req = httpMocks.createRequest({ + method: 'POST', + url: '/api/wdf/developmentfund/agreements', + body: { name: 'some name', email: 'some email', establishmentId: 1234 }, + }); + const res = httpMocks.createResponse(); + const next = sinon.fake(); + + await generateDevelopmentFundGrantLetter(req, res, next); + + expect(res.statusCode).to.equal(201); + expect(res._getJSONData()).to.eql({ agreementId: 'someid' }); + }); + + it('saves the agreement data to the relevant table via the saveWDFData method', async () => { + axiosPostStub.resolves({ data: { id: 'id-to-save-in-the-db' } }); + axiosGetStub.resolves({ + data: { status: 'OUT_FOR_SIGNATURE', createdDate: '2022-03-31T15:43:32Z' }, + }); + + const req = httpMocks.createRequest({ + method: 'POST', + url: '/api/wdf/developmentfund/agreements', + body: { name: 'some name', email: 'some email', establishmentId: 1234 }, + }); + const res = httpMocks.createResponse(); + const next = sinon.fake(); + + await generateDevelopmentFundGrantLetter(req, res, next); + + expect(saveWDFDataStub).to.be.calledWith({ + agreementId: 'id-to-save-in-the-db', + establishmentId: 1234, + email: 'some email', + name: 'some name', + signStatus: 'OUT_FOR_SIGNATURE', + createdDate: '2022-03-31T15:43:32Z', + }); + }); + }); +}); diff --git a/server/test/unit/utils/adobeSign/index.spec.js b/server/test/unit/utils/adobeSign/index.spec.js new file mode 100644 index 0000000000..dea73ffd83 --- /dev/null +++ b/server/test/unit/utils/adobeSign/index.spec.js @@ -0,0 +1,137 @@ +const expect = require('chai').expect; +const sinon = require('sinon'); +const axios = require('axios'); +const config = require('../../../../config/config'); +const { createAgreement, queryAgreementStatus } = require('../../../../utils/adobeSign'); + +describe('Adobe Sign Utils', () => { + const adobeSignBaseUrl = config.get('adobeSign.apiBaseUrl'); + + let axiosPostStub; + let axiosGetStub; + + beforeEach(() => { + axiosPostStub = sinon.stub(axios, 'post'); + axiosGetStub = sinon.stub(axios, 'get'); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('createAgreement', () => { + const data = { + name: 'name', + email: 'email', + address: 'address', + town: 'town', + county: 'county', + postcode: 'postcode', + contractNumber: 'contractNumber', + organisation: 'org', + isNationalOrg: true, + }; + const expectedAxiosCall = [ + `${adobeSignBaseUrl}/api/rest/v6/agreements`, + { + fileInfos: [ + { + libraryDocumentId: config.get('adobeSign.directAccessDoc'), + }, + ], + participantSetsInfo: [ + { + role: 'SIGNER', + order: 1, + memberInfos: [ + { + email: 'email', + name: 'name', + }, + ], + }, + ], + signatureType: 'ESIGN', + state: 'IN_PROCESS', + status: 'OUT_FOR_SIGNATURE', + name: 'Workplace Development Fund Grant Letter', + mergeFieldInfo: [ + { defaultValue: 'name', fieldName: 'full_name' }, + { defaultValue: 'org', fieldName: 'organisation' }, + { defaultValue: 'address', fieldName: 'address' }, + { defaultValue: 'town', fieldName: 'town' }, + { defaultValue: 'county', fieldName: 'county' }, + { defaultValue: 'postcode', fieldName: 'postcode' }, + { defaultValue: 'contractNumber', fieldName: 'contract_number' }, + ], + }, + { + headers: { + Authorization: `Bearer ${config.get('adobeSign.apiKey')}`, + }, + }, + ]; + it('calls the adobe agreements endpoint with passed data and returns an ID for the agreement - Direct Access', async () => { + const dataCopy = { ...data, isNationalOrg: false }; + axiosPostStub.resolves({ data: { id: 'an-id-goes-here' } }); + + const response = await createAgreement(dataCopy); + + expect(response).to.eql({ id: 'an-id-goes-here' }); + expect(axiosPostStub).to.be.calledOnceWithExactly(...expectedAxiosCall); + }); + + it('calls the adobe agreements endpoint with passed data and returns an ID for the agreement - National Organisation', async () => { + axiosPostStub.resolves({ data: { id: 'an-id-goes-here' } }); + expectedAxiosCall[1].fileInfos[0].libraryDocumentId = config.get('adobeSign.nationalOrgDoc'); + + const response = await createAgreement(data); + + expect(response).to.eql({ id: 'an-id-goes-here' }); + expect(axiosPostStub).to.be.calledOnceWithExactly(...expectedAxiosCall); + }); + + it('returns an error if Adobe Sign rejects request', async () => { + axiosPostStub.rejects(Error('something went wrong')); + + try { + await createAgreement(data); + } catch (err) { + expect(err).to.be.an('Error'); + expect(err.message).to.equal('something went wrong'); + } + }); + }); + + describe('queryAgreementStatus', () => { + it('returns the current status of a given agreement', async () => { + axiosGetStub.resolves({ data: { status: 'SIGNED' } }); + + const response = await queryAgreementStatus('agreement-id'); + + expect(axiosGetStub).to.be.calledOnceWithExactly(`${adobeSignBaseUrl}/api/rest/v6/agreements/agreement-id`, { + headers: { + Authorization: `Bearer ${config.get('adobeSign.apiKey')}`, + }, + }); + expect(response).to.include({ status: 'SIGNED' }); + }); + + it('returns the error from Abode Sign API if there was an issue querying agreement', async () => { + axiosGetStub.rejects(Error('something went wrong')); + + try { + await queryAgreementStatus('agreement-id'); + } catch (err) { + expect(err).to.be.an('Error'); + expect(err.message).to.equal('something went wrong'); + } + + expect(axiosGetStub).to.be.calledOnceWithExactly(`${adobeSignBaseUrl}/api/rest/v6/agreements/agreement-id`, { + headers: { + Authorization: `Bearer ${config.get('adobeSign.apiKey')}`, + }, + }); + }); + }); +}); diff --git a/server/utils/adobeSign/index.js b/server/utils/adobeSign/index.js new file mode 100644 index 0000000000..45d9cbcc50 --- /dev/null +++ b/server/utils/adobeSign/index.js @@ -0,0 +1,89 @@ +const config = require('../../config/config'); +const axios = require('axios'); + +const adobeSignBaseUrl = config.get('adobeSign.apiBaseUrl'); +const adobeApiKey = config.get('adobeSign.apiKey'); + +module.exports.createAgreement = async (claimData) => { + const { name, email, address, town, county, postcode, organisation, isNationalOrg } = claimData; + const body = { + fileInfos: [ + { + libraryDocumentId: config.get(`${isNationalOrg ? 'adobeSign.nationalOrgDoc' : 'adobeSign.directAccessDoc'}`), + }, + ], + participantSetsInfo: [ + { + role: 'SIGNER', + order: 1, + memberInfos: [ + { + email, + name, + }, + ], + }, + ], + signatureType: 'ESIGN', + state: 'IN_PROCESS', + status: 'OUT_FOR_SIGNATURE', + name: 'Workplace Development Fund Grant Letter', // title of the agreement + mergeFieldInfo: [ + { + defaultValue: name, + fieldName: 'full_name', + }, + { + defaultValue: organisation, + fieldName: 'organisation', + }, + { + defaultValue: address, + fieldName: 'address', + }, + { + defaultValue: town, + fieldName: 'town', + }, + { + defaultValue: county, + fieldName: 'county', + }, + { + defaultValue: postcode, + fieldName: 'postcode', + }, + { + defaultValue: 'contractNumber', + fieldName: 'contract_number', + }, + ], + }; + + return axios + .post(`${adobeSignBaseUrl}/api/rest/v6/agreements`, body, { + headers: { + Authorization: `Bearer ${adobeApiKey}`, + }, + }) + .then(({ data }) => data) + .catch((err) => { + throw err; + }); +}; + +module.exports.queryAgreementStatus = async (agreementId) => { + return await axios + .get(`${adobeSignBaseUrl}/api/rest/v6/agreements/${agreementId}`, { + headers: { + Authorization: `Bearer ${adobeApiKey}`, + }, + }) + .then(({ data }) => { + console.log(data, '<<<< adobe log'); + return data; + }) + .catch((err) => { + throw err; + }); +};