From 927a1d7e06018505ffd371b07b4af892c377ce7c Mon Sep 17 00:00:00 2001 From: Jason Claxton <30830544+Jozzey@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:18:09 +0100 Subject: [PATCH] Update existing return versions when a new one is added (#1216) https://eaflood.atlassian.net/browse/WATER-4568 Having worked out how to convert the information in the return requirements setup session into the various records in the DB (see https://github.com/DEFRA/water-abstraction-system/pull/1137) we now need to deal with how the new record impacts the existing ones. This PR will extend the persisting logic to deal with changes to the existing return versions. --- .../generate-return-version.service.js | 20 +- .../persist-return-version.service.js | 3 +- ...rocess-existing-return-versions.service.js | 230 ++++++++++++++++++ .../generate-return-version.service.test.js | 18 +- ...s-existing-return-versions.service.test.js | 114 +++++++++ .../submit-check.service.test.js | 1 + 6 files changed, 370 insertions(+), 16 deletions(-) create mode 100644 app/services/return-requirements/process-existing-return-versions.service.js create mode 100644 test/services/return-requirements/process-existing-return-versions.service.test.js diff --git a/app/services/return-requirements/generate-return-version.service.js b/app/services/return-requirements/generate-return-version.service.js index 97e4bf495d..1294e9c618 100644 --- a/app/services/return-requirements/generate-return-version.service.js +++ b/app/services/return-requirements/generate-return-version.service.js @@ -6,6 +6,7 @@ */ const GenerateReturnVersionRequirementsService = require('./generate-return-version-requirements.service.js') +const ProcessExistingReturnVersionsService = require('./process-existing-return-versions.service.js') const ReturnVersionModel = require('../../models/return-version.model.js') /** @@ -20,7 +21,9 @@ const ReturnVersionModel = require('../../models/return-version.model.js') * @returns {Promise} The new return version and requirement data for a licence */ async function go (sessionData, userId) { - const returnVersion = await _generateReturnVersion(sessionData, userId) + const returnVersionsExist = sessionData.licence.returnVersions.length > 0 + + const returnVersion = await _generateReturnVersion(returnVersionsExist, sessionData, userId) const returnRequirements = await _generateReturnRequirements(sessionData) return { @@ -36,7 +39,7 @@ function _calculateStartDate (sessionData) { return new Date(sessionData.startDateYear, sessionData.startDateMonth - 1, sessionData.startDateDay) } - return sessionData.licence.currentVersionStartDate + return new Date(sessionData.licence.currentVersionStartDate) } async function _generateReturnRequirements (sessionData) { @@ -53,15 +56,22 @@ async function _generateReturnRequirements (sessionData) { return returnRequirements } -async function _generateReturnVersion (sessionData, userId) { +async function _generateReturnVersion (returnVersionsExist, sessionData, userId) { + const startDate = _calculateStartDate(sessionData) + let endDate = null + + if (returnVersionsExist) { + endDate = await ProcessExistingReturnVersionsService.go(sessionData.licence.id, startDate) + } + return { createdBy: userId, - endDate: null, + endDate, licenceId: sessionData.licence.id, multipleUpload: _multipleUpload(sessionData?.additionalSubmissionOptions), notes: sessionData?.note?.content, reason: sessionData.reason, - startDate: _calculateStartDate(sessionData), + startDate, status: 'current', version: await _nextVersionNumber(sessionData.licence.id) } diff --git a/app/services/return-requirements/persist-return-version.service.js b/app/services/return-requirements/persist-return-version.service.js index 49d02e3275..dbcb8bc3c1 100644 --- a/app/services/return-requirements/persist-return-version.service.js +++ b/app/services/return-requirements/persist-return-version.service.js @@ -22,7 +22,7 @@ const ReturnVersionModel = require('../../models/return-version.model.js') async function go (returnVersionData) { const { returnRequirements, returnVersion } = returnVersionData - const { id: returnVersionId } = await ReturnVersionModel.query().insert(returnVersion).returning('id') + const { id: returnVersionId } = await ReturnVersionModel.query().insert(returnVersion) await _persistReturnRequirements(returnRequirements, returnVersionId) } @@ -48,7 +48,6 @@ async function _persistReturnRequirements (returnRequirements, returnVersionId) summer: returnRequirement.summer, twoPartTariff: returnRequirement.twoPartTariff }) - .returning('id') await _persistReturnRequirementsPoints(returnRequirement.returnRequirementPoints, returnRequirementId) await _persistReturnRequirementsPurposes(returnRequirement.returnRequirementPurposes, returnRequirementId) diff --git a/app/services/return-requirements/process-existing-return-versions.service.js b/app/services/return-requirements/process-existing-return-versions.service.js new file mode 100644 index 0000000000..c8c8f23659 --- /dev/null +++ b/app/services/return-requirements/process-existing-return-versions.service.js @@ -0,0 +1,230 @@ +'use strict' + +/** + * Processes existing return versions to update the their `status` and `endDate` when a new return version is created + * @module ProcessExistingReturnVersionsService + */ + +const ReturnVersionModel = require('../../models/return-version.model.js') + +/** + * Processes existing return versions to update the their `status` and `endDate` when a new return version is created + * + * Depending on the `startDate` of the new return version that is to be inserted. An existing return version may need + * its `status` or `endDate` to be updated. An `endDate` may also need to be calculated for the new return version if + * it is to be inserted between existing return versions, or is superseding an existing one that has an `endDate`. + * + * @param {String} licenceId - The UUID of the licence the requirements are for + * @param {Date} newVersionStartDate - The date that the new return version starts + * + * @returns {Promise} The calculated `endDate` for the new return version if there is one. Null will be returned + * if there is no `endDate` + */ +async function go (licenceId, newVersionStartDate) { + const previousVersions = await _previousVersions(licenceId) + const previousVersionEndDate = _previousVersionEndDate(newVersionStartDate) + + let result + + result = await _endLatestVersion(previousVersions, newVersionStartDate, previousVersionEndDate) + if (result) { + return null + } + + result = await _insertBetweenVersions(previousVersions, newVersionStartDate, previousVersionEndDate) + if (result) { + return result + } + + result = await _replaceLatestVersion(previousVersions, newVersionStartDate) + if (result) { + return null + } + + result = await _replacePreviousVersion(previousVersions, newVersionStartDate) + if (result) { + return result + } + + return null +} + +/** + * Update the end date of the latest return version whose start data is less than the new one and whose end date is null + * + * For example, imagine these are the existing return versions + * + * | Id | Start date | End date | Status | + * |----|------------|------------|---------| + * | 1 | 2008-04-01 | 2019-05-12 | current | + * | 2 | 2019-05-13 | 2022-03-31 | current | + * | 3 | 2022-04-01 | | current | + * + * The user adds a new return version staring 2024-08-01. The end result would be + * + * | Id | Start date | End date | Status | + * |----|------------|------------|---------| + * | 1 | 2008-04-01 | 2019-05-12 | current | + * | 2 | 2019-05-13 | 2022-03-31 | current | + * | 3 | 2022-04-01 | 2024-07-31 | current | + * | 4 | 2022-08-01 | | current | + * + * This function finds return version **3** and updates its end date to the start date of the new return version minus + * 1 day. We don't care about the end date because the new return version doesn't need one. + */ +async function _endLatestVersion (previousVersions, newVersionStartDate, endDate) { + const matchedReturnVersion = previousVersions.find((previousVersion) => { + return previousVersion.startDate < newVersionStartDate && + previousVersion.endDate === null + }) + + if (!matchedReturnVersion) { + return null + } + + return matchedReturnVersion.$query().patch({ endDate }) +} + +/** + * Update the end date of a previous version whose start date is less than the new one and whose end date is greater + * + * For example, imagine these are the existing return versions + * + * | Id | Start date | End date | Status | + * |----|------------|------------|---------| + * | 1 | 2008-04-01 | 2019-05-12 | current | + * | 2 | 2019-05-13 | 2022-03-31 | current | + * | 3 | 2022-04-01 | | current | + * + * The user adds a new return version staring 2021-07-01. The end result would be + * + * | Id | Start date | End date | Status | + * |----|------------|------------|---------| + * | 1 | 2008-04-01 | 2019-05-12 | current | + * | 2 | 2019-05-13 | 2021-06-30 | current | + * | 4 | 2021-07-01 | 2022-03-31 | current | + * | 3 | 2022-04-01 | | current | + * + * This function finds return version **2** and updates its end date to the start date of the new return version minus + * 1 day. We also return the end date from version **2** as this needs to be applied to the new return version. Hence, + * we are _inserting between versions_. + */ +async function _insertBetweenVersions (previousVersions, newVersionStartDate, endDate) { + const matchedReturnVersion = previousVersions.find((previousVersion) => { + return previousVersion.startDate < newVersionStartDate && + previousVersion.endDate > newVersionStartDate + }) + + if (!matchedReturnVersion) { + return null + } + + const newVersionEndDate = matchedReturnVersion.endDate + + await matchedReturnVersion.$query().patch({ endDate }) + + return newVersionEndDate +} + +function _previousVersionEndDate (newVersionStartDate) { + // NOTE: You have to create a new date from newVersionStartDate else when we call setDate we amend the source + // newVersionStartDate passed to the service. + const previousVersionEndDate = new Date(newVersionStartDate) + + previousVersionEndDate.setDate(previousVersionEndDate.getDate() - 1) + + return previousVersionEndDate +} + +function _previousVersions (licenceId) { + return ReturnVersionModel.query() + .select(['endDate', 'id', 'startDate']) + .where('licenceId', licenceId) + .where('status', 'current') + .orderBy('startDate', 'desc') +} + +/** + * Update the status of a previous version whose start date is equal to the new one and whose end date is null + * + * For example, imagine these are the existing return versions + * + * | Id | Start date | End date | Status | + * |----|------------|------------|---------| + * | 1 | 2008-04-01 | 2019-05-12 | current | + * | 2 | 2019-05-13 | 2022-03-31 | current | + * | 3 | 2022-04-01 | | current | + * + * The user adds a new return version staring 2022-04-01. The end result would be + * + * | Id | Start date | End date | Status | + * |----|------------|------------|------------| + * | 1 | 2008-04-01 | 2019-05-12 | current | + * | 2 | 2019-05-13 | 2022-03-31 | current | + * | 3 | 2022-04-01 | | superseded | + * | 4 | 2022-04-01 | | current | + * + * This function finds return version **3** and updates its status to `superseded`. We don't care about the end date + * because the new return version doesn't need one. + */ +async function _replaceLatestVersion (previousVersions, newVersionStartDate) { + const matchedReturnVersion = previousVersions.find((previousVersion) => { + // NOTE: When you use the equality operator JavaScript will check for reference equality. Dates being objects this + // will always return false, even though they refer to the exact same time. This means you need to convert them to + // a more primitive value like a string or number first. `getTime()` seems to be the winner according to + // stack overflow https://stackoverflow.com/a/4587089/6117745 + return previousVersion.startDate.getTime() === newVersionStartDate.getTime() && + previousVersion.endDate === null + }) + + if (!matchedReturnVersion) { + return null + } + + return matchedReturnVersion.$query().patch({ status: 'superseded' }) +} + +/** + * Update the status of a previous version whose start date is equal to the new one and whose end date is not null + * + * For example, imagine these are the existing return versions + * + * | Id | Start date | End date | Status | + * |----|------------|------------|---------| + * | 1 | 2008-04-01 | 2019-05-12 | current | + * | 2 | 2019-05-13 | 2022-03-31 | current | + * | 3 | 2022-04-01 | | current | + * + * The user adds a new return version staring 2019-05-13. The end result would be + * + * | Id | Start date | End date | Status | + * |----|------------|------------|------------| + * | 1 | 2008-04-01 | 2019-05-12 | current | + * | 2 | 2019-05-13 | 2022-03-31 | superseded | + * | 4 | 2019-05-13 | 2022-03-31 | current | + * | 3 | 2022-04-01 | | current | + * + * This function finds return version **2** and updates its status to `superseded`. We also return the end date from + * version **2** as this needs to be applied to the new return version as its end date. Hence, we are _replacing a + * previous version_. + */ +async function _replacePreviousVersion (previousVersions, newVersionStartDate) { + const matchedReturnVersion = previousVersions.find((previousVersion) => { + return previousVersion.startDate.getTime() === newVersionStartDate.getTime() && + previousVersion.endDate > newVersionStartDate + }) + + if (!matchedReturnVersion) { + return null + } + + const newVersionEndDate = matchedReturnVersion.endDate + + await matchedReturnVersion.$query().patch({ status: 'superseded' }) + + return newVersionEndDate +} + +module.exports = { + go +} diff --git a/test/services/return-requirements/generate-return-version.service.test.js b/test/services/return-requirements/generate-return-version.service.test.js index 186a3544f3..65308b868b 100644 --- a/test/services/return-requirements/generate-return-version.service.test.js +++ b/test/services/return-requirements/generate-return-version.service.test.js @@ -14,6 +14,7 @@ const ReturnVersionHelper = require('../../support/helpers/return-version.helper // Things we need to stub const GenerateReturnVersionRequirementsService = require('../../../app/services/return-requirements/generate-return-version-requirements.service.js') +const ProcessExistingReturnVersionsService = require('../../../app/services/return-requirements/process-existing-return-versions.service.js') // Thing under test const GenerateReturnVersionService = require('../../../app/services/return-requirements/generate-return-version.service.js') @@ -35,10 +36,6 @@ describe('Generate Return Version service', () => { describe('when called with the minimum possible session data and previous return versions exist', () => { beforeEach(async () => { licenceId = generateUUID() - - await ReturnVersionHelper.add({ licenceId, version: 100 }) - await ReturnVersionHelper.add({ licenceId, version: 102 }) - sessionData = { setup: 'use-existing-requirements', reason: 'minor-change', @@ -67,6 +64,11 @@ describe('Generate Return Version service', () => { checkPageVisited: true, startDateOptions: 'licenceStartDate' } + + await ReturnVersionHelper.add({ licenceId, version: 100 }) + await ReturnVersionHelper.add({ licenceId, version: 102 }) + + Sinon.stub(ProcessExistingReturnVersionsService, 'go').resolves('2024-04-01T00:00:00.000Z') }) it('generates the data required to populate a record in the "return_version" table', async () => { @@ -74,12 +76,12 @@ describe('Generate Return Version service', () => { expect(result.returnRequirements).to.equal('return requirements data') expect(result.returnVersion.createdBy).to.equal(userId) - expect(result.returnVersion.endDate).to.be.null() + expect(result.returnVersion.endDate).to.equal('2024-04-01T00:00:00.000Z') expect(result.returnVersion.licenceId).to.equal(licenceId) expect(result.returnVersion.multipleUpload).to.be.false() expect(result.returnVersion.notes).to.be.undefined() expect(result.returnVersion.reason).to.equal(sessionData.reason) - expect(result.returnVersion.startDate).to.equal(sessionData.licence.currentVersionStartDate) + expect(result.returnVersion.startDate).to.equal(new Date(sessionData.licence.currentVersionStartDate)) expect(result.returnVersion.status).to.equal('current') // Version number is 103 because this is the next version number after the previous version expect(result.returnVersion.version).to.equal(103) @@ -89,7 +91,6 @@ describe('Generate Return Version service', () => { describe('when called with the maximum possible session data and no previous return versions exist', () => { beforeEach(async () => { licenceId = generateUUID() - sessionData = { note: { content: 'This is a test note', @@ -140,7 +141,6 @@ describe('Generate Return Version service', () => { describe('when called with session data from the "no-returns-required" journey', () => { beforeEach(async () => { licenceId = generateUUID() - sessionData = { reason: 'returns-exception', journey: 'no-returns-required', @@ -169,7 +169,7 @@ describe('Generate Return Version service', () => { expect(result.returnVersion.multipleUpload).to.be.false() expect(result.returnVersion.notes).to.be.undefined() expect(result.returnVersion.reason).to.equal(sessionData.reason) - expect(result.returnVersion.startDate).to.equal(sessionData.licence.currentVersionStartDate) + expect(result.returnVersion.startDate).to.equal(new Date(sessionData.licence.currentVersionStartDate)) expect(result.returnVersion.status).to.equal('current') expect(result.returnVersion.version).to.equal(1) }) diff --git a/test/services/return-requirements/process-existing-return-versions.service.test.js b/test/services/return-requirements/process-existing-return-versions.service.test.js new file mode 100644 index 0000000000..ffd249c73d --- /dev/null +++ b/test/services/return-requirements/process-existing-return-versions.service.test.js @@ -0,0 +1,114 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') + +const { describe, it, beforeEach } = exports.lab = Lab.script() +const { expect } = Code + +// Test helpers +const { generateUUID } = require('../../../app/lib/general.lib.js') +const ReturnVersionHelper = require('../../support/helpers/return-version.helper.js') +const ReturnVersionModel = require('../../../app/models/return-version.model.js') + +// Thing under test +const ProcessExistingReturnVersionsService = require('../../../app/services/return-requirements/process-existing-return-versions.service.js') + +describe('Process Existing Return Versions service', () => { + let existingReturnVersionId + let licenceId + let newVersionStartDate + + describe('When a "current" return version has a "startDate" < "newVersionStartDate" and no "endDate"', () => { + beforeEach(async () => { + existingReturnVersionId = generateUUID() + licenceId = generateUUID() + newVersionStartDate = new Date('2024-06-01') + + await ReturnVersionHelper.add({ + id: existingReturnVersionId, + licenceId, + startDate: new Date('2024-04-01'), + endDate: null + }) + }) + + it('sets the "endDate" of the existing record, a null "endDate" is returned for the new return version', async () => { + const result = await ProcessExistingReturnVersionsService.go(licenceId, newVersionStartDate) + const existingReturnVersion = await ReturnVersionModel.query().findById(existingReturnVersionId) + + expect(result).to.be.null() + expect(existingReturnVersion.endDate).to.equal(new Date('2024-05-31')) + }) + }) + + describe('When a "current" return version has a "startDate" < "newVersionStartDate" and an "endDate" greater', () => { + beforeEach(async () => { + existingReturnVersionId = generateUUID() + licenceId = generateUUID() + newVersionStartDate = new Date('2024-06-01') + + await ReturnVersionHelper.add({ + id: existingReturnVersionId, + licenceId, + startDate: new Date('2024-04-01'), + endDate: new Date('2024-07-01') + }) + }) + + it('sets the "endDate" of the existing record and an "endDate" is returned for the new return version', async () => { + const result = await ProcessExistingReturnVersionsService.go(licenceId, newVersionStartDate) + const existingReturnVersion = await ReturnVersionModel.query().findById(existingReturnVersionId) + + expect(result).to.equal(new Date('2024-07-01')) + expect(existingReturnVersion.endDate).to.equal(new Date('2024-05-31')) + }) + }) + + describe('When a "current" return version has a "startDate" === "newVersionStartDate" and no "endDate"', () => { + beforeEach(async () => { + existingReturnVersionId = generateUUID() + licenceId = generateUUID() + newVersionStartDate = new Date('2024-04-01') + + await ReturnVersionHelper.add({ + id: existingReturnVersionId, + licenceId, + startDate: new Date('2024-04-01'), + endDate: null + }) + }) + + it('sets the "status" of the existing record, a null end date is returned for the new return version', async () => { + const result = await ProcessExistingReturnVersionsService.go(licenceId, newVersionStartDate) + const existingReturnVersion = await ReturnVersionModel.query().findById(existingReturnVersionId) + + expect(result).to.be.null() + expect(existingReturnVersion.status).to.equal('superseded') + }) + }) + + describe('When a "current" return version has a "startDate" === "newVersionStartDate" and an "endDate"', () => { + beforeEach(async () => { + existingReturnVersionId = generateUUID() + licenceId = generateUUID() + newVersionStartDate = new Date('2024-04-01') + + await ReturnVersionHelper.add({ + id: existingReturnVersionId, + licenceId, + startDate: new Date('2024-04-01'), + endDate: new Date('2024-07-01') + }) + }) + + it('sets the "status" of the existing record and an "endDate" is returned for the new return version', async () => { + const result = await ProcessExistingReturnVersionsService.go(licenceId, newVersionStartDate) + const existingReturnVersion = await ReturnVersionModel.query().findById(existingReturnVersionId) + + expect(result).to.equal(new Date('2024-07-01')) + expect(existingReturnVersion.status).to.equal('superseded') + }) + }) +}) diff --git a/test/services/return-requirements/submit-check.service.test.js b/test/services/return-requirements/submit-check.service.test.js index 3b32a36fbe..293f60b249 100644 --- a/test/services/return-requirements/submit-check.service.test.js +++ b/test/services/return-requirements/submit-check.service.test.js @@ -34,6 +34,7 @@ describe('Return Requirements - Submit Check service', () => { endDate: null, licenceRef: '01/ABC', licenceHolder: 'Turbo Kid', + returnVersions: [], startDate: '2022-04-01T00:00:00.000Z' }, journey: 'returns-required',