Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Cruikshanks committed Feb 28, 2025
1 parent 6eb9645 commit f764134
Show file tree
Hide file tree
Showing 10 changed files with 883 additions and 11 deletions.
15 changes: 8 additions & 7 deletions app/lib/general.lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,15 @@ function timestampForPostgres() {
/**
* Compare key properties of 2 transactions and determine if they are a 'match'
*
* We compare those properties which determine the charge value calculated by the charging module. If the properties
* are the same we return true. Else we return false.
* We compare those properties which determine the charge value calculated by the charging module. If the properties are
* the same we return true. Else we return false.
*
* This is used in the billing engines to determine 2 transactions within the same bill, often a debit and a credit,
* and whether they match. If they do we don't send either to the charge module or include them in the bill as they
* 'cancel' each other out.
* This is used in the billing engines to determine 2 transactions within the same bill, often a debit and a credit, and
* whether they match. If they do we don't send either to the charge module or include them in the bill as they 'cancel'
* each other out.
*
* The key properties are charge type, category code, and billable days. But we also need to compare agreements and
* additional charges because if those have changed, we need to credit the previous transaction and calculate the
* The key properties are charge type, category code, billable days, and volume. But we also need to compare agreements
* and additional charges because if those have changed, we need to credit the previous transaction and calculate the
* new debit value.
*
* Because what we are checking does not match up to what you see in the UI we have this reference
Expand Down Expand Up @@ -242,6 +242,7 @@ function transactionsMatch(left, right) {
left.chargeType === right.chargeType &&
left.chargeCategoryCode === right.chargeCategoryCode &&
left.billableDays === right.billableDays &&
left.volume === right.volume &&
left.section126Factor === right.section126Factor &&
left.section127Agreement === right.section127Agreement &&
left.section130Agreement === right.section130Agreement &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use strict'

/**
* Fetches the previously billed transactions that match, removing any debits cancelled out by previous credits
* @module FetchPreviousTransactionsService
*/

const { db } = require('../../../../db/db.js')
const { transactionsMatch } = require('../../../lib/general.lib.js')
const TransactionModel = require('../../../models/transaction.model.js')

/**
* Fetches the previously billed transactions that match, removing any debits cancelled out by previous credits
*
* @param {string} billingAccountId - The UUID that identifies the billing account we need to fetch transactions for
* @param {string} licenceId - The UUID that identifies the licence we need to fetch transactions for
* @param {number} financialYearEnding - The year the financial billing period ends that we need to fetch transactions
* for
*
* @returns {Promise<object[]>} The resulting matched transactions
*/
async function go(billingAccountId, licenceId, financialYearEnding) {
const transactions = await _fetch(billingAccountId, licenceId, financialYearEnding)

return _cleanse(transactions)
}

/**
* Cleanse the transactions by cancelling out matching pairs of debits and credits, and return the remaining debits.
*
* If a credit matches to a debit then its something that was dealt with in a previous supplementary bill run. We need
* to know only about debits that have not been credited.
*
* @private
*/
function _cleanse(transactions) {
const credits = transactions.filter((transaction) => {
return transaction.credit
})
const debits = transactions.filter((transaction) => {
return !transaction.credit
})

credits.forEach((credit) => {
const debitIndex = debits.findIndex((debit) => {
return transactionsMatch(debit, credit)
})

if (debitIndex > -1) {
debits.splice(debitIndex, 1)
}
})

return debits
}

async function _fetch(billingAccountId, licenceId, financialYearEnding) {
return TransactionModel.query()
.select([
'transactions.authorisedDays',
'transactions.billableDays',
'transactions.waterUndertaker',
'transactions.chargeReferenceId',
'transactions.startDate',
'transactions.endDate',
'transactions.source',
'transactions.season',
'transactions.loss',
'transactions.credit',
'transactions.chargeType',
'transactions.authorisedQuantity',
'transactions.billableQuantity',
'transactions.description',
'transactions.volume',
'transactions.section126Factor',
'transactions.section127Agreement',
'transactions.secondPartCharge',
'transactions.scheme',
'transactions.aggregateFactor',
'transactions.adjustmentFactor',
'transactions.chargeCategoryCode',
'transactions.chargeCategoryDescription',
'transactions.supportedSource',
'transactions.supportedSourceName',
'transactions.newLicence',
'transactions.waterCompanyCharge',
'transactions.winterOnly',
'transactions.purposes',
// NOTE: The section130Agreement field is a varchar in the DB for historic reasons. It seems some early PRESROC
// transactions recorded values other than 'true' or 'false'. For SROC though, it will only ever be true/false. We
// generate our calculated billing transaction lines based on the Section130 flag against charge_elements which is
// always a boolean. So, to avoid issues when we need to compare the values we cast this to a boolean when
// fetching the data.
db.raw('transactions.section_130_agreement::boolean')
])
.innerJoin('billLicences', 'transactions.billLicenceId', 'billLicences.id')
.innerJoin('bills', 'billLicences.billId', 'bills.id')
.innerJoin('billRuns', 'bills.billRunId', 'billRuns.id')
.where({
'billLicences.licenceId': licenceId,
'bills.billingAccountId': billingAccountId,
'bills.financialYearEnding': financialYearEnding,
'billRuns.status': 'sent',
'billRuns.scheme': 'sroc'
})
}

module.exports = {
go
}
152 changes: 152 additions & 0 deletions app/services/bill-runs/tpt-supplementary/generate-bill-run.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
'use strict'

/**
* Generates a tpt supplementary bill run after the users have completed reviewing its match & allocate results
* @module GenerateBillRunService
*/

const BillRunError = require('../../../errors/bill-run.error.js')
const BillRunModel = require('../../../models/bill-run.model.js')
const ChargingModuleGenerateBillRunRequest = require('../../../requests/charging-module/generate-bill-run.request.js')
const ExpandedError = require('../../../errors/expanded.error.js')
const {
calculateAndLogTimeTaken,
currentTimeInNanoseconds,
timestampForPostgres
} = require('../../../lib/general.lib.js')
const FetchBillingAccountsService = require('./fetch-billing-accounts.service.js')
const HandleErroredBillRunService = require('../handle-errored-bill-run.service.js')
const LegacyRefreshBillRunRequest = require('../../../requests/legacy/refresh-bill-run.request.js')
const ProcessBillingPeriodService = require('./process-billing-period.service.js')

/**
* Generates a tpt supplementary bill run after the users have completed reviewing its match & allocate results
*
* In this case, "generate" means that we create the required bills and transactions for them in both this service and
* the Charging Module.
*
* > In the other bill run types this would be the `ProcessBillRunService` but that has already been used to handle the
* > match and allocate process in tpt supplementary
*
* We first fetch all the billing accounts applicable to this bill run and their charge information. We pass these to
* `ProcessBillingPeriodService` which will generate the bill for each billing account both in WRLS and the
* {@link https://github.com/DEFRA/sroc-charging-module-api | Charging Module API (CHA)}.
*
* Once `ProcessBillingPeriodService` is complete we tell the CHA to generate the bill run (this calculates final values
* for each bill and the bill run overall).
*
* The final step is to ping the legacy
* {@link https://github.com/DEFRA/water-abstraction-service | water-abstraction-service} 'refresh' endpoint. That
* service will extract the final values from the CHA and update the records on our side, finally marking the bill run
* as **Ready**.
*
* @param {module:BillRunModel} billRunId - The UUID of the tpt supplementary bill run that has been reviewed and is
* ready for generating
*/
async function go(billRunId) {
const billRun = await _fetchBillRun(billRunId)

if (billRun.status !== 'review') {
throw new ExpandedError('Cannot process a two-part tariff supplementary bill run that is not in review', {
billRunId
})
}

await _updateStatus(billRunId, 'processing')

// NOTE: We do not await this call intentionally. We don't want to block the user while we generate the bill run
_generateBillRun(billRun)
}

/**
* Unlike other bill runs where the bill run itself and the bills are generated in one process, two-part tariff is
* split. The first part which matches and allocates charge information to returns create's the bill run itself. This
* service handles the second part where we create the bills using the match and allocate data.
*
* This means we've already determined the billing period and recorded it against the bill run. So, we retrieve it from
* the bill run rather than pass it into the service.
*
* @private
*/
function _billingPeriod(billRun) {
const { toFinancialYearEnding } = billRun

return {
startDate: new Date(`${toFinancialYearEnding - 1}-04-01`),
endDate: new Date(`${toFinancialYearEnding}-03-31`)
}
}

async function _fetchBillingAccounts(billRunId) {
try {
return await FetchBillingAccountsService.go(billRunId)
} catch (error) {
// We know we're saying we failed to process charge versions. But we're stuck with the legacy error codes and this
// is the closest one related to what stage we're at in the process
throw new BillRunError(error, BillRunModel.errorCodes.failedToProcessChargeVersions)
}
}

async function _fetchBillRun(billRunId) {
return BillRunModel.query()
.findById(billRunId)
.select(['id', 'batchType', 'createdAt', 'externalId', 'regionId', 'scheme', 'status', 'toFinancialYearEnding'])
}

async function _finaliseBillRun(billRun, billRunPopulated) {
// If there are no bill licences then the bill run is considered empty. We just need to set the status to indicate
// this in the UI
if (!billRunPopulated) {
await _updateStatus(billRun.id, 'empty')

return
}

// We now need to tell the Charging Module to run its generate process. This is where the Charging module finalises
// the debit and credit amounts, and adds any additional transactions needed, for example, minimum charge
await ChargingModuleGenerateBillRunRequest.send(billRun.externalId)

await LegacyRefreshBillRunRequest.send(billRun.id)
}

/**
* The go() has to deal with updating the status of the bill run and then passing a response back to the request to
* avoid the user seeing a timeout in their browser. So, this is where we actually generate the bills and record the
* time taken.
*
* @private
*/
async function _generateBillRun(billRun) {
const { id: billRunId } = billRun

try {
const startTime = currentTimeInNanoseconds()

const billingPeriod = _billingPeriod(billRun)

await _processBillingPeriod(billingPeriod, billRun)

calculateAndLogTimeTaken(startTime, 'Process bill run complete', { billRunId })
} catch (error) {
await HandleErroredBillRunService.go(billRunId, error.code)
global.GlobalNotifier.omfg('Bill run process errored', { billRun }, error)
}
}

async function _processBillingPeriod(billingPeriod, billRun) {
const { id: billRunId } = billRun

const billingAccounts = await _fetchBillingAccounts(billRunId)

const billRunPopulated = await ProcessBillingPeriodService.go(billRun, billingPeriod, billingAccounts)

await _finaliseBillRun(billRun, billRunPopulated)
}

async function _updateStatus(billRunId, status) {
return BillRunModel.query().findById(billRunId).patch({ status, updatedAt: timestampForPostgres() })
}

module.exports = {
go
}
Loading

0 comments on commit f764134

Please sign in to comment.