diff --git a/app/requests/charging-module/send-bill-run.request.js b/app/requests/charging-module/send-bill-run.request.js new file mode 100644 index 0000000000..86325aaedf --- /dev/null +++ b/app/requests/charging-module/send-bill-run.request.js @@ -0,0 +1,88 @@ +'use strict' + +/** + * Connects with the Charging Module to send a bill run + * @module ChargingModuleSendBillRunRequest + */ + +const ChargingModuleRequest = require('../charging-module.request.js') +const ExpandedError = require('../../errors/expanded.error.js') +const WaitForStatusRequest = require('./wait-for-status.request.js') + +/** + * Approve then send a bill run in the Charging Module API + * + * Our service does in one step what the CHA does in two; approve the bill run then 'send' it. Approving a CHA bill run + * doesn't actually do anything. It just changes the state to `approved` which the bill run has to be in before the bill + * run can be sent. + * + * Sending a bill run is the final step. Once sent a bill run cannot be changed or deleted. Sending involves the CHA + * generating a transaction reference for every bill in the bill run. It then generates a transaction reference for the + * bill run itself. This reference is used to name the transaction import file which the CHA also generates at this + * time. This is the file that will make it's way to SOP and be used to generate the invoice and credit notes that + * customers receive. + * + * For small bill runs the process is near instantaneous. Larger bill runs however it can take a number of seconds. + * Because of this when the request is first made the CHA switches the bill run's status to `pending`. Only when the + * process is complete does the status get set to `billed` or `billing_not_required`. + * + * See {@link https://defra.github.io/sroc-charging-module-api-docs/#/bill-run/SendBillRun | CHA API docs} for more + * details + * + * @param {string} billRunId - UUID of the charging module API bill run to send + * + * @returns {Promise} the promise returned is not intended to resolve to any particular value + */ +async function send (billRunId) { + await _approve(billRunId) + await _send(billRunId) + + return _waitForSent(billRunId) +} + +async function _approve (billRunId) { + const path = `v3/wrls/bill-runs/${billRunId}/approve` + const result = await ChargingModuleRequest.patch(path) + + if (!result.succeeded) { + const error = new ExpandedError( + 'Charging Module approve request failed', + { billRunExternalId: billRunId, responseBody: result.response.body } + ) + + throw error + } +} + +async function _send (billRunId) { + const path = `v3/wrls/bill-runs/${billRunId}/send` + const result = await ChargingModuleRequest.patch(path) + + if (!result.succeeded) { + const error = new ExpandedError( + 'Charging Module send request failed', + { billRunExternalId: billRunId, responseBody: result.response.body } + ) + + throw error + } +} + +async function _waitForSent (billRunId) { + const result = await WaitForStatusRequest.send(billRunId, ['billed', 'billing_not_required']) + + if (!result.succeeded) { + const error = new ExpandedError( + 'Charging Module wait request failed', + { billRunExternalId: billRunId, attempts: result.attempts, responseBody: result.response.body } + ) + + throw error + } + + return result +} + +module.exports = { + send +} diff --git a/test/requests/charging-module/send-bill-run-request.test.js b/test/requests/charging-module/send-bill-run-request.test.js new file mode 100644 index 0000000000..8efda27d41 --- /dev/null +++ b/test/requests/charging-module/send-bill-run-request.test.js @@ -0,0 +1,129 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') +const Sinon = require('sinon') + +const { describe, it, beforeEach, afterEach } = exports.lab = Lab.script() +const { expect } = Code + +// Test helpers +const ExpandedError = require('../../../app/errors/expanded.error.js') + +// Things we need to stub +const ChargingModuleRequest = require('../../../app/requests/charging-module.request.js') +const WaitForStatusRequest = require('../../../app/requests/charging-module/wait-for-status.request.js') + +// Thing under test +const SendBillRunRequest = require('../../../app/requests/charging-module/send-bill-run.request.js') + +describe('Charging Module Send Bill Run request', () => { + const billRunId = '2bbbe459-966e-4026-b5d2-2f10867bdddd' + + let chargingModuleRequestStub + + beforeEach(() => { + chargingModuleRequestStub = Sinon.stub(ChargingModuleRequest, 'patch') + }) + + afterEach(() => { + Sinon.restore() + }) + + describe('when the request can send a bill run', () => { + beforeEach(async () => { + chargingModuleRequestStub.resolves({ + succeeded: true, + response: { + info: { + gitCommit: '273604040a47e0977b0579a0fef0f09726d95e39', + dockerTag: 'ghcr.io/defra/sroc-charging-module-api:v0.19.0' + }, + statusCode: 204, + body: null + } + }) + Sinon.stub(WaitForStatusRequest, 'send').resolves({ succeeded: true, status: 'billed', attempts: 1 }) + }) + + it('returns a `true` success status', async () => { + const result = await SendBillRunRequest.send(billRunId) + + expect(result.succeeded).to.be.true() + }) + + it('returns the last status received', async () => { + const result = await SendBillRunRequest.send(billRunId) + + expect(result.status).to.equal('billed') + }) + + it('returns the number of attempts', async () => { + const result = await SendBillRunRequest.send(billRunId) + + expect(result.attempts).to.equal(1) + }) + }) + + describe('when the request cannot send a bill run', () => { + describe('because the approve request fails', () => { + beforeEach(async () => { + chargingModuleRequestStub.onFirstCall().resolves({ + succeeded: false, + response: { body: 'Boom' } + }) + }) + + it('throws an error', async () => { + const error = await expect(SendBillRunRequest.send(billRunId)).to.reject() + + expect(error).to.be.instanceOf(ExpandedError) + expect(error.message).to.equal('Charging Module approve request failed') + expect(error.billRunExternalId).to.equal(billRunId) + expect(error.responseBody).to.equal('Boom') + }) + }) + + describe('because the send request fails', () => { + beforeEach(async () => { + chargingModuleRequestStub.onFirstCall().resolves({ succeeded: true }) + chargingModuleRequestStub.onSecondCall().resolves({ + succeeded: false, + response: { body: 'Boom' } + }) + }) + + it('throws an error', async () => { + const error = await expect(SendBillRunRequest.send(billRunId)).to.reject() + + expect(error).to.be.instanceOf(ExpandedError) + expect(error.message).to.equal('Charging Module send request failed') + expect(error.billRunExternalId).to.equal(billRunId) + expect(error.responseBody).to.equal('Boom') + }) + }) + + describe('because the wait request fails', () => { + beforeEach(async () => { + chargingModuleRequestStub.onFirstCall().resolves({ succeeded: true }) + chargingModuleRequestStub.onSecondCall().resolves({ succeeded: true }) + Sinon.stub(WaitForStatusRequest, 'send').resolves({ + succeeded: false, + attempts: 100, + response: { body: 'Boom' } + }) + }) + + it('throws an error', async () => { + const error = await expect(SendBillRunRequest.send(billRunId)).to.reject() + + expect(error).to.be.instanceOf(ExpandedError) + expect(error.message).to.equal('Charging Module wait request failed') + expect(error.billRunExternalId).to.equal(billRunId) + expect(error.attempts).to.equal(100) + expect(error.responseBody).to.equal('Boom') + }) + }) + }) +})