Skip to content

Commit

Permalink
Multi-Year SROC Supplementary Billing (#228)
Browse files Browse the repository at this point in the history
https://eaflood.atlassian.net/browse/WATER-3984

Supplementary billing for SRoC currently only handles Financial Year 2022 - 2023, going forward it needs to pick up any changes to charge versions in any year from 2022. Once we have passed Annual Billing 2027 then the engine should only pick up changes to charge versions in the current year and the previous 5.

All transactions in the available billing period should be picked up in 1 bill run, and be triggered by a single action.

This PR adds the remainder of the functionality required for Multi-Year SROC Supplementary Billing not covered in the previous PR #226
  • Loading branch information
Jozzey authored Jun 6, 2023
1 parent 402309b commit 9400ec4
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ async function _fetch (regionId, billingPeriod) {
.where('regionId', regionId)
.where('chargeVersions.scheme', 'sroc')
.whereNotNull('chargeVersions.invoiceAccountId')
.where('chargeVersions.startDate', '>=', billingPeriod.startDate)
.where('chargeVersions.startDate', '<=', billingPeriod.endDate)
.where(builder => {
builder.whereNull('chargeVersions.endDate')
.orWhere('chargeVersions.endDate', '>=', billingPeriod.startDate)
})
.whereNot('chargeVersions.status', 'draft')
.whereNotExists(
ChargeVersionWorkflow.query()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,34 @@ const ProcessBillingPeriodService = require('./process-billing-period.service.js
const UnflagUnbilledLicencesService = require('./unflag-unbilled-licences.service.js')

async function go (billingBatch, billingPeriods) {
const currentBillingPeriod = billingPeriods[billingPeriods.length - 1]
const { billingBatchId } = billingBatch

try {
// Mark the start time for later logging
const startTime = process.hrtime.bigint()
const accumulatedLicenceIds = []

let isBatchPopulated = false

await _updateStatus(billingBatchId, 'processing')

const chargeVersions = await _fetchChargeVersions(billingBatch, currentBillingPeriod)
const isPopulated = await ProcessBillingPeriodService.go(billingBatch, currentBillingPeriod, chargeVersions)
for (const billingPeriod of billingPeriods) {
const chargeVersions = await _fetchChargeVersions(billingBatch, billingPeriod)
const isPeriodPopulated = await ProcessBillingPeriodService.go(billingBatch, billingPeriod, chargeVersions)
const licenceIdsForPeriod = _extractLicenceIds(chargeVersions)

accumulatedLicenceIds.push(...licenceIdsForPeriod)

if (isPeriodPopulated) {
isBatchPopulated = true
}
}

await _finaliseBillingBatch(billingBatch, chargeVersions, isPopulated)
// Creating a new set from accumulatedLicenceIds gives us just the unique ids. Objection does not accept sets in
// .findByIds() so we spread it into an array
const allLicenceIds = [...new Set(accumulatedLicenceIds)]

await _finaliseBillingBatch(billingBatch, allLicenceIds, isBatchPopulated)

// Log how long the process took
_calculateAndLogTime(billingBatchId, startTime)
Expand Down Expand Up @@ -50,6 +65,12 @@ function _calculateAndLogTime (billingBatchId, startTime) {
global.GlobalNotifier.omg(`Time taken to process billing batch ${billingBatchId}: ${timeTakenMs}ms`)
}

function _extractLicenceIds (chargeVersions) {
return chargeVersions.map((chargeVersion) => {
return chargeVersion.licence.licenceId
})
}

async function _fetchChargeVersions (billingBatch, billingPeriod) {
try {
const chargeVersions = await FetchChargeVersionsService.go(billingBatch.regionId, billingPeriod)
Expand All @@ -67,8 +88,8 @@ async function _fetchChargeVersions (billingBatch, billingPeriod) {
* process, and refreshes the billing batch locally. However if there were no resulting invoice licences then we simply
* unflag the unbilled licences and mark the billing batch with `empty` status
*/
async function _finaliseBillingBatch (billingBatch, chargeVersions, isPopulated) {
await UnflagUnbilledLicencesService.go(billingBatch.billingBatchId, chargeVersions)
async function _finaliseBillingBatch (billingBatch, allLicenceIds, isPopulated) {
await UnflagUnbilledLicencesService.go(billingBatch.billingBatchId, allLicenceIds)

// If there are no billing invoice licences then the bill run is considered empty. We just need to set the status to
// indicate this in the UI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,10 @@ const LicenceModel = require('../../models/water/licence.model.js')
* do this because we know this service has handled anything that was unbilled and not represented.
*
* @param {*} billingBatchId The ID of the bill run (billing batch) being processed
* @param {module:ChargeVersionModel[]} chargeVersions All charge versions being processed in the bill run
* @param {String[]} allLicenceIds All licence IDs being processed in the bill run
* @returns {Number} count of records updated
*/
async function go (billingBatchId, chargeVersions) {
const allLicenceIds = _extractUniqueLicenceIds(chargeVersions)

async function go (billingBatchId, allLicenceIds) {
return LicenceModel.query()
.patch({ includeInSrocSupplementaryBilling: false })
.whereIn('licenceId', allLicenceIds)
Expand All @@ -43,16 +41,6 @@ async function go (billingBatchId, chargeVersions) {
)
}

function _extractUniqueLicenceIds (chargeVersions) {
const allLicenceIds = chargeVersions.map((chargeVersion) => {
return chargeVersion.licence.licenceId
})

// Creating a new set from allLicenceIds gives us just the unique ids. Objection does not accept sets in
// .findByIds() so we spread it into an array
return [...new Set(allLicenceIds)]
}

module.exports = {
go
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,17 @@ describe('Fetch Charge Versions service', () => {

describe('when there are charge versions that should be considered for the next supplementary billing', () => {
let billingChargeCategory
let chargeElement
let chargePurpose
let chargeElement2023
let chargeElement2024
let chargePurpose2023
let chargePurpose2024
let changeReason
let licence

beforeEach(async () => {
billingPeriod = {
startDate: new Date('2022-04-01'),
endDate: new Date('2023-03-31')
startDate: new Date('2023-04-01'),
endDate: new Date('2024-03-31')
}

licence = await LicenceHelper.add({
Expand All @@ -58,9 +60,14 @@ describe('Fetch Charge Versions service', () => {
const { licenceId } = licence
changeReason = await ChangeReasonHelper.add({ triggersMinimumCharge: true })

// This creates a 'current' SROC charge version
const srocChargeVersion = await ChargeVersionHelper.add(
{ changeReasonId: changeReason.changeReasonId, licenceId }
// This creates a 'current' SROC charge version valid only FYE 2024
const sroc2024ChargeVersion = await ChargeVersionHelper.add(
{ startDate: new Date('2023-11-01'), changeReasonId: changeReason.changeReasonId, licenceId }
)

// This creates a 'current' SROC charge version valid in both FYE 2023 and 2024
const sroc2023ChargeVersion = await ChargeVersionHelper.add(
{ endDate: new Date('2023-10-31'), changeReasonId: changeReason.changeReasonId, licenceId }
)

// This creates a 'superseded' SROC charge version
Expand All @@ -73,19 +80,28 @@ describe('Fetch Charge Versions service', () => {
{ scheme: 'alcs', licenceId }
)

testRecords = [srocChargeVersion, srocSupersededChargeVersion, alcsChargeVersion]
testRecords = [sroc2024ChargeVersion, sroc2023ChargeVersion, srocSupersededChargeVersion, alcsChargeVersion]

// We test that related data is returned in the results. So, we create and link it to the srocChargeVersion
// ready for testing
billingChargeCategory = await BillingChargeCategoryHelper.add()

chargeElement = await ChargeElementHelper.add({
chargeVersionId: srocChargeVersion.chargeVersionId,
chargeElement2024 = await ChargeElementHelper.add({
chargeVersionId: sroc2024ChargeVersion.chargeVersionId,
billingChargeCategoryId: billingChargeCategory.billingChargeCategoryId
})

chargePurpose = await ChargePurposeHelper.add({
chargeElementId: chargeElement.chargeElementId
chargePurpose2024 = await ChargePurposeHelper.add({
chargeElementId: chargeElement2024.chargeElementId
})

chargeElement2023 = await ChargeElementHelper.add({
chargeVersionId: sroc2023ChargeVersion.chargeVersionId,
billingChargeCategoryId: billingChargeCategory.billingChargeCategoryId
})

chargePurpose2023 = await ChargePurposeHelper.add({
chargeElementId: chargeElement2023.chargeElementId
})
})

Expand All @@ -97,18 +113,20 @@ describe('Fetch Charge Versions service', () => {
it('returns the SROC charge versions that are applicable', async () => {
const result = await FetchChargeVersionsService.go(regionId, billingPeriod)

expect(result).to.have.length(2)
expect(result).to.have.length(3)
expect(result[0].chargeVersionId).to.equal(testRecords[0].chargeVersionId)
expect(result[1].chargeVersionId).to.equal(testRecords[1].chargeVersionId)
expect(result[2].chargeVersionId).to.equal(testRecords[2].chargeVersionId)
})
})

it("returns both 'current' and 'superseded' SROC charge versions that are applicable", async () => {
const result = await FetchChargeVersionsService.go(regionId, billingPeriod)

expect(result).to.have.length(2)
expect(result).to.have.length(3)
expect(result[0].chargeVersionId).to.equal(testRecords[0].chargeVersionId)
expect(result[1].chargeVersionId).to.equal(testRecords[1].chargeVersionId)
expect(result[2].chargeVersionId).to.equal(testRecords[2].chargeVersionId)
})

it('includes the related licence and region', async () => {
Expand All @@ -131,28 +149,50 @@ describe('Fetch Charge Versions service', () => {
it('includes the related charge elements, billing charge category and charge purposes', async () => {
const result = await FetchChargeVersionsService.go(regionId, billingPeriod)

const expectedResult = {
chargeElementId: chargeElement.chargeElementId,
source: chargeElement.source,
loss: chargeElement.loss,
volume: chargeElement.volume,
adjustments: chargeElement.adjustments,
additionalCharges: chargeElement.additionalCharges,
description: chargeElement.description,
const expectedResult2024 = {
chargeElementId: chargeElement2024.chargeElementId,
source: chargeElement2024.source,
loss: chargeElement2024.loss,
volume: chargeElement2024.volume,
adjustments: chargeElement2024.adjustments,
additionalCharges: chargeElement2024.additionalCharges,
description: chargeElement2024.description,
billingChargeCategory: {
reference: billingChargeCategory.reference,
shortDescription: billingChargeCategory.shortDescription
},
chargePurposes: [{
chargePurposeId: chargePurpose.chargePurposeId,
abstractionPeriodStartDay: chargePurpose.abstractionPeriodStartDay,
abstractionPeriodStartMonth: chargePurpose.abstractionPeriodStartMonth,
abstractionPeriodEndDay: chargePurpose.abstractionPeriodEndDay,
abstractionPeriodEndMonth: chargePurpose.abstractionPeriodEndMonth
chargePurposeId: chargePurpose2024.chargePurposeId,
abstractionPeriodStartDay: chargePurpose2024.abstractionPeriodStartDay,
abstractionPeriodStartMonth: chargePurpose2024.abstractionPeriodStartMonth,
abstractionPeriodEndDay: chargePurpose2024.abstractionPeriodEndDay,
abstractionPeriodEndMonth: chargePurpose2024.abstractionPeriodEndMonth
}]
}

expect(result[0].chargeElements[0]).to.equal(expectedResult)
const expectedResult2023 = {
chargeElementId: chargeElement2023.chargeElementId,
source: chargeElement2023.source,
loss: chargeElement2023.loss,
volume: chargeElement2023.volume,
adjustments: chargeElement2023.adjustments,
additionalCharges: chargeElement2023.additionalCharges,
description: chargeElement2023.description,
billingChargeCategory: {
reference: billingChargeCategory.reference,
shortDescription: billingChargeCategory.shortDescription
},
chargePurposes: [{
chargePurposeId: chargePurpose2023.chargePurposeId,
abstractionPeriodStartDay: chargePurpose2023.abstractionPeriodStartDay,
abstractionPeriodStartMonth: chargePurpose2023.abstractionPeriodStartMonth,
abstractionPeriodEndDay: chargePurpose2023.abstractionPeriodEndDay,
abstractionPeriodEndMonth: chargePurpose2023.abstractionPeriodEndMonth
}]
}

expect(result[0].chargeElements[0]).to.equal(expectedResult2024)
expect(result[1].chargeElements[0]).to.equal(expectedResult2023)
})
})

Expand Down Expand Up @@ -250,24 +290,24 @@ describe('Fetch Charge Versions service', () => {
})

describe('because none of them are in the billing period', () => {
describe('as they all have start dates before the billing period', () => {
describe('as they all have end dates before the billing period', () => {
beforeEach(async () => {
billingPeriod = {
startDate: new Date('2022-04-01'),
endDate: new Date('2023-03-31')
startDate: new Date('2023-04-01'),
endDate: new Date('2024-03-31')
}

const { licenceId } = await LicenceHelper.add({
regionId,
includeInSrocSupplementaryBilling: true
})

// This creates an SROC charge version with a start date before the billing period. This would have been
// picked up by a previous bill run
const alcsChargeVersion = await ChargeVersionHelper.add(
{ startDate: new Date('2022-03-01'), licenceId }
// This creates an SROC charge version with an end date before the billing period. This would have been
// picked up by a previous years bill run
const srocChargeVersion = await ChargeVersionHelper.add(
{ startDate: new Date('2022-04-01'), endDate: new Date('2022-10-01'), licenceId }
)
testRecords = [alcsChargeVersion]
testRecords = [srocChargeVersion]
})

it('returns no applicable charge versions', async () => {
Expand All @@ -291,10 +331,10 @@ describe('Fetch Charge Versions service', () => {

// This creates an SROC charge version with a start date after the billing period. This will be picked in
// next years bill runs
const alcsChargeVersion = await ChargeVersionHelper.add(
const srocChargeVersion = await ChargeVersionHelper.add(
{ startDate: new Date('2023-04-01'), licenceId }
)
testRecords = [alcsChargeVersion]
testRecords = [srocChargeVersion]
})

it('returns no applicable charge versions', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,25 @@ describe('Unflag unbilled licences service', () => {
})

describe('when there are licences flagged for SROC supplementary billing', () => {
let chargeVersions
const licences = {
notInBillRun: null,
notBilledInBillRun: null,
billedInBillRun: null
}
let allLicenceIds

beforeEach(async () => {
licences.notInBillRun = await LicenceHelper.add({ includeInSrocSupplementaryBilling: true })
licences.notBilledInBillRun = await LicenceHelper.add({ includeInSrocSupplementaryBilling: true })
licences.billedInBillRun = await LicenceHelper.add({ includeInSrocSupplementaryBilling: true })

chargeVersions = [
{ licence: { licenceId: licences.notBilledInBillRun.licenceId } },
{ licence: { licenceId: licences.billedInBillRun.licenceId } }
]
allLicenceIds = [licences.notBilledInBillRun.licenceId, licences.billedInBillRun.licenceId]
})

describe('those licences in the current bill run', () => {
describe('which were not billed', () => {
it('are unflagged (include_in_sroc_supplementary_billing set to false)', async () => {
await UnflagUnbilledLicencesService.go(billingBatchId, chargeVersions)
await UnflagUnbilledLicencesService.go(billingBatchId, allLicenceIds)

const licenceToBeChecked = await LicenceModel.query().findById(licences.notBilledInBillRun.licenceId)

Expand All @@ -61,7 +58,7 @@ describe('Unflag unbilled licences service', () => {
})

it('are left flagged (include_in_sroc_supplementary_billing still true)', async () => {
await UnflagUnbilledLicencesService.go(billingBatchId, chargeVersions)
await UnflagUnbilledLicencesService.go(billingBatchId, allLicenceIds)

const licenceToBeChecked = await LicenceModel.query().findById(licences.billedInBillRun.licenceId)

Expand All @@ -72,7 +69,7 @@ describe('Unflag unbilled licences service', () => {

describe('those licences not in the current bill run', () => {
it('leaves flagged (include_in_sroc_supplementary_billing still true)', async () => {
await UnflagUnbilledLicencesService.go(billingBatchId, chargeVersions)
await UnflagUnbilledLicencesService.go(billingBatchId, allLicenceIds)

const licenceToBeChecked = await LicenceModel.query().findById(licences.notInBillRun.licenceId)

Expand Down

0 comments on commit 9400ec4

Please sign in to comment.