From 525d5ff97662e5c98e4cfe3bbc7ea06ac7dee2c7 Mon Sep 17 00:00:00 2001 From: Alan Cruikshanks Date: Fri, 3 Nov 2023 09:58:20 +0000 Subject: [PATCH] Add new fetch bill service (#495) https://eaflood.atlassian.net/browse/WATER-4155 We are building our first proper page which will replace the bill summary page the [water-abstraction-ui](https://github.com/DEFRA/water-abstraction-ui) currently provides. Rather than try to show all licences _and_ all their transactions for a bill on one page we'll instead list the licences. Users can then see the transaction details by selecting a licence. We've already made several other changes to support this. This change adds a new service to fetch the bill and summaries for each licence linked to it. --- app/services/bills/fetch-bill-service.js | 103 +++++++++++++++ .../services/bills/fetch-bill.service.test.js | 118 ++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 app/services/bills/fetch-bill-service.js create mode 100644 test/services/bills/fetch-bill.service.test.js diff --git a/app/services/bills/fetch-bill-service.js b/app/services/bills/fetch-bill-service.js new file mode 100644 index 0000000000..4740b62573 --- /dev/null +++ b/app/services/bills/fetch-bill-service.js @@ -0,0 +1,103 @@ +'use strict' + +/** + * Fetches data needed for the bill page which includes a summary for each licence linked to the bill + * @module FetchBillService + */ + +const BillModel = require('../../models/water/bill.model.js') +const { db } = require('../../../db/db.js') + +/** + * Fetch the matching Bill plus a summary for each licence linked to it + * + * Was built to provide the data needed for the '/bills/{id}' page + * + * @param {string} id The UUID for the bill to fetch + * + * @returns {Object} the matching instance of BillModel plus a summary (ID, reference, and total net amount) for each + * licence linked to the bill + */ +async function go (id) { + const bill = await _fetchBill(id) + const licenceSummaries = await _fetchLicenceSummaries(id) + + return { + bill, + licenceSummaries + } +} + +async function _fetchBill (id) { + const result = BillModel.query() + .findById(id) + .select([ + 'billingInvoiceId', + 'creditNoteValue', + 'dateCreated', + 'invoiceAccountId', + 'invoiceNumber', + 'invoiceValue', + 'isCredit', + 'isFlaggedForRebilling', + 'netAmount', + 'rebillingState', + 'isDeMinimis' + ]) + .withGraphFetched('billRun') + .modifyGraph('billRun', (builder) => { + builder.select([ + 'billingBatchId', + 'batchType', + 'dateCreated', + 'fromFinancialYearEnding', + 'toFinancialYearEnding', + 'status', + 'billRunNumber', + 'transactionFileReference', + 'scheme', + 'isSummer', + 'source' + ]) + }) + .withGraphFetched('billRun.region') + .modifyGraph('billRun.region', (builder) => { + builder.select([ + 'regionId', + 'displayName' + ]) + }) + + return result +} + +/** + * Query the DB and generate a distinct summarised result for each licence + * + * Licences are linked to a bill (billing_invoice) via bill licences (billing_invoice_licences). But the bill licence + * doesn't contain any data, for example, the total for all transactions for the linked licence. + * + * The page requires us to show the total for each licence which means we need to calculate this by totalling the + * net amount on each linked transaction. + * + * To get the query to return a single line for each licence we need to use DISTINCT. So, due to the complexity of the + * query needed we've had to drop back down to Knex and generate the query ourselves rather than going through + * Objection.js. + */ +async function _fetchLicenceSummaries (id) { + const results = await db + .distinct(['bil.billingInvoiceLicenceId', 'bil.licenceRef']) + .sum('bt.net_amount AS total') + .from('water.billingInvoiceLicences AS bil') + .innerJoin('water.billing_transactions AS bt', 'bil.billingInvoiceLicenceId', 'bt.billingInvoiceLicenceId') + .where('bil.billingInvoiceId', id) + .where('bt.chargeType', 'standard') + .groupBy('bil.billingInvoiceLicenceId', 'bil.licenceRef') + .orderBy('bil.licenceRef') + + return results +} + +module.exports = { + go +} diff --git a/test/services/bills/fetch-bill.service.test.js b/test/services/bills/fetch-bill.service.test.js new file mode 100644 index 0000000000..fda3eafa33 --- /dev/null +++ b/test/services/bills/fetch-bill.service.test.js @@ -0,0 +1,118 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') + +const { describe, it, beforeEach } = exports.lab = Lab.script() +const { expect } = Code + +// Test helpers +const BillHelper = require('../../support/helpers/water/bill.helper.js') +const BillModel = require('../../../app/models/water/bill.model.js') +const BillRunHelper = require('../../support/helpers/water/bill-run.helper.js') +const BillRunModel = require('../../../app/models/water/bill-run.model.js') +const BillLicenceHelper = require('../../support/helpers/water/bill-licence.helper.js') +const DatabaseHelper = require('../../support/helpers/database.helper.js') +const RegionHelper = require('../../support/helpers/water/region.helper.js') +const RegionModel = require('../../../app/models/water/region.model.js') +const TransactionHelper = require('../../support/helpers/water/transaction.helper.js') + +// Thing under test +const FetchBillService = require('../../../app/services/bills/fetch-bill-service.js') + +describe('Fetch Bill service', () => { + let linkedBillLicences + let linkedBillRun + let linkedRegion + let testBill + let unlinkedBillLicence + + beforeEach(async () => { + await DatabaseHelper.clean() + + linkedRegion = await RegionHelper.add() + linkedBillRun = await BillRunHelper.add({ regionId: linkedRegion.regionId }) + + testBill = await BillHelper.add({ billingBatchId: linkedBillRun.billingBatchId }) + + linkedBillLicences = await Promise.all([ + BillLicenceHelper.add({ billingInvoiceId: testBill.billingInvoiceId }), + BillLicenceHelper.add({ billingInvoiceId: testBill.billingInvoiceId }) + ]) + + await Promise.all([ + TransactionHelper.add({ billingInvoiceLicenceId: linkedBillLicences[0].billingInvoiceLicenceId, netAmount: 10 }), + TransactionHelper.add({ billingInvoiceLicenceId: linkedBillLicences[0].billingInvoiceLicenceId, netAmount: 20 }), + TransactionHelper.add({ billingInvoiceLicenceId: linkedBillLicences[1].billingInvoiceLicenceId, netAmount: 30 }), + TransactionHelper.add({ billingInvoiceLicenceId: linkedBillLicences[1].billingInvoiceLicenceId, netAmount: 40 }) + ]) + + // Add an unlinked BillLicence and Transaction to demonstrate only linked ones are returned + unlinkedBillLicence = await BillLicenceHelper.add() + await TransactionHelper.add({ billingInvoiceLicenceId: unlinkedBillLicence.billingInvoiceLicenceId, netAmount: 50 }) + }) + + describe('when a bill with a matching ID exists', () => { + it('returns the matching instance of BillModel', async () => { + const { bill: result } = await FetchBillService.go(testBill.billingInvoiceId) + + expect(result.billingInvoiceId).to.equal(testBill.billingInvoiceId) + expect(result).to.be.an.instanceOf(BillModel) + }) + + it('returns the matching bill including the linked bill run', async () => { + const { bill: result } = await FetchBillService.go(testBill.billingInvoiceId) + const { billRun: returnedBillRun } = result + + expect(result.billingInvoiceId).to.equal(testBill.billingInvoiceId) + expect(result).to.be.an.instanceOf(BillModel) + + expect(returnedBillRun.billingBatchId).to.equal(linkedBillRun.billingBatchId) + expect(returnedBillRun).to.be.an.instanceOf(BillRunModel) + }) + + it('returns the matching bill including the linked bill run and the region it is linked to', async () => { + const { bill: result } = await FetchBillService.go(testBill.billingInvoiceId) + const { region: returnedRegion } = result.billRun + + expect(result.billingInvoiceId).to.equal(testBill.billingInvoiceId) + expect(result).to.be.an.instanceOf(BillModel) + + expect(returnedRegion.regionId).to.equal(linkedRegion.regionId) + expect(returnedRegion).to.be.an.instanceOf(RegionModel) + }) + + it('returns a transaction summary for each licence linked to the bill', async () => { + const { licenceSummaries: result } = await FetchBillService.go(testBill.billingInvoiceId) + + expect(result).to.have.length(2) + expect(result).to.include({ + billingInvoiceLicenceId: linkedBillLicences[0].billingInvoiceLicenceId, + licenceRef: linkedBillLicences[0].licenceRef, + total: 30 + }) + expect(result).to.include({ + billingInvoiceLicenceId: linkedBillLicences[1].billingInvoiceLicenceId, + licenceRef: linkedBillLicences[1].licenceRef, + total: 70 + }) + + expect(result).not.to.include({ + billingInvoiceLicenceId: unlinkedBillLicence.billingInvoiceLicenceId, + licenceRef: unlinkedBillLicence.licenceRef, + total: 50 + }) + }) + }) + + describe('when a bill with a matching ID does not exist', () => { + it('returns a result with no values set', async () => { + const result = await FetchBillService.go('93112100-152b-4860-abea-2adee11dcd69') + + expect(result).to.exist() + expect(result.bill).to.equal(undefined) + expect(result.licenceSummaries).to.equal([]) + }) + }) +})