diff --git a/app/lib/general.lib.js b/app/lib/general.lib.js index ba9be77e93..a1b4f3652f 100644 --- a/app/lib/general.lib.js +++ b/app/lib/general.lib.js @@ -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 @@ -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 && diff --git a/app/services/bill-runs/tpt-supplementary/fetch-previous-transactions.service.js b/app/services/bill-runs/tpt-supplementary/fetch-previous-transactions.service.js new file mode 100644 index 0000000000..324122466e --- /dev/null +++ b/app/services/bill-runs/tpt-supplementary/fetch-previous-transactions.service.js @@ -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} 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 +} diff --git a/app/services/bill-runs/tpt-supplementary/generate-bill-run.service.js b/app/services/bill-runs/tpt-supplementary/generate-bill-run.service.js new file mode 100644 index 0000000000..89c5bb54f8 --- /dev/null +++ b/app/services/bill-runs/tpt-supplementary/generate-bill-run.service.js @@ -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 +} diff --git a/app/services/bill-runs/tpt-supplementary/generate-transaction.service.js b/app/services/bill-runs/tpt-supplementary/generate-transaction.service.js new file mode 100644 index 0000000000..3b4db1e3fa --- /dev/null +++ b/app/services/bill-runs/tpt-supplementary/generate-transaction.service.js @@ -0,0 +1,136 @@ +'use strict' + +/** + * Generate tpt supplementary transaction data from the the charge reference and other information passed in + * @module GenerateTransactionService + */ + +const { generateUUID } = require('../../../lib/general.lib.js') + +/** + * Generate tpt supplementary transaction data from the the charge reference and other information passed in + * + * Unlike a standard transaction, we don't have to calculate the billing days for the transaction. Instead, two-part + * tariff transactions focus on volume. This information comes from the review data that results after the match & + * allocate results have been checked and amended by users. + * + * As well as allocated volume, users can override in the review + * + * - the authorised volume + * - the aggregate + * - the charge adjustment + * + * So, we have to grab those values as well. Finally, because the standard annual bill run will have handled the + * compensation charge we don't have to generate an additional transaction alongside our two-part tariff one. + * + * @param {string} billLicenceId - The UUID of the bill licence the transaction will be linked to + * @param {module:ChargeReferenceModel} chargeReference - The charge reference the transaction generated will be + * generated from + * @param {object} chargePeriod - A start and end date representing the charge period for the transaction + * @param {boolean} newLicence - Whether the charge reference is linked to a new licence + * @param {boolean} waterUndertaker - Whether the charge reference is linked to a water undertaker licence + * + * @returns {object} the tpt supplementary transaction + */ +function go(billLicenceId, chargeReference, chargePeriod, newLicence, waterUndertaker) { + const billableQuantity = _billableQuantity(chargeReference.chargeElements) + + if (billableQuantity === 0) { + return null + } + + return _standardTransaction( + billLicenceId, + billableQuantity, + chargeReference, + chargePeriod, + newLicence, + waterUndertaker + ) +} + +function _billableQuantity(chargeElements) { + return chargeElements.reduce((total, chargeElement) => { + total += chargeElement.reviewChargeElements[0].amendedAllocated + + return total + }, 0) +} + +function _description(chargeReference) { + // If the value is false, undefined, null or simply doesn't exist we return the standard description + if (!chargeReference.adjustments.s127) { + return `Water abstraction charge: ${chargeReference.description}` + } + + return `Two-part tariff second part water abstraction charge: ${chargeReference.description}` +} + +/** + * Returns a json representation of all charge elements in a charge reference + * + * @private + */ +function _generateElements(chargeReference) { + const jsonChargeElements = chargeReference.chargeElements.map((chargeElement) => { + delete chargeElement.reviewChargeElements + + return chargeElement.toJSON() + }) + + return JSON.stringify(jsonChargeElements) +} + +/** + * Generates a standard transaction based on the supplied data, along with some default fields (eg. status) + * + * @private + */ +function _standardTransaction( + billLicenceId, + billableQuantity, + chargeReference, + chargePeriod, + newLicence, + waterUndertaker +) { + return { + id: generateUUID(), + billLicenceId, + authorisedDays: 0, + billableDays: 0, + newLicence, + waterUndertaker, + chargeReferenceId: chargeReference.id, + startDate: chargePeriod.startDate, + endDate: chargePeriod.endDate, + source: chargeReference.source, + season: 'all year', + loss: chargeReference.loss, + credit: false, + chargeType: 'standard', + authorisedQuantity: chargeReference.reviewChargeReferences[0].amendedAuthorisedVolume, + billableQuantity, + status: 'candidate', + description: _description(chargeReference), + volume: billableQuantity, + section126Factor: Number(chargeReference.adjustments.s126) || 1, + section127Agreement: !!chargeReference.adjustments.s127, + section130Agreement: !!chargeReference.adjustments.s130, + secondPartCharge: true, + scheme: 'sroc', + aggregateFactor: chargeReference.reviewChargeReferences[0].amendedAggregate, + adjustmentFactor: chargeReference.reviewChargeReferences[0].amendedChargeAdjustment, + chargeCategoryCode: chargeReference.chargeCategory.reference, + chargeCategoryDescription: chargeReference.chargeCategory.shortDescription, + supportedSource: !!chargeReference.additionalCharges?.supportedSource?.name, + supportedSourceName: chargeReference.additionalCharges?.supportedSource?.name || null, + waterCompanyCharge: !!chargeReference.additionalCharges?.isSupplyPublicWater, + winterOnly: !!chargeReference.adjustments.winter, + purposes: _generateElements(chargeReference) + } +} + +module.exports = { + go +} diff --git a/app/services/bill-runs/tpt-supplementary/process-billing-period.service.js b/app/services/bill-runs/tpt-supplementary/process-billing-period.service.js new file mode 100644 index 0000000000..4d7502342b --- /dev/null +++ b/app/services/bill-runs/tpt-supplementary/process-billing-period.service.js @@ -0,0 +1,314 @@ +'use strict' + +/** + * Process the billing accounts for a given billing period and creates their supplementary two-part tariff bills + * @module ProcessBillingPeriodService + */ + +const BillRunError = require('../../../errors/bill-run.error.js') +const BillRunModel = require('../../../models/bill-run.model.js') +const BillModel = require('../../../models/bill.model.js') +const BillLicenceModel = require('../../../models/bill-licence.model.js') +const DetermineChargePeriodService = require('../determine-charge-period.service.js') +const DetermineMinimumChargeService = require('../determine-minimum-charge.service.js') +const { generateUUID } = require('../../../lib/general.lib.js') +const GenerateTransactionService = require('./generate-transaction.service.js') +const ProcessTransactionsService = require('./process-transactions.service.js') +const SendTransactionsService = require('../send-transactions.service.js') +const TransactionModel = require('../../../models/transaction.model.js') + +const BillingConfig = require('../../../../config/billing.config.js') + +/** + * Process the billing accounts for a given billing period and creates their supplementary two-part tariff bills + * + * @param {module:BillRunModel} billRun - The two-part tariff supplementary bill run we need to process + * @param {object} billingPeriod - An object representing the financial year the bills will be for + * @param {module:BillingAccountModel[]} billingAccounts - The billing accounts to create bills for + * + * @returns {Promise} true if the bill run is not empty (there are transactions to bill) else false + */ +async function go(billRun, billingPeriod, billingAccounts) { + let billRunIsPopulated = false + + if (billingAccounts.length === 0) { + return billRunIsPopulated + } + + // We set the batch size and number of billing accounts here rather than determine them for every iteration of the + // loop. It's a very minor nod towards performance. + const batchSize = BillingConfig.annual.batchSize + const billingAccountsCount = billingAccounts.length + + // Loop through the billing accounts to be processed by the size of the batch. For example, if we have 100 billing + // accounts to process and the batch size is 10, we'll make 10 iterations of the loop + for (let i = 0; i < billingAccountsCount; i += batchSize) { + // Use slice(start, end) to extract the next batch of billing accounts to process. For example, if we have 100 + // billing accounts, a batch size of 10 then + // + // - 1st pass: slice(0, 10) will return billingAccounts[0] to billingAccounts[9] + // - 2nd pass: slice(10, 20) will return billingAccounts[10] to billingAccounts[19] + // + // Both the start and end params are zero-based indexes for the array being sliced. The bit that might confuse is + // end is not inclusive! + const accountsToProcess = billingAccounts.slice(i, i + batchSize) + + // NOTE: we purposefully loop through each billing account in the batch without awaiting them to be processed. This + // is for performance purposes. If our batch size is 10 we'll start processing one after the other. We then wait for + // all 10 to complete. The overall process time will only be that of the one that takes the longest. If we await + // instead the overall time will be the sum of the time to process each one. + const processes = accountsToProcess.map((accountToProcess) => { + return _processBillingAccount(accountToProcess, billRun, billingPeriod) + }) + + // _processBillingAccount() will return true (bill was created) else false (no bill created) for each billing + // account processed. Promise.all() will return these results as an array. Only one of them has to be true for us to + // mark the bill run as populated. The complication is we're not doing this once but multiple times as we iterate + // the billing accounts. As soon as we have flagged the bill run as populated we can stop checking. + const results = await Promise.all(processes) + + if (!billRunIsPopulated) { + billRunIsPopulated = results.some((result) => { + return result + }) + } + } + + return billRunIsPopulated +} + +/** + * Generate the bill licence and transaction records for a bill, then cleanse them so only billable remains + * + * For each billing account we need to create a bill (1-to-1). Linked to the billing account will be multiple charge + * versions which is what we actually use to calculate a bill. A charge version can have multiple charge references and + * for each one we need to generate a transaction line. + * + * The complication is we group transactions by licence (via the bill licence), not charge version. So, as we iterate + * the charge versions we have to determine if its for a licence that we have already generated a bill licence for, or + * if we have to create a new one. + * + * Once we have generated the bill licences and the transactions against them, we then need to 'cleanse' them for + * billing. + * + * - Those with no generated lines are simply removed (most likely some has ended the licence so the charge versions + * used to generate the transactions no longer applied) + * - Those which when the generated lines are 'paired' to previously billed lines results in nothing needing to be + * billed + * + * From those that remain, we extract the bill licences and transactions to allow us to 'batch' the persist queries + * we fire at the DB. + * + * @private + */ +async function _generateBillLicencesAndTransactions(billId, billingAccount, billingPeriod) { + const allBillLicences = [] + + for (const chargeVersion of billingAccount.chargeVersions) { + const billLicence = _findOrCreateBillLicence(allBillLicences, chargeVersion.licence, billId) + + const generatedTransactions = await _generateTransactions(billLicence.id, billingPeriod, chargeVersion) + + billLicence.transactions.push(...generatedTransactions) + } + + return allBillLicences +} + +async function _processBillLicences(billLicences, billingAccountId, billingPeriod) { + const financialYearEnding = billingPeriod.endDate.getFullYear() + const cleansedBillLicences = [] + + for (const billLicence of billLicences) { + if (billLicence.transactions.length === 0) { + continue + } + + const { id: billLicenceId, licence, transactions } = billLicence + const processedTransactions = await ProcessTransactionsService.go( + transactions, + billLicenceId, + billingAccountId, + licence.id, + financialYearEnding + ) + + if (processedTransactions.length === 0) { + continue + } + + billLicence.transactions = processedTransactions + + cleansedBillLicences.push(billLicence) + } + + return cleansedBillLicences +} + +/** + * Intended to be used in conjunction with _generateBillLicencesAndTransactions() it extracts only those bill licences + * where + * + * - we generated transactions + * - after comparing them to previously billed transactions a difference is found requiring us to generate a bill + * + * Along with the bill licences we extract the transactions + * we generated transactions. This avoids us persisting a bill licence record with no transaction records. + * + * A billing account can be linked to multiple licences but not all of them may be billable. We add a flag to each + * one that denotes if transactions were generated so we can easily filter the billable ones out. But we also need + * to remove that flag because it doesn't exist in the DB and will cause issues if we try and persist the object. + * + * @private + */ + +/** + * Use to either find or create the bill licence for a given bill (billing account) and licence + * + * For each billing account being processed the engine will create a bill. But the transactions within a bill are + * grouped by licence because the details of the licence will effect the charge against the transactions. + * + * So, transactions are linked to a bill by a 'bill licence' record. Things are further complicated by the fact we don't + * directly work with the licences against a billing account. The link is via charge versions, and more than one charge + * version can link to the same licence. + * + * > The information needed for billing is held in charge versions and their associated records. A licence will have at + * > least one of these but may have more should changes have been made, for example, a change in the amount that can be + * > abstracted. It's the charge version that is directly linked to the billing account, not the licence. + * + * Because we're processing the charge versions for a billing account the same licence might appear more than once. This + * means we need to find the existing bill licence record or if one doesn't exist create it. + * + * @param {object[]} billLicences - The existing bill licences created for the bill being generated + * @param {object} licence - the licence we're looking for an existing bill licence record + * @param {string} billId - the ID of the bill we're creating + * + * @return {object} returns either an existing bill licence or a new one for the licence and bill being generated + * + * @private + */ +function _findOrCreateBillLicence(billLicences, licence, billId) { + let billLicence = billLicences.find((existingBillLicence) => { + return existingBillLicence.licence.id === licence.id + }) + + if (!billLicence) { + billLicence = { + id: generateUUID(), + billId, + licence, + transactions: [] + } + + billLicences.push(billLicence) + } + + return billLicence +} + +/** + * Generate the transaction(s) for a charge version + * + * For each charge reference linked to the charge version we have to generate a transaction record. One of the things we + * need to know is if the charge version is the first charge on a new licence. This information needs to be passed to + * the Charging Module API as it affects the calculation. + * + * This function iterates the charge references generating transaction(s) for each one. + * + * @private + */ +async function _generateTransactions(billLicenceId, billingPeriod, chargeVersion) { + try { + const chargePeriod = DetermineChargePeriodService.go(chargeVersion, billingPeriod) + + // Guard clause against invalid charge periods, for example, a licence 'ends' before the charge version starts + if (!chargePeriod.startDate) { + return [] + } + + const firstChargeOnNewLicence = DetermineMinimumChargeService.go(chargeVersion, chargePeriod) + + const transactions = [] + + chargeVersion.chargeReferences.forEach((chargeReference) => { + const transaction = GenerateTransactionService.go( + billLicenceId, + chargeReference, + chargePeriod, + firstChargeOnNewLicence, + chargeVersion.licence.waterUndertaker + ) + + if (transaction) { + transactions.push(transaction) + } + }) + + return transactions + } catch (error) { + throw new BillRunError(error, BillRunModel.errorCodes.failedToPrepareTransactions) + } +} + +/** + * Processes the billing account + * + * We create a bill object that will eventually be persisted for the billing account. We then call + * `_createBillLicencesAndTransactions()` which handles generating the bill licences and transactions that will form + * our bill. + * + * Once everything has been generated we persist the results to the DB. + * + * @private + */ +async function _processBillingAccount(billingAccount, billRun, billingPeriod) { + const { id: billingAccountId, accountNumber } = billingAccount + const { id: billRunId, externalId: billRunExternalId } = billRun + + const bill = { + id: generateUUID(), + accountNumber, + address: {}, // Address is set to an empty object for SROC billing invoices + billingAccountId, + billRunId, + credit: false, + financialYearEnding: billingPeriod.endDate.getFullYear() + } + + const allBillLicences = await _generateBillLicencesAndTransactions(bill.id, billingAccount, billingPeriod) + const processedBillLicences = await _processBillLicences(allBillLicences, billingAccount.id, billingPeriod) + + if (processedBillLicences.length === 0) { + return false + } + + return _persistGeneratedData(bill, processedBillLicences, billRunExternalId, billingAccount.accountNumber) +} + +async function _persistGeneratedData(bill, processedBillLicences, billRunExternalId, accountNumber) { + const billLicences = [] + const transactions = [] + + for (const processedBillLicence of processedBillLicences) { + const { billId, id, licence, transactions: processedTransactions } = processedBillLicence + const sentTransactions = await SendTransactionsService.go( + processedTransactions, + billRunExternalId, + accountNumber, + licence + ) + + billLicences.push({ billId, id, licenceId: licence.id, licenceRef: licence.licenceRef }) + transactions.push(...sentTransactions) + } + + await BillModel.query().insert(bill) + await BillLicenceModel.query().insert(billLicences) + await TransactionModel.query().insert(transactions) + + return true +} + +module.exports = { + go +} diff --git a/app/services/bill-runs/tpt-supplementary/process-transactions.service.js b/app/services/bill-runs/tpt-supplementary/process-transactions.service.js new file mode 100644 index 0000000000..813677d81b --- /dev/null +++ b/app/services/bill-runs/tpt-supplementary/process-transactions.service.js @@ -0,0 +1,111 @@ +'use strict' + +/** + * Fetches matching debit transactions from previous bill runs, then compares them as credits those just generated + * @module ProcessTransactionsService + */ + +const FetchPreviousTransactionsService = require('./fetch-previous-transactions.service.js') +const { transactionsMatch } = require('../../../lib/general.lib.js') +const ReverseTransactionsService = require('./reverse-transactions.service.js') + +/** + * Fetches matching debit transactions from previous bill runs, then compares them as credits those just generated + * + * The `FetchPreviousTransactionsService` finds all previous debit and credit transactions. It then compares the credits + * to the debits. If they match, the debit transaction is dropped. This is because it must have been dealt with by a + * previous supplementary bill run. + * + * Any debits that remain are then reversed as credits. These are then compared against the ones we've generated for the + * bill run in progress. + * + * If any matches occur here, both transactions are dropped. There is no point in sending them to the Charging Module + * API and including them in the bill, as the result will just be £0, and we've been asked to limit zero-value bills + * where we can. + * + * Any generated transactions and reversed debits (now credits) that didn't match are returned as the transactions to + * be sent to the Charging Module API and to be used in the bill. + * + * @param {object[]} generatedTransactions - The generated transactions for the bill licence being processed + * @param {string} billLicenceId - The UUID that will be used for the bill licence we are processing transactions for + * @param {string} billingAccountId - The UUID for the billing account we are processing transactions for and for which + * we need to fetch previous transactions + * @param {string} licenceId - The UUID for the licence we are processing transactions for and and for which we need to + * fetch previous transactions + * @param {number} financialYearEnding - Which financial year to look for previous transactions in + * + * @returns {Promise} An array of the remaining generated transactions and reversed debits from previous + * transactions (ie. those which were not cancelled out when the generated and reversed were compared) + */ +async function go(generatedTransactions, billLicenceId, billingAccountId, licenceId, financialYearEnding) { + const previousTransactions = await FetchPreviousTransactionsService.go( + billingAccountId, + licenceId, + financialYearEnding + ) + + if (previousTransactions.length === 0) { + return generatedTransactions + } + + const reversedTransactions = ReverseTransactionsService.go(previousTransactions, billLicenceId) + + return _cleanseTransactions(generatedTransactions, reversedTransactions) +} + +/** + * Compares a generated transaction to the transactions from previous bill runs + * + * Takes a single generated debit transaction and checks to see if the provided array of reversed (credit) transactions + * contains a transaction that will cancel it out, returning `true` or `false` to indicate if it does or doesn't. + * + * NOTE: This function will mutate the provided array of reversed transactions if one of the transactions in it will + * cancel the generated transaction; in this case, we remove the reversed transaction from the array as it can only + * cancel one generated transaction. + * + * @private + */ +function _cancelGeneratedTransaction(generatedTransaction, reversedTransactions) { + const result = reversedTransactions.findIndex((reversedTransaction) => { + return transactionsMatch(reversedTransaction, generatedTransaction) + }) + + if (result === -1) { + return false + } + + reversedTransactions.splice(result, 1) + + return true +} + +/** + * Remove any "cancelling pairs" of transaction lines. We define a "cancelling pair" as a pair of transactions belonging + * to the same bill licence which would send the same data to the Charging Module (and therefore return the same values) + * but with opposing credit flags -- in other words, a credit and a debit which cancel each other out. All remaining + * transactions (both generated transactions and reverse transactions) are returned. + * + * @private + */ +function _cleanseTransactions(generatedTransactions, reverseTransactions) { + const cleansedTransactionLines = [] + + // Iterate over each generated transaction to see if a transaction in the reverse transactions would form a + // "cancelling pair" with it. If not then add the unpaired generated transaction to our array of cleansed transaction + // lines. Note that `reverseTransactions` will be mutated to remove any reverse transactions which form a cancelling + // pair. + generatedTransactions.forEach((generatedTransaction) => { + if (!_cancelGeneratedTransaction(generatedTransaction, reverseTransactions)) { + cleansedTransactionLines.push(generatedTransaction) + } + }) + + // Add the remaining reverse transactions (ie. those which didn't form a cancelling pair) + cleansedTransactionLines.push(...reverseTransactions) + + return cleansedTransactionLines +} + +module.exports = { + go +} diff --git a/app/services/bill-runs/tpt-supplementary/reverse-transactions.service.js b/app/services/bill-runs/tpt-supplementary/reverse-transactions.service.js new file mode 100644 index 0000000000..5686bdf41d --- /dev/null +++ b/app/services/bill-runs/tpt-supplementary/reverse-transactions.service.js @@ -0,0 +1,36 @@ +'use strict' + +/** + * Takes an array of transactions and returns an array of transactions which will reverse them. + * @module ReverseTransactionsService + */ + +const { generateUUID } = require('../../../lib/general.lib.js') + +/** + * Takes an array of transactions and returns an array of transactions which will reverse them. + * + * In some situations we need to "reverse" transactions; this is done by issuing new transactions which cancel them out. + * This service takes an array of transactions and a bill licence, and returns an array of transactions which will + * reverse the original transactions, with their bill licence id set to the ID of the supplied billing licence. + * + * @param {module:TransactionModel[]} transactions - Array of transactions to be reversed + * @param {string} billLicenceId - The bill licence UUID these transactions are to be added to + * + * @returns {object[]} Array of reversed transactions with `billLicenceId` set to the id of the supplied `billLicence` + */ +function go(transactions, billLicenceId) { + return transactions.map((transaction) => { + return { + ...transaction, + id: generateUUID(), + billLicenceId, + credit: true, + status: 'candidate' + } + }) +} + +module.exports = { + go +} diff --git a/app/services/bill-runs/two-part-tariff/generate-transaction.service.js b/app/services/bill-runs/two-part-tariff/generate-transaction.service.js index a2f25a7f8b..8e6250d17e 100644 --- a/app/services/bill-runs/two-part-tariff/generate-transaction.service.js +++ b/app/services/bill-runs/two-part-tariff/generate-transaction.service.js @@ -1,7 +1,7 @@ 'use strict' /** - * Generate a two-part tariff transaction data from the the charge reference and other information passed in + * Generate two-part tariff transaction data from the the charge reference and other information passed in * @module GenerateTransactionService */ diff --git a/app/services/bill-runs/two-part-tariff/process-billing-period.service.js b/app/services/bill-runs/two-part-tariff/process-billing-period.service.js index b4a6fc20b7..d332554bfb 100644 --- a/app/services/bill-runs/two-part-tariff/process-billing-period.service.js +++ b/app/services/bill-runs/two-part-tariff/process-billing-period.service.js @@ -21,7 +21,7 @@ const BillingConfig = require('../../../../config/billing.config.js') /** * Process the billing accounts for a given billing period and creates their annual two-part tariff bills * - * @param {module:BillRunModel} billRun - The two-part tariff bill run we need to process + * @param {module:BillRunModel} billRun - The two-part tariff annual bill run we need to process * @param {object} billingPeriod - An object representing the financial year the bills will be for * @param {module:BillingAccountModel[]} billingAccounts - The billing accounts to create bills for * @@ -35,7 +35,7 @@ async function go(billRun, billingPeriod, billingAccounts) { } // We set the batch size and number of billing accounts here rather than determine them for every iteration of the - // loop. It's a very minor node towards performance. + // loop. It's a very minor nod towards performance. const batchSize = BillingConfig.annual.batchSize const billingAccountsCount = billingAccounts.length diff --git a/test/lib/general.lib.test.js b/test/lib/general.lib.test.js index e2de05f42c..77168c7c33 100644 --- a/test/lib/general.lib.test.js +++ b/test/lib/general.lib.test.js @@ -332,7 +332,7 @@ describe('GeneralLib', () => { let rightTransaction beforeEach(() => { - // NOTE: The properties the function is comparing are; chargeType, chargeCategoryCode, billableDays, + // NOTE: The properties the function is comparing are; chargeType, chargeCategoryCode, billableDays, volume, // section126Factor, section127Agreement, section130Agreement, aggregateFactor, adjustmentFactor, winterOnly, // supportedSource, supportedSourceName, waterCompanyCharge. // @@ -398,6 +398,18 @@ describe('GeneralLib', () => { }) }) + describe('because the volume is different', () => { + beforeEach(() => { + rightTransaction.volume = 10 + }) + + it('returns false', () => { + const result = GeneralLib.transactionsMatch(leftTransaction, rightTransaction) + + expect(result).to.be.false() + }) + }) + describe('because the canal and river trust agreement (section 130) is different', () => { beforeEach(() => { rightTransaction.section130Agreement = true