Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SROC invoice reissuing #256

Merged
merged 44 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
05501eb
Implement SROC invoice reissuing
StuAA78 Jun 6, 2023
b251f8a
Refactor to accommodate future service
StuAA78 Jun 6, 2023
0d8b63b
Quick docs
StuAA78 Jun 6, 2023
9669f61
Merge branch 'main' into implement-sroc-invoice-reissuing
StuAA78 Jun 6, 2023
db60c17
Remove `.only` from test
StuAA78 Jun 13, 2023
0cfad4b
First pass at service and initial basic unit tests
StuAA78 Jun 13, 2023
5c18e1f
Continue unit tests
StuAA78 Jun 14, 2023
dfa5228
Use correct billing batch
StuAA78 Jun 14, 2023
65187b5
Correctly set `isCredit`
StuAA78 Jun 14, 2023
10c18ee
Comment and formatting tweaks
StuAA78 Jun 14, 2023
ff9f984
Add finalising process
StuAA78 Jun 15, 2023
c0c6b2d
Remove unneeded "get invoices"
StuAA78 Jun 15, 2023
0aa3be3
Small refactor of test CM data
StuAA78 Jun 15, 2023
52a2572
Test for correct number of invoices, invoice licences and transactions
StuAA78 Jun 15, 2023
aac5b56
Fix and refactor billing invoice & invoice licence generation
StuAA78 Jun 15, 2023
db68186
Reformat code and rename variables in accordance with team standards
StuAA78 Jun 19, 2023
06c0f0c
Refactor to remove top-level try/catch
StuAA78 Jun 19, 2023
735e4e0
Move main loop into separate function
StuAA78 Jun 19, 2023
3981dd9
Split `ReissueInvoicesService` into 3 services
StuAA78 Jun 19, 2023
dc3c02e
Unit test `ReissueInvoicesService`
StuAA78 Jun 19, 2023
3e1bb96
Unit test `ReissueInvoiceService`
StuAA78 Jun 20, 2023
f2a11ef
Update docs
StuAA78 Jun 22, 2023
2db1029
Optimise `FetchInvoicesToBeReissued` query
StuAA78 Jun 26, 2023
356867e
Add error logging
StuAA78 Jun 27, 2023
5d34371
Revert `ProcessBillingBatchService` changes
StuAA78 Jun 27, 2023
fc6f091
Merge branch 'main' into implement-sroc-invoice-reissuing
StuAA78 Jun 27, 2023
d77b1c1
Send correct billing batch to Charging Module
StuAA78 Jun 28, 2023
348e998
Remove `originalBillingBatch` completely
StuAA78 Jun 28, 2023
28d79cd
Change error logging based on review
StuAA78 Jun 28, 2023
3a4c9f6
Merge branch 'main' into implement-sroc-invoice-reissuing
StuAA78 Jun 28, 2023
c282921
Fix typos
StuAA78 Jun 28, 2023
9daf896
Merge branch 'main' into implement-sroc-invoice-reissuing
StuAA78 Jun 28, 2023
65635ca
Amend error handling
StuAA78 Jul 4, 2023
6cea97b
Rename `dataToPersist` transactions property
StuAA78 Jul 4, 2023
f4f5409
Remove iteration when adding data to `dataToPersist`
StuAA78 Jul 4, 2023
b9529f4
Update app/services/supplementary-billing/reissue-invoice.service.js
StuAA78 Jul 4, 2023
22703df
Rename `chargingModuleReissueResponses`
StuAA78 Jul 4, 2023
aecec05
Merge branch 'main' into implement-sroc-invoice-reissuing
StuAA78 Jul 4, 2023
766a06e
Update app/services/supplementary-billing/reissue-invoice.service.js
StuAA78 Jul 5, 2023
8a5e324
Update app/services/supplementary-billing/reissue-invoice.service.js
StuAA78 Jul 5, 2023
3ec186f
Rework generation of transactions
StuAA78 Jul 5, 2023
7bded81
Refactor unit tests
StuAA78 Jul 5, 2023
5c1c7ea
Merge branch 'main' into implement-sroc-invoice-reissuing
StuAA78 Jul 5, 2023
16e6522
Update test/services/supplementary-billing/reissue-invoice.service.te…
StuAA78 Jul 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use strict'

/**
* Fetches billing invoices to be reissued
* @module FetchInvoicesToBeReissuedService
*/

const BillingInvoiceModel = require('../../models/water/billing-invoice.model.js')

/**
* Takes a region and fetches billing invoices in that region marked for reissuing, along with their transactions
*
* @param {String} regionId The uuid of the region
*
* @returns {module:BillingInvoiceModel[]} Array of billing invoices to be reissued
*/
async function go (regionId) {
try {
const result = await BillingInvoiceModel.query()
.select(
'billingInvoices.externalId',
'invoiceAccountId',
'invoiceAccountNumber',
'originalBillingInvoiceId'
)
.where('isFlaggedForRebilling', true)
.joinRelated('billingBatch')
.where('billingBatch.regionId', regionId)
.withGraphFetched('billingInvoiceLicences.billingTransactions')
.modifyGraph('billingInvoiceLicences', (builder) => {
builder.select(
'licenceRef',
'licenceId'
)
})

return result
} catch (error) {
// If getting invoices errors then we log the error and return an empty array; the db hasn't yet been modified at
// this stage so we can simply move on to the next stage of processing the billing batch.

global.GlobalNotifier.omfg(
'Could not fetch reissue invoices',
{ region: regionId },
error
)

return []
}
}

module.exports = {
go
}
268 changes: 268 additions & 0 deletions app/services/supplementary-billing/reissue-invoice.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
'use strict'

/**
* Handles the reissuing of a single invoice
* @module ReissueInvoiceService
*/
const { randomUUID } = require('crypto')

const ChargingModuleReissueInvoiceService = require('../charging-module/reissue-invoice.service.js')
const ChargingModuleViewInvoiceService = require('../charging-module/view-invoice.service.js')
const GenerateBillingInvoiceLicenceService = require('./generate-billing-invoice-licence.service.js')
const GenerateBillingInvoiceService = require('./generate-billing-invoice.service.js')

/**
* Handles the reissuing of a single invoice
*
* This service raises a reissue request with the Charging Module for an invoice (referred to as the "source" invoice)
* which creates 2 invoices on the CM side: a "cancelling" invoice (ie. one which cancels out the source invoice by
* being a credit for the same amount instead of a debit, or vice-versa) and a "reissuing" invoice (ie. the replacement
* for the source invoice).
*
* This request is all that needs to be done on the CM, so we then move on to our side of things. We create two new
* billing invoices on our side (a cancelling invoice and a reissuing invoice), and for each one we re-create the
* billing invoice licences and transactions of the source invoice. While doing this we are loading up a `dataToReturn`
* object with the new invoices, invoice licences and transactions.
*
* Once the invoice has been handled, we mark the source invoice as `rebilled` in the db (note that "rebill" is legacy
* terminology for "reissue") along with the `originalBillingInvoiceId` field; if this is empty then we update it with
* the source invoice's billing invoice id, or if it's already filled in then we leave it as-is. This ensures that if an
* invoice is reissued, and that reissuing invoice is itself reissued, then `originalBillingInvoiceId` will still point
* directly to the original source invoice.
*
* @param {module:BillingInvoiceModel} sourceInvoice The invoice to be reissued. Note that we expect it to include the
* billing invoice licences and transactions
* @param {module:BillingBatchModel} reissueBillingBatch The billing batch that the new invoices should belong to
*
* @returns {Object} dataToReturn Data that has been generated while reissuing the invoice
* @returns {Object[]} dataToReturn.billingInvoices Array of billing invoices
* @returns {Object[]} dataToReturn.billingInvoiceLicences Array of billing invoice licences
* @returns {Object[]} dataToReturn.billingTransactions Array of transactions
*/

async function go (sourceInvoice, reissueBillingBatch) {
const dataToReturn = {
billingInvoices: [],
billingInvoiceLicences: [],
billingTransactions: []
}

// When a reissue request is sent to the Charging Module, it creates 2 new invoices (one to cancel out the original
// invoice and one to be the new version of it) and returns their ids
const chargingModuleReissueInvoiceIds = await _sendReissueRequest(
reissueBillingBatch.externalId,
sourceInvoice.externalId
)

for (const chargingModuleReissueInvoiceId of chargingModuleReissueInvoiceIds) {
// Because we only have the CM invoice's id we now need to fetch its details via the "view invoice" endpoint
const chargingModuleReissueInvoice = await _sendViewInvoiceRequest(
reissueBillingBatch,
chargingModuleReissueInvoiceId
)

const reissueBillingInvoice = _retrieveOrGenerateBillingInvoice(
dataToReturn,
sourceInvoice,
reissueBillingBatch,
chargingModuleReissueInvoice
)

// The invoice we want to reissue will have one or more billing invoice licences on it which we need to re-create
for (const sourceInvoiceLicence of sourceInvoice.billingInvoiceLicences) {
const reissueInvoiceLicence = _retrieveOrGenerateBillingInvoiceLicence(
dataToReturn,
sourceInvoice,
reissueBillingInvoice.billingInvoiceId,
sourceInvoiceLicence
)

const chargingModuleLicence = _retrieveChargingModuleLicence(
chargingModuleReissueInvoice,
sourceInvoiceLicence.licenceRef
)

// The invoice licence we are re-creating will have one or more transactions on it which we also need to re-create
for (const sourceTransaction of sourceInvoiceLicence.billingTransactions) {
// Get the original transaction from the charging module data so we can create our new one
const chargingModuleReissueTransaction = _retrieveChargingModuleTransaction(
chargingModuleLicence,
sourceTransaction.externalId
)

const reissueTransaction = _generateTransaction(
chargingModuleReissueTransaction,
sourceTransaction,
reissueInvoiceLicence.billingInvoiceLicenceId
)

dataToReturn.billingTransactions.push(reissueTransaction)
}
}
}

await _markSourceInvoiceAsRebilled(sourceInvoice)

return dataToReturn
}

/**
* Generates a new transaction using sourceTransaction as a base and amending properties as appropriate
*/
function _generateTransaction (chargingModuleReissueTransaction, sourceTransaction, billingInvoiceLicenceId) {
return {
...sourceTransaction,
billingTransactionId: randomUUID({ disableEntropyCache: true }),
externalId: chargingModuleReissueTransaction.id,
isCredit: chargingModuleReissueTransaction.credit,
netAmount: _determineSignOfNetAmount(
chargingModuleReissueTransaction.chargeValue,
chargingModuleReissueTransaction.credit
),
billingInvoiceLicenceId
}
}

// The Charging Module always returns a positive value for net amount whereas our db has a positive amount for debits
// and a negative value for credits. We therefore use the CM charge value and credit flag to determine whether our net
// amount should be positive or negative
function _determineSignOfNetAmount (chargeValue, credit) {
return credit ? -chargeValue : chargeValue
}

/**
* Maps the provided CM invoice fields to their billing invoice equivalents
*/
function _mapChargingModuleInvoice (chargingModuleInvoice) {
const chargingModuleRebilledTypes = new Map()
.set('C', 'reversal')
.set('R', 'rebill')
.set('O', null)
StuAA78 marked this conversation as resolved.
Show resolved Hide resolved

return {
externalId: chargingModuleInvoice.id,
netAmount: chargingModuleInvoice.netTotal,
isDeMinimis: chargingModuleInvoice.deminimisInvoice,
invoiceValue: chargingModuleInvoice.debitLineValue,
// As per legacy code we invert the sign of creditLineValue
creditNoteValue: -chargingModuleInvoice.creditLineValue,
rebillingState: chargingModuleRebilledTypes.get(chargingModuleInvoice.rebilledType)
}
}

/**
* Updates the source invoice's rebilling state and original billing invoice id
*/
async function _markSourceInvoiceAsRebilled (sourceInvoice) {
await sourceInvoice.$query().patch({
rebillingState: 'rebilled',
// If the source invoice's originalBillingInvoiceId field is `null` then we update it with the invoice's id;
// otherwise, we use its existing value. This ensures that if we reissue an invoice, then reissue that reissuing
// invoice, every invoice in the chain will have originalBillingInvoiceId pointing back to the very first invoice in
// the chain
originalBillingInvoiceId: sourceInvoice.originalBillingInvoiceId ?? sourceInvoice.billingInvoiceId
})
}

function _retrieveChargingModuleTransaction (chargingModuleLicence, id) {
return chargingModuleLicence.transactions.find((transaction) => {
return transaction.rebilledTransactionId === id
})
}

function _retrieveChargingModuleLicence (chargingModuleInvoice, licenceRef) {
return chargingModuleInvoice.licences.find((licence) => {
return licence.licenceNumber === licenceRef
})
}

/**
* If a billing invoice exists for this combination of source invoice and CM reissue invoice then return it; otherwise,
* generate it, store it and then return it.
*/
function _retrieveOrGenerateBillingInvoice (dataToReturn, sourceInvoice, reissueBillingBatch, chargingModuleReissueInvoice) {
// Because we have nested iteration of source invoice and Charging Module reissue invoice, we need to ensure we have
// a billing invoice for every combination of these, hence we search by both of their ids
const existingBillingInvoice = dataToReturn.billingInvoices.find((invoice) => {
return invoice.invoiceAccountId === sourceInvoice.invoiceAccountId &&
invoice.externalId === chargingModuleReissueInvoice.id
})

if (existingBillingInvoice) {
return existingBillingInvoice
}

const translatedChargingModuleInvoice = _mapChargingModuleInvoice(chargingModuleReissueInvoice)
const generatedBillingInvoice = GenerateBillingInvoiceService.go(
sourceInvoice,
reissueBillingBatch.billingBatchId,
sourceInvoice.financialYearEnding
)

// Construct our new billing invoice by taking the generated billing invoice and combining it with the mapped fields
// from the CM invoice, then updating its `originalBillingInvoiceId` field
const newBillingInvoice = {
...generatedBillingInvoice,
...translatedChargingModuleInvoice,
originalBillingInvoiceId: sourceInvoice.billingInvoiceId
}

dataToReturn.billingInvoices.push(newBillingInvoice)

return newBillingInvoice
}

/**
* If a billing invoice licence exists for this invoice account id then return it; otherwise, generate it, store it and
* then return it.
*/
function _retrieveOrGenerateBillingInvoiceLicence (dataToReturn, sourceInvoice, billingInvoiceId, sourceInvoiceLicence) {
const existingBillingInvoiceLicence = dataToReturn.billingInvoiceLicences.find((invoice) => {
return invoice.invoiceAccountId === sourceInvoice.invoiceAccountId
})

if (existingBillingInvoiceLicence) {
return existingBillingInvoiceLicence
}

const newBillingInvoiceLicence = GenerateBillingInvoiceLicenceService.go(billingInvoiceId, sourceInvoiceLicence)

dataToReturn.billingInvoiceLicences.push(newBillingInvoiceLicence)

return newBillingInvoiceLicence
}

async function _sendReissueRequest (billingBatchExternalId, invoiceExternalId) {
const result = await ChargingModuleReissueInvoiceService.go(billingBatchExternalId, invoiceExternalId)

if (!result.succeeded) {
const error = new Error('Charging Module reissue request failed')
error.billingBatchExternalId = billingBatchExternalId
error.invoiceExternalId = invoiceExternalId

throw error
}

// The CM returns a few bits of info but we only need the id
return result.response.invoices.map((invoice) => {
return invoice.id
})
}

async function _sendViewInvoiceRequest (billingBatch, reissueInvoiceId) {
const result = await ChargingModuleViewInvoiceService.go(billingBatch.externalId, reissueInvoiceId)

if (!result.succeeded) {
const error = new Error('Charging Module view invoice request failed')
error.billingBatchExternalId = billingBatch.externalId
error.reissueInvoiceExternalId = reissueInvoiceId

throw error
}

return result.response.invoice
}

module.exports = {
go
}
69 changes: 69 additions & 0 deletions app/services/supplementary-billing/reissue-invoices.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict'

/**
* Handles the reissuing of invoices
* @module ReissueInvoicesService
*/

const BillingInvoiceModel = require('../../models/water/billing-invoice.model.js')
const BillingInvoiceLicenceModel = require('../../models/water/billing-invoice-licence.model.js')
const BillingTransactionModel = require('../../models/water/billing-transaction.model.js')
const FetchInvoicesToBeReissuedService = require('./fetch-invoices-to-be-reissued.service.js')
const ReissueInvoiceService = require('./reissue-invoice.service.js')

/**
* Handles the reissuing of invoices
*
* We receive the billing batch that the reissued invoices are to be created on and infer from this the region to be
* reissued. We check this region for invoices marked for reissuing. For each one of these we call
* `ReissueInvoiceService` which handles the actual reissuing of an invoice and collects the returned invoice, invoice
* licence and transaction data which we batch persist once all invoices have been reissued. Finally we return a boolean
* to indicate whether or not any invoices were reissued.
*
* @param {module:BillingBatchModel} reissueBillingBatch The billing batch that the reissued invoices will be created on
*
* @returns {Boolean} `true` if any invoices were reissued; `false` if not
*/

async function go (reissueBillingBatch) {
const sourceInvoices = await FetchInvoicesToBeReissuedService.go(reissueBillingBatch.regionId)

if (sourceInvoices.length === 0) {
return false
}

const dataToPersist = {
billingInvoices: [],
billingInvoiceLicences: [],
billingTransactions: []
}

for (const sourceInvoice of sourceInvoices) {
const newData = await ReissueInvoiceService.go(sourceInvoice, reissueBillingBatch)

_addNewDataToDataToPersist(dataToPersist, newData)
}

await _persistData(dataToPersist)

return true
}

/**
* Adds the data held in each key of `newData` to the corresponding keys in `dataToPersist`
*/
function _addNewDataToDataToPersist (dataToPersist, newData) {
dataToPersist.billingInvoices.push(...newData.billingInvoices)
dataToPersist.billingInvoiceLicences.push(...newData.billingInvoiceLicences)
dataToPersist.billingTransactions.push(...newData.billingTransactions)
}

async function _persistData (dataToPersist) {
await BillingInvoiceModel.query().insert(dataToPersist.billingInvoices)
await BillingInvoiceLicenceModel.query().insert(dataToPersist.billingInvoiceLicences)
await BillingTransactionModel.query().insert(dataToPersist.billingTransactions)
}

module.exports = {
go
}
Loading