diff --git a/app/lib/general.lib.js b/app/lib/general.lib.js index 195174711b..bf7ed1de70 100644 --- a/app/lib/general.lib.js +++ b/app/lib/general.lib.js @@ -27,6 +27,53 @@ function generateUUID () { return randomUUID({ disableEntropyCache: true }) } +/** + * Tests if one set of periods (represented by a start and end date) overlaps with another + * + * Added as part of two-part tariff and the need to match returns and lines to charge elements. A common complication in + * WRLS is the need to convert an abstract period, for example '1 Nov to 31 Mar' to a concrete period (2023-11-01 to + * 2024-03-31). It gets even more complex when the period crosses over another, for example the start or end of a + * billing period. Then the only way to represent the abstract period in a usable way is as 2 separate periods. Hence + * this service deals with arrays of periods. + * + * > See the comments for `DetermineAbstractionPeriodService` to better understand the complexity of going from + * > abstract to concrete periods + * + * Then there are times we need to test if the periods of one thing overlap with another. In two-part tariff that's the + * abstraction periods of a charge element with those of a return. If _any_ of the periods overlap then the return is + * 'matched' and can be allocated to the charge element. + * + * This method iterates through the `referencePeriods`. It then filters the `checkPeriods` by testing if any of them + * overlap with the `referencePeriod`. If any do `checkPeriods.filter()` will return a non-empty array at which point + * `periodsOverlap()` will return `true`. + * + * Else, having compared all the `checkPeriods` against each `referencePeriod` and finding no overlaps the function will + * return false. + * + * @param {Object[]} referencePeriods Each period is an object containing a `startDate` and `endDate` property + * @param {Object[]} checkPeriods Each period is an object containing a `startDate` and `endDate` property. These + * periods will be checked against the `referencePeriods for any overlaps + * + * @returns {boolean} Returns true if there _any_ check period overlaps with a reference period, else false + */ +function periodsOverlap (referencePeriods, checkPeriods) { + for (const referencePeriod of referencePeriods) { + const overLappingPeriods = checkPeriods.filter((checkPeriod) => { + if (checkPeriod.startDate > referencePeriod.endDate || referencePeriod.startDate > checkPeriod.endDate) { + return false + } + + return true + }) + + if (overLappingPeriods.length) { + return true + } + } + + return false +} + /** * Returns the current date and time as an ISO string * @@ -44,5 +91,6 @@ function timestampForPostgres () { module.exports = { generateUUID, + periodsOverlap, timestampForPostgres } diff --git a/test/lib/general.lib.test.js b/test/lib/general.lib.test.js index ba58b40656..42d6baa88d 100644 --- a/test/lib/general.lib.test.js +++ b/test/lib/general.lib.test.js @@ -48,4 +48,129 @@ describe('RequestLib', () => { expect(result).to.equal('2015-10-21T20:31:57.000Z') }) }) + + describe('#periodsOverlap', () => { + let referencePeriod + let checkPeriod + + describe('when given periods that do not overlap', () => { + beforeEach(() => { + referencePeriod = [{ + startDate: new Date('2023-02-01'), + endDate: new Date('2023-02-02') + }] + + checkPeriod = [{ + startDate: new Date('2023-01-01'), + endDate: new Date('2023-01-31') + }] + }) + + it('returns false', () => { + const result = GeneralLib.periodsOverlap(referencePeriod, checkPeriod) + + expect(result).to.equal(false) + }) + }) + + describe('when a check period overlaps the start of a reference period', () => { + beforeEach(() => { + referencePeriod = [{ + startDate: new Date('2023-02-01'), + endDate: new Date('2023-02-28') + }] + + checkPeriod = [{ + startDate: new Date('2023-01-15'), + endDate: new Date('2023-02-15') + }] + }) + + it('returns true', () => { + const result = GeneralLib.periodsOverlap(referencePeriod, checkPeriod) + + expect(result).to.equal(true) + }) + }) + + describe('when a check period overlaps the end of a reference period', () => { + beforeEach(() => { + referencePeriod = [{ + startDate: new Date('2023-01-01'), + endDate: new Date('2023-01-31') + }] + + checkPeriod = [{ + startDate: new Date('2023-01-15'), + endDate: new Date('2023-02-15') + }] + }) + + it('returns true', () => { + const result = GeneralLib.periodsOverlap(referencePeriod, checkPeriod) + + expect(result).to.equal(true) + }) + }) + + describe('when a reference period is completely inside a check period', () => { + beforeEach(() => { + referencePeriod = [{ + startDate: new Date('2023-02-01'), + endDate: new Date('2023-02-15') + }] + + checkPeriod = [{ + startDate: new Date('2023-01-01'), + endDate: new Date('2023-02-28') + }] + }) + + it('returns true', () => { + const result = GeneralLib.periodsOverlap(referencePeriod, checkPeriod) + + expect(result).to.equal(true) + }) + }) + + describe('when a check period is completely inside a reference period', () => { + beforeEach(() => { + referencePeriod = [{ + startDate: new Date('2023-01-01'), + endDate: new Date('2023-02-28') + }] + + checkPeriod = [{ + startDate: new Date('2023-02-01'), + endDate: new Date('2023-02-15') + }] + }) + + it('returns true', () => { + const result = GeneralLib.periodsOverlap(referencePeriod, checkPeriod) + + expect(result).to.equal(true) + }) + }) + + describe('when the periods are the same', () => { + beforeEach(() => { + referencePeriod = [{ + startDate: new Date('2023-02-01'), + endDate: new Date('2023-02-28') + }] + + checkPeriod = [{ + startDate: new Date('2023-02-01'), + endDate: new Date('2023-02-28') + }] + }) + + it('returns true', () => { + const result = GeneralLib.periodsOverlap(referencePeriod, checkPeriod) + + expect(result).to.equal(true) + }) + }) + }) })