Skip to content

Commit

Permalink
Handle errors in ProcessBillingBatchService (#161)
Browse files Browse the repository at this point in the history
https://eaflood.atlassian.net/browse/WATER-3924

Should an error occur during the supplementary bill run process it brings down the service. We're not handling the exception and because we moved the process to the background in our bill runs controller, there is nothing to deal with it when thrown.

So, we need to protect the service from _any_ errors during the process, to prevent the service from going down. But we also need to mimic the legacy service and use an 'error code' to provide extra information about the error. We've already used this concept with [Create error bill run when CM fails](#104) where we apply a code to the `billing_batch` record which the UI then uses to indicate what happened to the user.

This change deals with both; ensuring we handle any exceptions raised and where possible, setting a relevant error code when updating the `billing_batch` status to 'errored'.
  • Loading branch information
Cruikshanks authored Mar 13, 2023
1 parent daceafc commit 6ce785e
Show file tree
Hide file tree
Showing 4 changed files with 383 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict'

/**
* Handles an errored billing batch (setting status etc.)
* @module HandleErroredBillingBatchService
*/

const BillingBatchModel = require('../../models/water/billing-batch.model.js')

/**
* Sets the status of the specified billing batch to `error`, and logs an error if this can't be done.
*
* We keep this in a separate service so we don't need to worry about multiple/nested try-catch blocks in cases where a
* billing batch fails and setting its status to error also fails.
*
* Note that although this is async we would generally not call it asyncronously as the intent is you can call it and
* continue with whatever error logging is required
*
* @param {string} billingBatchId UUID of the billing batch to be marked with `error` status
* @param {number} [errorCode] Numeric error code as defined in BillingBatchModel. Defaults to `null`
*/
async function go (billingBatchId, errorCode = null) {
try {
await _updateBillingBatch(billingBatchId, errorCode)
} catch (error) {
global.GlobalNotifier.omfg('Failed to set error status on billing batch', { error, billingBatchId, errorCode })
}
}

async function _updateBillingBatch (billingBatchId, errorCode) {
await BillingBatchModel.query()
.findById(billingBatchId)
.patch({
status: 'error',
errorCode
})
}

module.exports = {
go
}
230 changes: 149 additions & 81 deletions app/services/supplementary-billing/process-billing-batch.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ const FetchChargeVersionsService = require('./fetch-charge-versions.service.js')
const GenerateBillingTransactionsService = require('./generate-billing-transactions.service.js')
const GenerateBillingInvoiceService = require('./generate-billing-invoice.service.js')
const GenerateBillingInvoiceLicenceService = require('./generate-billing-invoice-licence.service.js')
const HandleErroredBillingBatchService = require('./handle-errored-billing-batch.service.js')
const LegacyRequestLib = require('../../lib/legacy-request.lib.js')

let generatedInvoices = []
let generatedInvoiceLicences = []

/**
* Creates the invoices and transactions in both WRLS and the Charging Module API
*
Expand All @@ -31,127 +35,185 @@ const LegacyRequestLib = require('../../lib/legacy-request.lib.js')
async function go (billingBatch, billingPeriod) {
const { billingBatchId } = billingBatch

await _updateStatus(billingBatchId, 'processing')
try {
await _updateStatus(billingBatchId, 'processing')

const chargeVersions = await _fetchChargeVersions(billingBatch, billingPeriod)

for (const chargeVersion of chargeVersions) {
const { licence } = chargeVersion

// We know in the future we will be calculating multiple billing periods and so will have to iterate through each,
// generating bill runs and reviewing if there is anything to bill. For now, whilst our knowledge of the process
// is low we are focusing on just the current financial year, and intending to ship a working version for just it.
// This is why we are only passing through the first billing period; we know there is only one!
const chargeVersions = await FetchChargeVersionsService.go(billingBatch.regionId, billingPeriod)
const billingInvoice = await _generateBillingInvoice(chargeVersion, billingBatchId, billingPeriod)
const billingInvoiceLicence = _generateBillingInvoiceLicence(billingInvoice, licence)

let generatedInvoices = []
let generatedInvoiceLicences = []
const transactionLines = _generateTransactionLines(billingPeriod, chargeVersion, billingBatchId)

await _createTransactionLines(
transactionLines,
billingPeriod,
billingInvoice,
billingInvoiceLicence,
chargeVersion,
billingBatch
)
}

for (const chargeVersion of chargeVersions) {
const { chargeElements, licence } = chargeVersion
await _finaliseBillingBatch(billingBatch)
} catch (error) {
global.GlobalNotifier.omfg('Billing Batch process errored', { billingBatch, error })
}
}

async function _generateBillingInvoice (chargeVersion, billingBatchId, billingPeriod) {
try {
const billingInvoiceData = await GenerateBillingInvoiceService.go(
generatedInvoices,
chargeVersion.invoiceAccountId,
billingBatchId,
billingPeriod.endDate.getFullYear()
)
generatedInvoices = billingInvoiceData.billingInvoices
const { billingInvoice } = billingInvoiceData

return billingInvoiceData.billingInvoice
} catch (error) {
HandleErroredBillingBatchService.go(billingBatchId)

throw error
}
}

function _generateBillingInvoiceLicence (billingInvoice, licence) {
try {
const billingInvoiceLicenceData = GenerateBillingInvoiceLicenceService.go(
generatedInvoiceLicences,
billingInvoiceData.billingInvoice.billingInvoiceId,
billingInvoice.billingInvoiceId,
licence
)
generatedInvoiceLicences = billingInvoiceLicenceData.billingInvoiceLicences
const { billingInvoiceLicence } = billingInvoiceLicenceData

if (chargeElements) {
const transactionLines = _generateTransactionLines(billingPeriod, chargeVersion)

if (transactionLines.length > 0) {
await _createTransactionLines(
transactionLines,
billingPeriod,
billingInvoice.invoiceAccountNumber,
billingInvoiceLicence.billingInvoiceLicenceId,
chargeVersion,
billingBatch.externalId
)

billingInvoice.persist = true
billingInvoiceLicence.persist = true
}
}

return billingInvoiceLicenceData.billingInvoiceLicence
} catch (error) {
HandleErroredBillingBatchService.go(billingInvoice.billingBatchId)

throw error
}
}

await _finaliseBillingBatch(billingBatch, generatedInvoices, generatedInvoiceLicences)
async function _fetchChargeVersions (billingBatch, billingPeriod) {
try {
// We know in the future we will be calculating multiple billing periods and so will have to iterate through each,
// generating bill runs and reviewing if there is anything to bill. For now, whilst our knowledge of the process
// is low we are focusing on just the current financial year, and intending to ship a working version for just it.
// This is why we are only passing through the first billing period; we know there is only one!
return await FetchChargeVersionsService.go(billingBatch.regionId, billingPeriod)
} catch (error) {
HandleErroredBillingBatchService.go(
billingBatch.billingBatchId,
BillingBatchModel.errorCodes.failedToProcessChargeVersions
)

throw error
}
}

async function _createTransactionLines (
transactionLines,
billingPeriod,
invoiceAccountNumber,
billingInvoiceLicenceId,
billingInvoice,
billingInvoiceLicence,
chargeVersion,
chargingModuleBillRunId
billingBatch
) {
for (const transaction of transactionLines) {
const chargingModuleRequest = ChargingModuleCreateTransactionPresenter.go(
transaction,
billingPeriod,
invoiceAccountNumber,
chargeVersion.licence
)
if (transactionLines.length === 0) {
return
}

try {
for (const transaction of transactionLines) {
const chargingModuleRequest = ChargingModuleCreateTransactionPresenter.go(
transaction,
billingPeriod,
billingInvoice.invoiceAccountNumber,
chargeVersion.licence
)

const chargingModuleResponse = await ChargingModuleCreateTransactionService.go(chargingModuleBillRunId, chargingModuleRequest)
const chargingModuleResponse = await ChargingModuleCreateTransactionService.go(billingBatch.externalId, chargingModuleRequest)

// TODO: Handle a failed request
transaction.status = 'charge_created'
transaction.externalId = chargingModuleResponse.response.body.transaction.id
transaction.billingInvoiceLicenceId = billingInvoiceLicenceId
transaction.status = 'charge_created'
transaction.externalId = chargingModuleResponse.response.body.transaction.id
transaction.billingInvoiceLicenceId = billingInvoiceLicence.billingInvoiceLicenceId

await CreateBillingTransactionService.go(transaction)
}

await CreateBillingTransactionService.go(transaction)
billingInvoice.persist = true
billingInvoiceLicence.persist = true
} catch (error) {
HandleErroredBillingBatchService.go(
billingBatch.billingBatchId,
BillingBatchModel.errorCodes.failedToCreateCharge
)

throw error
}
}

async function _finaliseBillingBatch (billingBatch, generatedInvoices, generatedInvoiceLicences) {
const { billingBatchId, externalId } = billingBatch
async function _finaliseBillingBatch (billingBatch) {
try {
const { billingBatchId, externalId } = billingBatch

const billingInvoicesToInsert = generatedInvoices.filter((billingInvoice) => billingInvoice.persist)
const billingInvoicesToInsert = generatedInvoices.filter((billingInvoice) => billingInvoice.persist)

// The bill run is considered empty. We just need to set the status to indicate this in the UI
if (billingInvoicesToInsert.length === 0) {
await _updateStatus(billingBatchId, 'empty')
// The bill run is considered empty. We just need to set the status to indicate this in the UI
if (billingInvoicesToInsert.length === 0) {
await _updateStatus(billingBatchId, 'empty')

return
}
return
}

// We need to persist the billing invoice and billing invoice licence records
const billingInvoiceLicencesToInsert = generatedInvoiceLicences.filter((invoiceLicence) => invoiceLicence.persist)

// We need to persist the billing invoice and billing invoice licence records
const billingInvoiceLicencesToInsert = generatedInvoiceLicences.filter((invoiceLicence) => invoiceLicence.persist)
await _persistBillingInvoices(billingInvoicesToInsert)
await _persistBillingInvoiceLicences(billingInvoiceLicencesToInsert)

await _persistBillingInvoices(billingInvoicesToInsert)
await _persistBillingInvoiceLicences(billingInvoiceLicencesToInsert)
// We then 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 ChargingModuleGenerateService.go(externalId)

// We then 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 ChargingModuleGenerateService.go(externalId)
// We then tell our legacy service to queue up its refresh totals job. This requests the finalised bill run and
// invoice detail from the Charging Module and updates our data with it. The good news is the legacy code handles
// all that within this job. We just need to queue it up 😁
await LegacyRequestLib.post('water', `billing/batches/${billingBatchId}/refresh`)
} catch (error) {
HandleErroredBillingBatchService.go(billingBatch.billingBatchId)

// We then tell our legacy service to queue up its refresh totals job. This requests the finalised bill run and
// invoice detail from the Charging Module and updates our data with it. The good news is the legacy code handles
// all that within this job. We just need to queue it up 😁
await LegacyRequestLib.post('water', `billing/batches/${billingBatchId}/refresh`)
throw error
}
}

function _generateTransactionLines (billingPeriod, chargeVersion) {
const financialYearEnding = billingPeriod.endDate.getFullYear()
const chargePeriod = DetermineChargePeriodService.go(chargeVersion, financialYearEnding)
const isNewLicence = DetermineMinimumChargeService.go(chargeVersion, financialYearEnding)
const isWaterUndertaker = chargeVersion.licence.isWaterUndertaker
function _generateTransactionLines (billingPeriod, chargeVersion, billingBatchId) {
try {
const financialYearEnding = billingPeriod.endDate.getFullYear()
const chargePeriod = DetermineChargePeriodService.go(chargeVersion, financialYearEnding)
const isNewLicence = DetermineMinimumChargeService.go(chargeVersion, financialYearEnding)
const isWaterUndertaker = chargeVersion.licence.isWaterUndertaker

const transactionLines = []
for (const chargeElement of chargeVersion.chargeElements) {
const result = GenerateBillingTransactionsService.go(chargeElement, billingPeriod, chargePeriod, isNewLicence, isWaterUndertaker)
transactionLines.push(...result)
}

const transactionLines = []
for (const chargeElement of chargeVersion.chargeElements) {
const result = GenerateBillingTransactionsService.go(chargeElement, billingPeriod, chargePeriod, isNewLicence, isWaterUndertaker)
transactionLines.push(...result)
}
return transactionLines
} catch (error) {
HandleErroredBillingBatchService.go(
billingBatchId,
BillingBatchModel.errorCodes.failedToPrepareTransactions
)

return transactionLines
throw error
}
}

async function _persistBillingInvoiceLicences (billingInvoiceLicences) {
Expand Down Expand Up @@ -181,9 +243,15 @@ async function _persistBillingInvoices (billingInvoices) {
}

async function _updateStatus (billingBatchId, status) {
await BillingBatchModel.query()
.findById(billingBatchId)
.patch({ status })
try {
await BillingBatchModel.query()
.findById(billingBatchId)
.patch({ status })
} catch (error) {
HandleErroredBillingBatchService.go(billingBatchId)

throw error
}
}

module.exports = {
Expand Down
Loading

0 comments on commit 6ce785e

Please sign in to comment.