diff --git a/app/services/supplementary-billing/determine-charge-period.service.js b/app/services/supplementary-billing/determine-charge-period.service.js new file mode 100644 index 0000000000..61b25a5a9f --- /dev/null +++ b/app/services/supplementary-billing/determine-charge-period.service.js @@ -0,0 +1,64 @@ +'use strict' + +/** + * Determines what the charge period is for a charge version in a given billing period + * @module DetermineChargePeriodService + */ + +/** + * Returns a charge period, which is an object comprising `startDate` and `endDate` + * + * The charge period is determined as the overlap between the charge version's start and end dates, and the financial + * year. So the charge period's start date is the later of the two's start dates, and the charge period end date is the + * earlier of the two's end dates. + * + * > Charge versions may not have an end date; in this case, we simply use the financial year end date. + * + * @param {module:ChargeVersionModel} chargeVersion the charge version being processed for billing + * @param {number} financialYearEnding the year the financial billing period ends + * + * @returns {Object} the start and end date of the calculated charge period + */ +function go (chargeVersion, financialYearEnding) { + const financialYearStartDate = new Date(financialYearEnding - 1, 3, 1) + const financialYearEndDate = new Date(financialYearEnding, 2, 31) + + if (_periodIsInvalid(chargeVersion, financialYearStartDate, financialYearEndDate)) { + throw new Error(`Charge version is outside billing period ${financialYearEnding}`) + } + + const chargeVersionStartDate = chargeVersion.startDate + const latestStartDateTimestamp = Math.max(financialYearStartDate, chargeVersionStartDate) + + // If the charge version has no end date then use the financial year end date instead + const chargeVersionEndDate = chargeVersion.endDate || financialYearEndDate + const earliestEndDateTimestamp = Math.min(financialYearEndDate, chargeVersionEndDate) + + return { + startDate: new Date(latestStartDateTimestamp), + endDate: new Date(earliestEndDateTimestamp) + } +} + +/** + * Determine if the charge version starts after or ends before the billing period + * + * We never expect a charge version outside the financial period but we do this just to ensure we never return + * nonsense and all possible scenarios are covered in our tests 😁 + * + * @param {Object} chargeVersion chargeVersion being billed + * @param {Date} financialYearStartDate billing period (financial year) start date + * @param {Date} financialYearEndDate billing period (financial year) end date + * + * @returns {boolean} true if invalid else false + */ +function _periodIsInvalid (chargeVersion, financialYearStartDate, financialYearEndDate) { + const chargeVersionStartsAfterFinancialYear = chargeVersion.startDate > financialYearEndDate + const chargeVersionEndsBeforeFinancialYear = chargeVersion.endDate && chargeVersion.endDate < financialYearStartDate + + return chargeVersionStartsAfterFinancialYear || chargeVersionEndsBeforeFinancialYear +} + +module.exports = { + go +} diff --git a/app/services/supplementary-billing/format-sroc-transaction-line.service.js b/app/services/supplementary-billing/format-sroc-transaction-line.service.js index a9864d6ca6..0bb60a41b6 100644 --- a/app/services/supplementary-billing/format-sroc-transaction-line.service.js +++ b/app/services/supplementary-billing/format-sroc-transaction-line.service.js @@ -8,6 +8,7 @@ const AbstractionBillingPeriodService = require('./abstraction-billing-period.service.js') const ConsolidateDateRangesService = require('./consolidate-date-ranges.service.js') +const DetermineChargePeriodService = require('./determine-charge-period.service.js') /** * Takes a charge element, charge version and financial year and returns an object representing an sroc transaction @@ -27,7 +28,7 @@ const ConsolidateDateRangesService = require('./consolidate-date-ranges.service. function go (chargeElement, chargeVersion, financialYearEnding, options) { const optionsData = _optionsDefaults(options) - const chargePeriod = _determineChargePeriod(chargeVersion, financialYearEnding) + const chargePeriod = DetermineChargePeriodService.go(chargeVersion, financialYearEnding) return { chargeElementId: chargeElement.chargeElementId, @@ -78,31 +79,6 @@ function _optionsDefaults (options) { } } -/** - * Returns a charge period, which is an object comprising `startDate` and `endDate` - * - * The charge period is determined as the overlap between the charge version's start and end dates, and the financial - * year. So the charge period's start date is the later of the two's start dates, and the charge period end date is the - * earlier of the two's end dates. - * - * Note that charge versions may not have an end date; in this case, we simply use the financial year end date. - */ -function _determineChargePeriod (chargeVersion, financialYearEnding) { - const financialYearStartDate = new Date(financialYearEnding - 1, 3, 1) - const chargeVersionStartDate = chargeVersion.startDate - const latestStartDateTimestamp = Math.max(financialYearStartDate, chargeVersionStartDate) - - const financialYearEndDate = new Date(financialYearEnding, 2, 31) - // If the charge version has no end date then use the financial year end date instead - const chargeVersionEndDate = chargeVersion.endDate || financialYearEndDate - const earliestEndDateTimestamp = Math.min(financialYearEndDate, chargeVersionEndDate) - - return { - startDate: new Date(latestStartDateTimestamp), - endDate: new Date(earliestEndDateTimestamp) - } -} - /** * Calculates the number of authorised days, ie. the number of overlapping days of the financial year and the charge * element's abstraction periods diff --git a/test/services/supplementary-billing/determine-charge-period.service.test.js b/test/services/supplementary-billing/determine-charge-period.service.test.js new file mode 100644 index 0000000000..62921d0575 --- /dev/null +++ b/test/services/supplementary-billing/determine-charge-period.service.test.js @@ -0,0 +1,172 @@ +'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 + +// Thing under test +const DetermineChargePeriodService = require('../../../app/services/supplementary-billing/determine-charge-period.service.js') + +describe('Determine charge period service', () => { + const financialYear = { + startDate: new Date('2022-04-01'), + endDate: new Date('2023-03-31'), + yearEnding: 2023 + } + let chargeVersion + + describe('charge version starts inside the financial period', () => { + beforeEach(() => { + chargeVersion = { + startDate: new Date('2022-05-01'), + endDate: null + } + }) + + describe('and the charge version has an end date', () => { + beforeEach(() => { + chargeVersion.endDate = new Date('2022-05-31') + }) + + it('returns the charge version start and end dates', () => { + const result = DetermineChargePeriodService.go(chargeVersion, financialYear.yearEnding) + + expect(result.startDate).to.equal(chargeVersion.startDate) + expect(result.endDate).to.equal(chargeVersion.endDate) + }) + }) + + describe('and the charge version does not have an end date', () => { + it('returns the charge version start date and financial year end date', () => { + const result = DetermineChargePeriodService.go(chargeVersion, financialYear.yearEnding) + + expect(result.startDate).to.equal(chargeVersion.startDate) + expect(result.endDate).to.equal(financialYear.endDate) + }) + }) + }) + + describe('financial period starts inside the charge version', () => { + beforeEach(() => { + chargeVersion = { + startDate: new Date('2022-02-01'), + endDate: null + } + }) + + describe('and the charge version has an end date', () => { + beforeEach(() => { + chargeVersion.endDate = new Date('2023-05-31') + }) + + it('returns the financial start and end dates', () => { + const result = DetermineChargePeriodService.go(chargeVersion, financialYear.yearEnding) + + expect(result.startDate).to.equal(financialYear.startDate) + expect(result.endDate).to.equal(financialYear.endDate) + }) + }) + + describe('and the charge version does not have an end date', () => { + it('returns the financial start and end dates', () => { + const result = DetermineChargePeriodService.go(chargeVersion, financialYear.yearEnding) + + expect(result.startDate).to.equal(financialYear.startDate) + expect(result.endDate).to.equal(financialYear.endDate) + }) + }) + }) + + describe('charge version starts before the financial period', () => { + beforeEach(() => { + chargeVersion = { + startDate: new Date('2022-02-01'), + endDate: null + } + }) + + describe('and the charge version has an end date that is inside the financial period', () => { + beforeEach(() => { + chargeVersion.endDate = new Date('2022-05-31') + }) + + it('returns the financial start and charge version end date', () => { + const result = DetermineChargePeriodService.go(chargeVersion, financialYear.yearEnding) + + expect(result.startDate).to.equal(financialYear.startDate) + expect(result.endDate).to.equal(chargeVersion.endDate) + }) + }) + + describe('and the charge version does not have an end date', () => { + it('returns the financial start and end dates', () => { + const result = DetermineChargePeriodService.go(chargeVersion, financialYear.yearEnding) + + expect(result.startDate).to.equal(financialYear.startDate) + expect(result.endDate).to.equal(financialYear.endDate) + }) + }) + }) + + describe('financial period starts before the charge version', () => { + beforeEach(() => { + chargeVersion = { + startDate: new Date('2022-05-01'), + endDate: null + } + }) + + describe('and the charge version has an end date that is outside the financial period', () => { + beforeEach(() => { + chargeVersion.endDate = new Date('2023-05-31') + }) + + it('returns the charge version start date and financial period end date', () => { + const result = DetermineChargePeriodService.go(chargeVersion, financialYear.yearEnding) + + expect(result.startDate).to.equal(chargeVersion.startDate) + expect(result.endDate).to.equal(financialYear.endDate) + }) + }) + + describe('and the charge version does not have an end date', () => { + it('returns the charge version start date and financial period end date', () => { + const result = DetermineChargePeriodService.go(chargeVersion, financialYear.yearEnding) + + expect(result.startDate).to.equal(chargeVersion.startDate) + expect(result.endDate).to.equal(financialYear.endDate) + }) + }) + }) + + describe('neither period overlaps', () => { + describe('because the charge version start date is after the financial period', () => { + beforeEach(() => { + chargeVersion = { + startDate: new Date('2023-05-01'), + endDate: null + } + }) + + it('throws an error', () => { + expect(() => DetermineChargePeriodService.go(chargeVersion, financialYear.yearEnding)).to.throw() + }) + }) + + describe('because the charge version end date is before the financial period', () => { + beforeEach(() => { + chargeVersion = { + startDate: new Date('2021-05-01'), + endDate: new Date('2021-05-31') + } + }) + + it('throws an error', () => { + expect(() => DetermineChargePeriodService.go(chargeVersion, financialYear.yearEnding)).to.throw() + }) + }) + }) +})