Skip to content

Commit

Permalink
Add new Charging Module SendBillRunRequest (#831)
Browse files Browse the repository at this point in the history
https://eaflood.atlassian.net/browse/WATER-4390

> Part of a series of changes to migrate the legacy [send a bill run](https://defra.github.io/sroc-charging-module-api-docs/#/bill-run/SendBillRun) to this project

The [Charging Module API](https://github.com/DEFRA/sroc-charging-module.api) generates transaction references for every bill in a bill run and creates the transaction files when it receives a `/send` request.

For that to happen though we need to send the 'send a bill' request. However, something that is a button click on our side is 3 stages in the CHA.

- a bill run has to be 'approved' before it can be sent
- the `/send` request is then made
- wait for the CHA to complete the 'send' process and mark the bill run as either `billed` or `billing_not_required`

When we make the request on our side, we don't care about the interstitial stages. So, we are adding a `SendBillRunRequest` which manages all this for us.
  • Loading branch information
Cruikshanks authored Mar 17, 2024
1 parent b2b0974 commit 1d457d9
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 0 deletions.
88 changes: 88 additions & 0 deletions app/requests/charging-module/send-bill-run.request.js
Original file line number Diff line number Diff line change
@@ -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<Object>} 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
}
129 changes: 129 additions & 0 deletions test/requests/charging-module/send-bill-run-request.test.js
Original file line number Diff line number Diff line change
@@ -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')
})
})
})
})

0 comments on commit 1d457d9

Please sign in to comment.