Skip to content

Commit

Permalink
Add FetchBillingAccountsService for 2-part tariff (#1129)
Browse files Browse the repository at this point in the history
https://eaflood.atlassian.net/browse/WATER-4196

> Part of the work for two-part tariff annual billing

We're ready to generate a bill run from our two-part tariff review data and with [Add Continue bill run btn to 2PT review screen](#1122) and [Add new two-part tariff generate bill run endpoint](#1123) we have the means to trigger it.

We're following the pattern used in SROC annual billing of fetching the data needed by billing account. A bill run is made up of 'bills', one for each billing account. We learned during the building of the SROC annual engine that having the root record as the billing account vastly simplified the process (we will complete our refactor of SROC supplementary one day!)

So, this change adds the `FetchBillingAccountsService` for two-part tariff bill runs. It goes without saying it is a bit of a beast! We are not just having to retrieve charge version, reference and element records, but their equivalents in the review dataset.

It results in a massive [Objection.js](https://vincit.github.io/objection.js/) query. The good news is that there is little complexity. It is a straight-up data retrieval service.

Still a lot of code though! 😳😬
  • Loading branch information
Cruikshanks authored Jul 8, 2024
1 parent e4470b1 commit 5933403
Show file tree
Hide file tree
Showing 2 changed files with 359 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
'use strict'

/**
* Fetches all billing accounts linked to a bill run to be processed as part of two-part tariff billing
* @module FetchBillingAccountsService
*/

const { ref } = require('objection')

const BillingAccountModel = require('../../../models/billing-account.model.js')
const ChargeVersionModel = require('../../../models/charge-version.model.js')

/**
* Fetches all billing accounts linked to a bill run to be processed as part of two-part tariff billing
*
* Unlike regular annual and supplementary, when we come to generate the bill run the licences that will be in it have
* already been determined. This is because they first would have gone through 'review' after the match & allocate
* engine had compared their charge information to the return submissions.
*
* The complexity is we are having to go `billing account -> charge version -> review licence -> bill run ID` in order
* to filter them.
*
* Once we've got them, we then need to get each level of charge information and their associated review records. We can
* then combine the source charge information with the result of review in order to generate the transactions.
*
* That information is extracted in `ProcessBillingPeriodService` though. This just focuses on fetching the data.
*
* @param {string} billRunId - The UUID of the two-part tariff bill run to fetches billing accounts for
*
* @returns {Promise<[module:BillingAccountModel]>} An array of `BillingAccountModel` to be billed and their relevant
* licence, charge version, charge element etc records, plus the two-part tariff review details needed to generate the
* bill run
*/
async function go (billRunId) {
return BillingAccountModel.query()
.select([
'billingAccounts.id',
'billingAccounts.accountNumber'
])
.whereExists(_whereBillingAccountExistsClause(billRunId))
.orderBy([
{ column: 'billingAccounts.accountNumber' }
])
.withGraphFetched('chargeVersions')
.modifyGraph('chargeVersions', (builder) => {
builder
.select([
'chargeVersions.id',
'chargeVersions.scheme',
'chargeVersions.startDate',
'chargeVersions.endDate',
'chargeVersions.billingAccountId',
'chargeVersions.status'
])
.innerJoin('reviewChargeVersions', 'reviewChargeVersions.chargeVersionId', 'chargeVersions.id')
.innerJoin('reviewLicences', 'reviewLicences.id', 'reviewChargeVersions.reviewLicenceId')
.where('reviewLicences.billRunId', billRunId)
.orderBy([
{ column: 'licenceId', order: 'ASC' },
{ column: 'startDate', order: 'ASC' }
])
})
.withGraphFetched('chargeVersions.licence')
.modifyGraph('chargeVersions.licence', (builder) => {
builder.select([
'id',
'licenceRef',
'waterUndertaker',
ref('licences.regions:historicalAreaCode').castText().as('historicalAreaCode'),
ref('licences.regions:regionalChargeArea').castText().as('regionalChargeArea'),
'startDate',
'expiredDate',
'lapsedDate',
'revokedDate'
])
})
.withGraphFetched('chargeVersions.chargeReferences')
.modifyGraph('chargeVersions.chargeReferences', (builder) => {
builder.select([
'chargeReferences.id',
'chargeReferences.source',
'chargeReferences.loss',
'chargeReferences.volume',
'chargeReferences.adjustments',
'chargeReferences.additionalCharges',
'chargeReferences.description'
])
.innerJoin('reviewChargeReferences', 'reviewChargeReferences.chargeReferenceId', 'chargeReferences.id')
.innerJoin('reviewChargeVersions', 'reviewChargeVersions.id', 'reviewChargeReferences.reviewChargeVersionId')
.innerJoin('reviewLicences', 'reviewLicences.id', 'reviewChargeVersions.reviewLicenceId')
.where('reviewLicences.billRunId', billRunId)
})
.withGraphFetched('chargeVersions.chargeReferences.reviewChargeReferences')
.modifyGraph('chargeVersions.chargeReferences.reviewChargeReferences', (builder) => {
builder.select([
'reviewChargeReferences.id',
'reviewChargeReferences.amendedAggregate',
'reviewChargeReferences.amendedChargeAdjustment',
'reviewChargeReferences.amendedAuthorisedVolume'
])
.innerJoin('reviewChargeVersions', 'reviewChargeVersions.id', 'reviewChargeReferences.reviewChargeVersionId')
.innerJoin('reviewLicences', 'reviewLicences.id', 'reviewChargeVersions.reviewLicenceId')
.where('reviewLicences.billRunId', billRunId)
})
.withGraphFetched('chargeVersions.chargeReferences.chargeCategory')
.modifyGraph('chargeVersions.chargeReferences.chargeCategory', (builder) => {
builder.select([
'id',
'reference',
'shortDescription'
])
})
.withGraphFetched('chargeVersions.chargeReferences.chargeElements')
.modifyGraph('chargeVersions.chargeReferences.chargeElements', (builder) => {
builder.select([
'chargeElements.id',
'chargeElements.abstractionPeriodStartDay',
'chargeElements.abstractionPeriodStartMonth',
'chargeElements.abstractionPeriodEndDay',
'chargeElements.abstractionPeriodEndMonth'
])
.innerJoin('reviewChargeElements', 'reviewChargeElements.chargeElementId', 'chargeElements.id')
.innerJoin('reviewChargeReferences', 'reviewChargeReferences.id', 'reviewChargeElements.reviewChargeReferenceId')
.innerJoin('reviewChargeVersions', 'reviewChargeVersions.id', 'reviewChargeReferences.reviewChargeVersionId')
.innerJoin('reviewLicences', 'reviewLicences.id', 'reviewChargeVersions.reviewLicenceId')
.where('reviewLicences.billRunId', billRunId)
})
.withGraphFetched('chargeVersions.chargeReferences.chargeElements.reviewChargeElements')
.modifyGraph('chargeVersions.chargeReferences.chargeElements.reviewChargeElements', (builder) => {
builder.select([
'reviewChargeElements.id',
'reviewChargeElements.amendedAllocated'
])
.innerJoin('reviewChargeReferences', 'reviewChargeReferences.id', 'reviewChargeElements.reviewChargeReferenceId')
.innerJoin('reviewChargeVersions', 'reviewChargeVersions.id', 'reviewChargeReferences.reviewChargeVersionId')
.innerJoin('reviewLicences', 'reviewLicences.id', 'reviewChargeVersions.reviewLicenceId')
.where('reviewLicences.billRunId', billRunId)
})
}

function _whereBillingAccountExistsClause (billRunId) {
const query = ChargeVersionModel.query().select(1)

query
.innerJoin('reviewChargeVersions', 'reviewChargeVersions.chargeVersionId', 'chargeVersions.id')
.innerJoin('reviewLicences', 'reviewLicences.id', 'reviewChargeVersions.reviewLicenceId')
.whereColumn('chargeVersions.billingAccountId', 'billingAccounts.id')
.where('reviewLicences.billRunId', billRunId)

return query
}

module.exports = {
go
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
'use strict'

// Test framework dependencies
const Lab = require('@hapi/lab')
const Code = require('@hapi/code')

const { describe, it, before } = exports.lab = Lab.script()
const { expect } = Code

// Test helpers
const BillRunHelper = require('../../../support/helpers/bill-run.helper.js')
const BillingAccountHelper = require('../../../support/helpers/billing-account.helper.js')
const BillingAccountModel = require('../../../../app/models/billing-account.model.js')
const ChargeCategoryHelper = require('../../../support/helpers/charge-category.helper.js')
const ChargeElementHelper = require('../../../support/helpers/charge-element.helper.js')
const ChargeReferenceHelper = require('../../../support/helpers/charge-reference.helper.js')
const ChargeVersionHelper = require('../../../support/helpers/charge-version.helper.js')
const LicenceHelper = require('../../../support/helpers/licence.helper.js')
const ReviewChargeElementHelper = require('../../../support/helpers/review-charge-element.helper.js')
const ReviewChargeReferenceHelper = require('../../../support/helpers/review-charge-reference.helper.js')
const ReviewChargeVersionHelper = require('../../../support/helpers/review-charge-version.helper.js')
const ReviewLicenceHelper = require('../../../support/helpers/review-licence.helper.js')

// Thing under test
const FetchBillingAccountsService = require('../../../../app/services/bill-runs/two-part-tariff/fetch-billing-accounts.service.js')

describe('Fetch Billing Accounts service', () => {
let billRun
let billingAccount
let billingAccountNotInBillRun
let chargeCategory
let chargeElement
let chargeReference
let chargeVersion
let licence

let reviewChargeElement
let reviewChargeReference
let reviewChargeVersion

before(async () => {
billRun = await BillRunHelper.add()

licence = await LicenceHelper.add()

billingAccount = await BillingAccountHelper.add()
billingAccountNotInBillRun = await BillingAccountHelper.add()

const reviewLicence = await ReviewLicenceHelper.add({ billRunId: billRun.id, licenceId: licence.id })
const { id: reviewLicenceId } = reviewLicence

chargeVersion = await ChargeVersionHelper.add({
startDate: new Date('2023-11-01'),
billingAccountId: billingAccount.id,
licenceId: licence.id,
licenceRef: licence.licenceRef
})
const { id: chargeVersionId } = chargeVersion

reviewChargeVersion = await ReviewChargeVersionHelper.add({ chargeVersionId, reviewLicenceId })
const { id: reviewChargeVersionId } = reviewChargeVersion

chargeCategory = await ChargeCategoryHelper.add()
const { id: chargeCategoryId } = chargeCategory

chargeReference = await ChargeReferenceHelper.add({ chargeVersionId, chargeCategoryId })
const { id: chargeReferenceId } = chargeReference

reviewChargeReference = await ReviewChargeReferenceHelper.add({ chargeReferenceId, reviewChargeVersionId })
const { id: reviewChargeReferenceId } = reviewChargeReference

chargeElement = await ChargeElementHelper.add({ chargeReferenceId })
const { id: chargeElementId } = chargeElement

reviewChargeElement = await ReviewChargeElementHelper.add({ chargeElementId, reviewChargeReferenceId })
})

describe('when there are billing accounts that are linked to a two-part tariff bill run', () => {
it('returns the applicable billing accounts', async () => {
const results = await FetchBillingAccountsService.go(billRun.id)

expect(results).to.have.length(1)

expect(results[0]).to.be.instanceOf(BillingAccountModel)
expect(results[0].id).to.equal(billingAccount.id)
expect(results[0].accountNumber).to.equal(billingAccount.accountNumber)
})

describe('and each billing account', () => {
describe('for the charge versions property', () => {
it('returns the applicable charge versions', async () => {
const results = await FetchBillingAccountsService.go(billRun.id)

const { chargeVersions } = results[0]

expect(chargeVersions[0].id).to.equal(chargeVersion.id)
expect(chargeVersions[0].scheme).to.equal('sroc')
expect(chargeVersions[0].startDate).to.equal(new Date('2023-11-01'))
expect(chargeVersions[0].endDate).to.be.null()
expect(chargeVersions[0].billingAccountId).to.equal(billingAccount.id)
expect(chargeVersions[0].status).to.equal('current')
})

describe('and against each charge version', () => {
it('includes the licence', async () => {
const results = await FetchBillingAccountsService.go(billRun.id)

const { licence } = results[0].chargeVersions[0]

expect(licence.id).to.equal(licence.id)
expect(licence.licenceRef).to.equal(licence.licenceRef)
expect(licence.waterUndertaker).to.equal(false)
expect(licence.historicalAreaCode).to.equal('SAAR')
expect(licence.regionalChargeArea).to.equal('Southern')
})

it('includes the applicable charge references', async () => {
const results = await FetchBillingAccountsService.go(billRun.id)

const { chargeReferences } = results[0].chargeVersions[0]

expect(chargeReferences[0].id).to.equal(chargeReference.id)
expect(chargeReferences[0].source).to.equal('non-tidal')
expect(chargeReferences[0].loss).to.equal('low')
expect(chargeReferences[0].volume).to.equal(6.819)
expect(chargeReferences[0].adjustments).to.equal({
s126: null, s127: false, s130: false, charge: null, winter: false, aggregate: '0.562114443'
})
expect(chargeReferences[0].additionalCharges).to.equal({ isSupplyPublicWater: true })
expect(chargeReferences[0].description).to.equal('Mineral washing')
})

describe('and against each charge reference', () => {
it('includes the charge category', async () => {
const results = await FetchBillingAccountsService.go(billRun.id)

const { chargeCategory: result } = results[0].chargeVersions[0].chargeReferences[0]

expect(result.id).to.equal(chargeCategory.id)
expect(result.reference).to.equal(chargeCategory.reference)
expect(result.shortDescription).to.equal(chargeCategory.shortDescription)
})

it('includes the review charge references', async () => {
const results = await FetchBillingAccountsService.go(billRun.id)

const { reviewChargeReferences: result } = results[0].chargeVersions[0].chargeReferences[0]

expect(result[0].id).to.equal(reviewChargeReference.id)
expect(result[0].amendedAggregate).to.equal(reviewChargeReference.amendedAggregate)
expect(result[0].amendedChargeAdjustment).to.equal(reviewChargeReference.amendedChargeAdjustment)
expect(result[0].amendedAuthorisedVolume).to.equal(reviewChargeReference.amendedAuthorisedVolume)
})

it('includes the charge elements', async () => {
const results = await FetchBillingAccountsService.go(billRun.id)

const { chargeElements: result } = results[0].chargeVersions[0].chargeReferences[0]

expect(result[0].id).to.equal(chargeElement.id)
expect(result[0].abstractionPeriodStartDay).to.equal(chargeElement.abstractionPeriodStartDay)
expect(result[0].abstractionPeriodStartMonth).to.equal(chargeElement.abstractionPeriodStartMonth)
expect(result[0].abstractionPeriodEndDay).to.equal(chargeElement.abstractionPeriodEndDay)
expect(result[0].abstractionPeriodEndMonth).to.equal(chargeElement.abstractionPeriodEndMonth)
})

describe('and against each charge element', () => {
it('includes the review charge elements', async () => {
const results = await FetchBillingAccountsService.go(billRun.id)

const { reviewChargeElements: result } = results[0]
.chargeVersions[0]
.chargeReferences[0]
.chargeElements[0]

expect(result[0].id).to.equal(reviewChargeElement.id)
expect(result[0].amendedAllocated).to.equal(reviewChargeElement.amendedAllocated)
})
})
})
})
})
})
})

describe('when there are billing accounts not linked to a two-part tariff bill run', () => {
it('does not include them in the results', async () => {
const results = await FetchBillingAccountsService.go(billRun.id)

expect(results).to.have.length(1)

expect(results[0]).to.be.instanceOf(BillingAccountModel)
expect(results[0].id).not.to.equal(billingAccountNotInBillRun.id)
})
})

describe('when there are no billing accounts at all (no results)', () => {
it('returns no results', async () => {
const results = await FetchBillingAccountsService.go('1c1f7af5-9cba-47a7-8fc4-2c03b0d1124d')

expect(results).to.be.empty()
})
})
})

0 comments on commit 5933403

Please sign in to comment.