-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add FetchBillingAccountsService for 2-part tariff (#1129)
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
1 parent
e4470b1
commit 5933403
Showing
2 changed files
with
359 additions
and
0 deletions.
There are no files selected for viewing
155 changes: 155 additions & 0 deletions
155
app/services/bill-runs/two-part-tariff/fetch-billing-accounts.service.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
204 changes: 204 additions & 0 deletions
204
test/services/bill-runs/two-part-tariff/fetch-billing-accounts.service.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) | ||
}) |