Skip to content

Commit

Permalink
Enable 2PT supplementary bill run to be created (#1748)
Browse files Browse the repository at this point in the history
https://eaflood.atlassian.net/browse/WATER-4201

> Part of the work to support two-part tariff bill runs

Prior to this change, we'd updated the set-up bill run to include an option for two-part tariff supplementary, and added all the checks to ensure it was possible.

However, we have some temporary code that causes the engine to do nothing when the create button is clicked on the `/check` bill run page.

This change removes that. We also add a service to manage the processing of the bill run, and update some of the supporting services to allow this to happen.
  • Loading branch information
Cruikshanks authored Feb 26, 2025
1 parent fbee1a9 commit d977580
Show file tree
Hide file tree
Showing 11 changed files with 479 additions and 162 deletions.
4 changes: 2 additions & 2 deletions app/services/bill-runs/determine-billing-periods.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ function _billingPeriods(billRunType, financialYear) {

const years = { startYear: financialYear.startDate.getFullYear(), endYear: financialYear.endDate.getFullYear() }

// Annual and two-part-tariff bill runs will always be a single period
if (['annual', 'two_part_tariff'].includes(billRunType)) {
// Annual, two-part tariff, and two-part tariff supplementary bill runs will always be a single period
if (['annual', 'two_part_tariff', 'two_part_supplementary'].includes(billRunType)) {
_addBillingPeriod(billingPeriods, years.startYear, years.endYear)

return billingPeriods
Expand Down
27 changes: 24 additions & 3 deletions app/services/bill-runs/match/fetch-charge-versions.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { ref } = require('objection')

const ChargeReferenceModel = require('../../../models/charge-reference.model.js')
const ChargeVersionModel = require('../../../models/charge-version.model.js')
const LicenceSupplementaryYearModel = require('../../../models/licence-supplementary-year.model.js')
const Workflow = require('../../../models/workflow.model.js')

/**
Expand All @@ -27,15 +28,17 @@ const Workflow = require('../../../models/workflow.model.js')
*
* @param {string} regionId - UUID of the region being billed
* @param {object} billingPeriod - Object with a `startDate` and `endDate` property representing the period being billed
* @param {boolean} [supplementary=false] - flag to indicate if an annual or supplementary two-part tariff bill run is
* being created
*
* @returns {Promise<object>} Contains an array of two-part tariff charge versions with linked licences, charge
* references, charge elements and related purpose
*/
async function go(regionId, billingPeriod) {
return _fetch(regionId, billingPeriod)
async function go(regionId, billingPeriod, supplementary = false) {
return _fetch(regionId, billingPeriod, supplementary)
}

async function _fetch(regionId, billingPeriod) {
async function _fetch(regionId, billingPeriod, supplementary) {
const chargeVersions = await ChargeVersionModel.query()
.select(['chargeVersions.id', 'chargeVersions.startDate', 'chargeVersions.endDate', 'chargeVersions.status'])
.innerJoinRelated('licence')
Expand Down Expand Up @@ -70,6 +73,24 @@ async function _fetch(regionId, billingPeriod) {
// rather than have to remember that quirk we stick with whereJsonPath() which works in all cases.
.whereJsonPath('chargeReferences.adjustments', '$.s127', '=', true)
)
.where((builder) => {
// NOTE: Ordinarily we'd assign the query to a variable, then use `supplementary` to add the `where` clause to it.
// But because the query is already very complex, breaking it apart to allow us to add this `where` clause in the
// middle doesn't seem worth it, when we can just use a little SQL magic!
//
// If `supplementary` is false (we're getting the charge versions for an annual 2PT) we basically tack on a
// `where TRUE` clause to the query, i.e. the clause does nothing.
if (supplementary) {
builder.whereExists(
LicenceSupplementaryYearModel.query()
.select(1)
.whereColumn('licenceSupplementaryYears.licenceId', 'chargeVersions.licenceId')
.whereColumn('licenceSupplementaryYears.financialYearEnd', billingPeriod.endDate.getFullYear())
)
} else {
builder.whereRaw('1=1')
}
})
.orderBy('chargeVersions.licenceRef', 'asc')
.withGraphFetched('changeReason')
.modifyGraph('changeReason', (builder) => {
Expand Down
6 changes: 4 additions & 2 deletions app/services/bill-runs/match/fetch-licences.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ const FetchChargeVersionsService = require('./fetch-charge-versions.service.js')
*
* @param {string} regionId - UUID of the region being billed that the licences must be linked to
* @param {object} billingPeriod - Object with a `startDate` and `endDate` property representing the period being billed
* @param {boolean} [supplementary=false] - flag to indicate if an annual or supplementary two-part tariff bill run is
* being created
*
* @returns {Promise<object[]>} the licences to be matched, each containing an array of charge versions applicable for
* two-part tariff
*/
async function go(regionId, billingPeriod) {
const chargeVersions = await FetchChargeVersionsService.go(regionId, billingPeriod)
async function go(regionId, billingPeriod, supplementary = false) {
const chargeVersions = await FetchChargeVersionsService.go(regionId, billingPeriod, supplementary)

const uniqueLicenceIds = _extractUniqueLicenceIds(chargeVersions)

Expand Down
3 changes: 2 additions & 1 deletion app/services/bill-runs/match/match-and-allocate.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const PrepareReturnLogsService = require('./prepare-return-logs.service.js')
* @returns {Promise<boolean>} - True if there are any licences matched to returns, false otherwise
*/
async function go(billRun, billingPeriod) {
const licences = await FetchLicencesService.go(billRun.regionId, billingPeriod)
const supplementary = billRun.batchType === 'two_part_supplementary'
const licences = await FetchLicencesService.go(billRun.regionId, billingPeriod, supplementary)

if (licences.length > 0) {
await _process(licences, billingPeriod, billRun)
Expand Down
6 changes: 1 addition & 5 deletions app/services/bill-runs/setup/submit-check.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,7 @@ async function go(sessionId, auth) {
// blocking bill run was found. This is just protection against malicious use, or more likely, someone has left the
// page idle and another user has triggered a bill run that now blocks it.
if (blockingResults.trigger !== engineTriggers.neither) {
// Temporary code to end the journey if the bill run type is two-part supplementary as processing this bill run type
// is not currently possible
if (session.type !== 'two_part_supplementary') {
await CreateService.go(session, blockingResults, auth.credentials.user)
}
await CreateService.go(session, blockingResults, auth.credentials.user)

return {}
}
Expand Down
4 changes: 4 additions & 0 deletions app/services/bill-runs/start-bill-run-process.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const InitiateBillRunService = require('./initiate-bill-run.service.js')
const NoBillingPeriodsError = require('../../errors/no-billing-periods.error.js')
const SupplementaryProcessBillRunService = require('./supplementary/process-bill-run.service.js')
const TwoPartTariffProcessBillRunService = require('./two-part-tariff/process-bill-run.service.js')
const TwoPartTariffSupplementaryProcessBillRunService = require('./tpt-supplementary/process-bill-run.service.js')

/**
* Manages the creation of a new bill run
Expand Down Expand Up @@ -55,6 +56,9 @@ function _processBillRun(billRun, billingPeriods) {
case 'two_part_tariff':
TwoPartTariffProcessBillRunService.go(billRun, billingPeriods)
break
case 'two_part_supplementary':
TwoPartTariffSupplementaryProcessBillRunService.go(billRun, billingPeriods)
break
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use strict'

/**
* Process a two-part tariff supplementary bill run for the given billing period
* @module ProcessBillRunService
*/

const BillRunModel = require('../../../models/bill-run.model.js')
const { calculateAndLogTimeTaken, currentTimeInNanoseconds } = require('../../../lib/general.lib.js')
const HandleErroredBillRunService = require('../handle-errored-bill-run.service.js')
const MatchAndAllocateService = require('../match/match-and-allocate.service.js')

/**
* Process a two-part tariff supplementary bill run for the given billing period
*
* Matches and allocates licences to returns for a two-part tariff bill run
*
* The results of the matching process are then persisted to the database ready for the results to be reviewed. The bill
* run status is also updated to 'review'.
*
* In the unlikely event of no licences match to returns it will set the status to 'empty'. It will also handle updating
* the bill run if an error occurs during the process.
*
* @param {module:BillRunModel} billRun - The two-part tariff supplementary bill run being processed
* @param {object[]} billingPeriods - An array of billing periods each containing a `startDate` and `endDate`. For 2PT
* this will only ever contain a single period
*/
async function go(billRun, billingPeriods) {
const { id: billRunId } = billRun
// NOTE: billingPeriods come from `DetermineBillingPeriodsService` which always returns an array because it is used by
// all billing types. For two-part tariff we know it will only contain one because 2PT supplementary bill runs are
// only for a single financial year
const billingPeriod = billingPeriods[0]

try {
const startTime = currentTimeInNanoseconds()

await _updateStatus(billRunId, 'processing')

// `populated` will be set to true if `MatchAndAllocateService` processes at least one licence
const populated = await MatchAndAllocateService.go(billRun, billingPeriod)

await _setBillRunStatus(billRunId, populated)

calculateAndLogTimeTaken(startTime, 'Process bill run complete', { billRunId, type: 'two_part_supplementary' })
} catch (error) {
await HandleErroredBillRunService.go(billRunId)
global.GlobalNotifier.omfg('Process bill run failed', { billRun }, error)
}
}

async function _setBillRunStatus(billRunId, populated) {
// It is highly unlikely no licences were matched to returns. So we default status to 'review'
let status = 'review'

// Just in case no licences were found to be matched to returns we set the status to 'empty'
if (!populated) {
status = 'empty'
}

// Update the bill run's status
await _updateStatus(billRunId, status)
}

async function _updateStatus(billRunId, status) {
await BillRunModel.query().findById(billRunId).patch({ status })
}

module.exports = {
go
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const { expect } = Code
// Thing under test
const DetermineBillingPeriodsService = require('../../../app/services/bill-runs/determine-billing-periods.service.js')

describe('Determine Billing Periods service', () => {
describe('Bill Runs - Determine Billing Periods service', () => {
let billRunType
let financialYearEnding

Expand Down Expand Up @@ -48,6 +48,23 @@ describe('Determine Billing Periods service', () => {
})
})

describe('when the bill run type is "two_part_supplementary"', () => {
beforeEach(() => {
billRunType = 'two_part_supplementary'
financialYearEnding = 2024
})

it('returns a single billing period for the financial year provided', () => {
const result = DetermineBillingPeriodsService.go(billRunType, financialYearEnding)

expect(result).to.have.length(1)
expect(result[0]).to.equal({
startDate: new Date('2023-04-01'),
endDate: new Date('2024-03-31')
})
})
})

describe('when the bill run type is "supplementary"', () => {
beforeEach(() => {
billRunType = 'supplementary'
Expand Down
Loading

0 comments on commit d977580

Please sign in to comment.