diff --git a/app/controllers/bill-runs.controller.js b/app/controllers/bill-runs.controller.js
index 783feeb25d..386cf0297c 100644
--- a/app/controllers/bill-runs.controller.js
+++ b/app/controllers/bill-runs.controller.js
@@ -10,6 +10,7 @@ const Boom = require('@hapi/boom')
const CreateBillRunValidator = require('../validators/create-bill-run.validator.js')
const StartBillRunProcessService = require('../services/bill-runs/start-bill-run-process.service.js')
const ViewBillRunService = require('../services/bill-runs/view-bill-run.service.js')
+const ReviewBillRunService = require('../services/bill-runs/two-part-tariff/review-bill-run.service.js')
async function create (request, h) {
const validatedData = CreateBillRunValidator.go(request.payload)
@@ -28,10 +29,15 @@ async function create (request, h) {
}
}
-async function review (_request, h) {
+async function review (request, h) {
+ const { id } = request.params
+
+ const pageData = await ReviewBillRunService.go(id)
+
return h.view('bill-runs/review.njk', {
- pageTitle: 'Review Two Part Tariff SROC',
- activeNavBar: 'bill-runs'
+ pageTitle: 'Review licences',
+ activeNavBar: 'bill-runs',
+ ...pageData
})
}
diff --git a/app/models/licence.model.js b/app/models/licence.model.js
index 4144fd0bd5..6d31e0b81c 100644
--- a/app/models/licence.model.js
+++ b/app/models/licence.model.js
@@ -64,6 +64,14 @@ class LicenceModel extends BaseModel {
to: 'regions.id'
}
},
+ reviewResults: {
+ relation: Model.HasManyRelation,
+ modelClass: 'review-result.model',
+ join: {
+ from: 'licences.id',
+ to: 'reviewResults.licenceId'
+ }
+ },
workflows: {
relation: Model.HasManyRelation,
modelClass: 'workflow.model',
diff --git a/app/models/review-result.model.js b/app/models/review-result.model.js
index ba18c215b9..845c7be022 100644
--- a/app/models/review-result.model.js
+++ b/app/models/review-result.model.js
@@ -31,6 +31,14 @@ class ReviewResultModel extends BaseModel {
from: 'reviewResults.reviewReturnResultId',
to: 'reviewReturnResults.id'
}
+ },
+ licence: {
+ relation: Model.BelongsToOneRelation,
+ modelClass: 'licence.model',
+ join: {
+ from: 'reviewResults.licenceId',
+ to: 'licences.id'
+ }
}
}
}
diff --git a/app/presenters/bill-runs/two-part-tariff/review-bill-run.presenter.js b/app/presenters/bill-runs/two-part-tariff/review-bill-run.presenter.js
new file mode 100644
index 0000000000..959e9705ee
--- /dev/null
+++ b/app/presenters/bill-runs/two-part-tariff/review-bill-run.presenter.js
@@ -0,0 +1,77 @@
+'use strict'
+
+/**
+ * Formats the two part tariff review data ready for presenting in the review page
+ * @module ReviewBillRunPresenter
+ */
+
+const { formatLongDate } = require('../../base.presenter.js')
+
+/**
+ * Prepares and processes bill run and licence data for presentation
+ *
+ * @param {module:BillRunModel} billRun the data from the bill run
+ * @param {module:LicenceModel} licences the licences data asociated with the bill run
+ *
+ * @returns {Object} the prepared bill run and licence data to be passed to the review page
+ */
+function go (billRun, licences) {
+ const { licencesToReviewCount, preparedLicences } = _prepareLicences(licences)
+
+ const preparedBillRun = _prepareBillRun(billRun, preparedLicences, licencesToReviewCount)
+
+ return { ...preparedBillRun, preparedLicences }
+}
+
+function _prepareLicences (licences) {
+ let licencesToReviewCount = 0
+ const preparedLicences = []
+
+ for (const licence of licences) {
+ if (licence.status === 'review') {
+ licencesToReviewCount++
+ }
+
+ preparedLicences.push({
+ licenceRef: licence.licenceRef,
+ licenceHolder: licence.licenceHolder,
+ status: licence.status,
+ issue: _getIssueOnLicence(licence.issues)
+ })
+ }
+
+ return { preparedLicences, licencesToReviewCount }
+}
+
+function _prepareBillRun (billRun, billRunLicences, licencesToReviewCount) {
+ return {
+ region: billRun.region.displayName,
+ status: billRun.status,
+ dateCreated: formatLongDate(billRun.createdAt),
+ financialYear: _financialYear(billRun.toFinancialYearEnding),
+ billRunType: 'two-part tariff',
+ numberOfLicences: billRunLicences.length,
+ licencesToReviewCount
+ }
+}
+
+function _financialYear (financialYearEnding) {
+ const startYear = financialYearEnding - 1
+ const endYear = financialYearEnding
+
+ return `${startYear} to ${endYear}`
+}
+
+function _getIssueOnLicence (issues) {
+ if (issues.length > 1) {
+ return 'Multiple Issues'
+ } else if (issues.length === 1) {
+ return issues[0]
+ } else {
+ return ''
+ }
+}
+
+module.exports = {
+ go
+}
diff --git a/app/services/bill-runs/two-part-tariff/determine-bill-run-issues.service.js b/app/services/bill-runs/two-part-tariff/determine-bill-run-issues.service.js
new file mode 100644
index 0000000000..8db95ca5a5
--- /dev/null
+++ b/app/services/bill-runs/two-part-tariff/determine-bill-run-issues.service.js
@@ -0,0 +1,206 @@
+'use strict'
+
+/**
+ * Determines the issues on the licences for a two-part tariff bill run
+ * @module DetermineBillRunIssuesService
+ */
+
+const FetchReviewResultsService = require('./fetch-review-results.service.js')
+
+// A list of issues that would put a licence into a status of 'review'
+const REVIEW_STATUSES = [
+ 'Aggregate factor', 'Checking query', 'Overlap of charge dates', 'Returns received but not processed',
+ 'Returns split over charge references', 'Unable to match returns'
+]
+
+/**
+ * Fetches the review data on a licence and determines all the issues and licence status
+ *
+ * @param {module:LicenceModel} licences the two-part tariff licence included in the bill run
+ */
+async function go (licences) {
+ for (const licence of licences) {
+ const licenceReviewResults = await FetchReviewResultsService.go(licence.id)
+ const { issues, status } = _determineIssues(licenceReviewResults)
+
+ licence.issues = issues
+ licence.status = status
+ }
+}
+
+function _abstractionOutsidePeriod (issues, licenceReviewResults) {
+ const abstractionOutsidePeriod = licenceReviewResults.some((licenceReviewResult) => {
+ return licenceReviewResult.reviewReturnResults?.abstractionOutsidePeriod
+ })
+
+ if (abstractionOutsidePeriod) {
+ issues.push('Abstraction outside period')
+ }
+}
+
+function _determineIssues (licenceReviewResults) {
+ const issues = []
+
+ _abstractionOutsidePeriod(issues, licenceReviewResults)
+
+ _hasAggregate(issues, licenceReviewResults)
+
+ _underQuery(issues, licenceReviewResults)
+
+ _noReturnsReceived(issues, licenceReviewResults)
+
+ _overAbstracted(issues, licenceReviewResults)
+
+ _hasChargeDatesOverlap(issues, licenceReviewResults)
+
+ _notProcessed(issues, licenceReviewResults)
+
+ _receivedLate(issues, licenceReviewResults)
+
+ _returnsSplitOverChargeReference(issues, licenceReviewResults)
+
+ _returnsNotReceived(issues, licenceReviewResults)
+
+ _matchingReturns(issues, licenceReviewResults)
+
+ const status = _determineIssueStatus(issues)
+
+ return { issues, status }
+}
+
+function _determineIssueStatus (issues) {
+ const hasReviewIssue = issues.some((issue) => {
+ return REVIEW_STATUSES.includes(issue)
+ })
+
+ if (hasReviewIssue) {
+ return 'review'
+ }
+
+ return 'ready'
+}
+
+function _hasAggregate (issues, licenceReviewResults) {
+ const hasAggregate = licenceReviewResults.some((licenceReviewResult) => {
+ return licenceReviewResult.reviewChargeElementResults?.aggregate !== 1
+ })
+
+ if (hasAggregate) {
+ issues.push('Aggregate factor')
+ }
+}
+
+function _hasChargeDatesOverlap (issues, licenceReviewResults) {
+ const hasChargeDatesOverlap = licenceReviewResults.some((licenceReviewResult) => {
+ return licenceReviewResult.reviewChargeElementResults?.chargeDatesOverlap
+ })
+
+ if (hasChargeDatesOverlap) {
+ issues.push('Overlap of charge dates')
+ }
+}
+
+function _matchingReturns (issues, licenceReviewResults) {
+ const matchingReturns = licenceReviewResults.some((licenceReviewResult) => {
+ return !(licenceReviewResult?.reviewChargeElementResultId && licenceReviewResult?.reviewReturnResultId)
+ })
+
+ if (matchingReturns) {
+ issues.push('Unable to match returns')
+ }
+}
+
+function _notProcessed (issues, licenceReviewResults) {
+ const notProcessed = licenceReviewResults.some((licenceReviewResult) => {
+ return licenceReviewResult.reviewReturnResults?.status === 'received'
+ })
+
+ if (notProcessed) {
+ issues.push('Returns received but not processed')
+ }
+}
+
+function _noReturnsReceived (issues, licenceReviewResults) {
+ const noReturnsReceived = licenceReviewResults.some((licenceReviewResult) => {
+ return licenceReviewResult.reviewReturnResults?.status === 'due' || licenceReviewResult.reviewReturnResults?.status === 'overdue'
+ })
+
+ if (noReturnsReceived) {
+ issues.push('No returns received')
+ }
+}
+
+function _overAbstracted (issues, licenceReviewResults) {
+ const overAbstracted = licenceReviewResults.some((licenceReviewResult) => {
+ return licenceReviewResult.reviewReturnResults?.quantity > licenceReviewResult.reviewReturnResults?.allocated
+ })
+
+ if (overAbstracted) {
+ issues.push('Over abstraction')
+ }
+}
+
+function _receivedLate (issues, licenceReviewResults) {
+ const receivedLate = licenceReviewResults.some((licenceReviewResult) => {
+ return licenceReviewResult.reviewReturnResults?.receivedDate > licenceReviewResult.reviewReturnResults?.dueDate
+ })
+
+ if (receivedLate) {
+ issues.push('Returns received late')
+ }
+}
+
+function _underQuery (issues, licenceReviewResults) {
+ const underQuery = licenceReviewResults.some((licenceReviewResult) => {
+ return licenceReviewResult.reviewReturnResults?.underQuery
+ })
+
+ if (underQuery) {
+ issues.push('Checking query')
+ }
+}
+
+/**
+ * Checks if any of the licence review results indicate that a charge element has not received its returns
+ */
+function _returnsNotReceived (issues, licenceReviewResults) {
+ const returnsNotReceived = licenceReviewResults.some((licenceReviewResult) => {
+ return (licenceReviewResult.reviewReturnResultId &&
+ licenceReviewResult.reviewChargeElementResultId &&
+ (licenceReviewResult.reviewReturnResults.status === 'due' ||
+ licenceReviewResult.reviewReturnResults.status === 'overdue')
+ )
+ })
+
+ if (returnsNotReceived) {
+ issues.push('Some returns not received')
+ }
+}
+
+/**
+ * Determines if there are multiple charge references associated with a matched return
+ */
+function _returnsSplitOverChargeReference (issues, licenceReviewResults) {
+ const seenReviewReturnResults = {}
+ let returnsSplitOverChargeReference
+
+ for (const result of licenceReviewResults) {
+ const { chargeReferenceId, reviewReturnResultId } = result
+
+ if (seenReviewReturnResults[reviewReturnResultId]) {
+ if (seenReviewReturnResults[reviewReturnResultId] !== chargeReferenceId) {
+ returnsSplitOverChargeReference = true
+ }
+ } else {
+ seenReviewReturnResults[reviewReturnResultId] = chargeReferenceId
+ }
+ }
+
+ if (returnsSplitOverChargeReference) {
+ issues.push('Returns split over charge references')
+ }
+}
+
+module.exports = {
+ go
+}
diff --git a/app/services/bill-runs/two-part-tariff/fetch-bill-run-licences.service.js b/app/services/bill-runs/two-part-tariff/fetch-bill-run-licences.service.js
new file mode 100644
index 0000000000..917e584ec2
--- /dev/null
+++ b/app/services/bill-runs/two-part-tariff/fetch-bill-run-licences.service.js
@@ -0,0 +1,67 @@
+'use strict'
+
+/**
+ * Fetches bill run and licences data for two-part-tariff billing review
+ * @module FetchBillRunLicencesService
+ */
+
+const BillRunModel = require('../../../models/bill-run.model.js')
+const LicenceModel = require('../../../models/licence.model.js')
+
+/**
+ * Takes the bill run ID and fetches all the data needed to review the bill run
+ *
+ * Fetches specifically the bill run data, a list of the licences in the bill run with the licence holder and licence
+ * ref.
+ * @param {String} id The UUID for the bill run
+ *
+ * @returns {Object} an object containing the billRun data and an array of licences for the bill run
+ */
+async function go (id) {
+ const billRun = await _fetchBillRun(id)
+ const licences = await _fetchLicences(id)
+
+ return { billRun, licences }
+}
+
+async function _fetchBillRun (id) {
+ const billRun = await BillRunModel.query()
+ .findById(id)
+ .select([
+ 'id',
+ 'createdAt',
+ 'status',
+ 'toFinancialYearEnding',
+ 'batchType'
+ ])
+ .withGraphFetched('region')
+ .modifyGraph('region', (builder) => {
+ builder.select([
+ 'id',
+ 'displayName'
+ ])
+ })
+
+ return billRun
+}
+
+async function _fetchLicences (id) {
+ const licences = await LicenceModel.query()
+ .distinct([
+ 'licences.id',
+ 'licenceRef'
+ ])
+ .innerJoinRelated('reviewResults')
+ .where('billRunId', id)
+ .modify('licenceHolder')
+
+ for (const licence of licences) {
+ licence.licenceHolder = licence.$licenceHolder()
+ }
+
+ return licences
+}
+
+module.exports = {
+ go
+}
diff --git a/app/services/bill-runs/two-part-tariff/fetch-review-results.service.js b/app/services/bill-runs/two-part-tariff/fetch-review-results.service.js
new file mode 100644
index 0000000000..db81b431d4
--- /dev/null
+++ b/app/services/bill-runs/two-part-tariff/fetch-review-results.service.js
@@ -0,0 +1,56 @@
+'use strict'
+
+/**
+ * Fetches the review results data for a given licence along with their associated charge elements and return logs
+ * @module FetchReviewResultService
+ */
+
+const ReviewResultModel = require('../../../models/review-result.model.js')
+
+/**
+ * Fetches the review results data for a given licence along with their associated charge elements and return logs
+ *
+ * @param {String} licenceId the id of the licence
+ *
+ * @returns {module:ReviewResultModel} the review result data associated with the given licence
+ */
+async function go (licenceId) {
+ const reviewResults = await _fetchReviewResults(licenceId)
+
+ return reviewResults
+}
+
+async function _fetchReviewResults (licenceId) {
+ return ReviewResultModel.query()
+ .where('licenceId', licenceId)
+ .select(
+ 'reviewChargeElementResultId',
+ 'chargeReferenceId',
+ 'reviewReturnResultId'
+ )
+ .withGraphFetched('reviewChargeElementResults')
+ .modifyGraph('reviewChargeElementResults', (builder) => {
+ builder.select([
+ 'id',
+ 'chargeDatesOverlap',
+ 'aggregate'
+ ])
+ })
+ .withGraphFetched('reviewReturnResults')
+ .modifyGraph('reviewReturnResults', (builder) => {
+ builder.select([
+ 'id',
+ 'underQuery',
+ 'quantity',
+ 'allocated',
+ 'abstractionOutsidePeriod',
+ 'status',
+ 'dueDate',
+ 'receivedDate'
+ ])
+ })
+}
+
+module.exports = {
+ go
+}
diff --git a/app/services/bill-runs/two-part-tariff/match-and-allocate.service.js b/app/services/bill-runs/two-part-tariff/match-and-allocate.service.js
index 69ba8592b6..6187d18c38 100644
--- a/app/services/bill-runs/two-part-tariff/match-and-allocate.service.js
+++ b/app/services/bill-runs/two-part-tariff/match-and-allocate.service.js
@@ -64,7 +64,7 @@ async function _process (licences, billingPeriods, billRun) {
})
})
- await PersistAllocatedLicenceToResultsService.go(billRun.billingBatchId, licence)
+ await PersistAllocatedLicenceToResultsService.go(billRun.id, licence)
}
}
diff --git a/app/services/bill-runs/two-part-tariff/review-bill-run.service.js b/app/services/bill-runs/two-part-tariff/review-bill-run.service.js
new file mode 100644
index 0000000000..844c9c4bd1
--- /dev/null
+++ b/app/services/bill-runs/two-part-tariff/review-bill-run.service.js
@@ -0,0 +1,32 @@
+'use strict'
+
+/**
+ * Orchestrates fetching and presenting the data needed for the review bill run page
+ * @module ReviewBillRunService
+ */
+
+const DetermineBillRunIssuesService = require('./determine-bill-run-issues.service.js')
+const FetchBillRunLicencesService = require('./fetch-bill-run-licences.service.js')
+const ReviewBillRunPresenter = require('../../../presenters/bill-runs/two-part-tariff/review-bill-run.presenter.js')
+
+/**
+ * Orchestrates fetching and presenting the data needed for the review bill run page
+ *
+ * @param {string} id The UUID for the bill run to review
+ *
+ * @returns {Object} an object representing the `pageData` needed by the review bill run template. It contains details of
+ * the bill run and the licences linked to it.
+ */
+async function go (id) {
+ const { billRun, licences } = await FetchBillRunLicencesService.go(id)
+
+ await DetermineBillRunIssuesService.go(licences)
+
+ const pageData = ReviewBillRunPresenter.go(billRun, licences)
+
+ return pageData
+}
+
+module.exports = {
+ go
+}
diff --git a/app/services/bills/fetch-bill-service.js b/app/services/bills/fetch-bill-service.js
index dbec2a16e4..98feb571a1 100644
--- a/app/services/bills/fetch-bill-service.js
+++ b/app/services/bills/fetch-bill-service.js
@@ -29,7 +29,7 @@ async function go (id) {
}
async function _fetchBill (id) {
- const result = BillModel.query()
+ return BillModel.query()
.findById(id)
.select([
'id',
@@ -66,8 +66,6 @@ async function _fetchBill (id) {
'displayName'
])
})
-
- return result
}
/**
@@ -84,7 +82,7 @@ async function _fetchBill (id) {
* Objection.js.
*/
async function _fetchLicenceSummaries (id) {
- const results = await db
+ return db
.distinct(['bil.id', 'bil.licenceRef'])
.sum('bt.net_amount AS total')
.from('billLicences AS bil')
@@ -93,8 +91,6 @@ async function _fetchLicenceSummaries (id) {
.where('bt.chargeType', 'standard')
.groupBy('bil.id', 'bil.licenceRef')
.orderBy('bil.licenceRef')
-
- return results
}
module.exports = {
diff --git a/app/services/jobs/export/fetch-table-names.service.js b/app/services/jobs/export/fetch-table-names.service.js
index 68403a396d..4e398a0f5c 100644
--- a/app/services/jobs/export/fetch-table-names.service.js
+++ b/app/services/jobs/export/fetch-table-names.service.js
@@ -35,7 +35,7 @@ async function _fetchTableNames (schemaName) {
AND table_type = 'BASE TABLE';
`
- return await db.raw(query)
+ return db.raw(query)
}
function _pluckTableNames (tableData) {
diff --git a/app/views/bill-runs/review.njk b/app/views/bill-runs/review.njk
index 2d8c0e1756..b58fd9895c 100644
--- a/app/views/bill-runs/review.njk
+++ b/app/views/bill-runs/review.njk
@@ -1,8 +1,167 @@
+{% extends 'layout.njk' %}
+{% from "govuk/components/back-link/macro.njk" import govukBackLink %}
+{% from "govuk/components/tag/macro.njk" import govukTag %}
+{% from "govuk/components/button/macro.njk" import govukButton %}
+{% from "govuk/components/summary-list/macro.njk" import govukSummaryList %}
+{% from "govuk/components/inset-text/macro.njk" import govukInsetText %}
+{% from "govuk/components/input/macro.njk" import govukInput %}
+{% from "govuk/components/radios/macro.njk" import govukRadios %}
+{% from "govuk/components/details/macro.njk" import govukDetails %}
+{% from "govuk/components/table/macro.njk" import govukTable %}
+
+{% from "macros/badge.njk" import badge %}
+
+{% block breadcrumbs %}
+ {# Back link #}
+ {{
+ govukBackLink({
+ text: 'Go back to bill runs',
+ href: '/billing/batch/list'
+ })
+ }}
+{% endblock %}
+
{% block content %}
{# Main heading #}
-
-
-
-
+
+
+
{{region}} {{billRunType}} bill run
+
{{pageTitle}}
+
+ {# Status badge #}
+ {% if status == 'review' %}
+ {% set colour = "govuk-tag--blue govuk-!-font-size-27" %}
+ {% else %}
+ {% set colour = "govuk-tag--green govuk-!-font-size-27" %}
+ {% endif %}
+
+
+ {{govukTag({
+ text: status,
+ classes: colour
+ })}}
+
+
+ {# Bill run meta-data #}
+ {{ govukSummaryList({
+ classes: 'govuk-summary-list--no-border',
+ rows: [
+ {
+ key: {
+ text: 'Date created',
+ classes: "meta-data__label"
+ },
+ value: {
+ text: dateCreated,
+ classes: "meta-data__value"
+ }
+ },
+ {
+ key: {
+ text: 'Region',
+ classes: "meta-data__label"
+ },
+ value: {
+ text: region,
+ classes: "meta-data__value"
+ }
+ },
+ {
+ key: {
+ text: 'Bill run type',
+ classes: "meta-data__label"
+ },
+ value: {
+ text: billRunType | capitalize,
+ classes: "meta-data__value"
+ }
+ },
+ {
+ key: {
+ text: 'Charge scheme',
+ classes: "meta-data__label"
+ },
+ value: {
+ text: 'Current',
+ classes: "meta-data__value"
+ }
+ },
+ {
+ key: {
+ text: 'Financial year',
+ classes: "meta-data__label"
+ },
+ value: {
+ text: financialYear,
+ classes: "meta-data__value"
+ }
+ }
+ ]
+ }) }}
+
+
+ {# Dynamic message either telling the user they have issues to deal with or that they can generate bills #}
+ {% if licencesToReviewCount > 0 %}
+
+ {{ govukInsetText({
+ text: 'You need to review ' + licencesToReviewCount + ' licences with returns data issues. You can then continue and send the bill run.'
+ }) }}
+
+ {% else %}
+
+ You have resolved all returns data issues. Continue to generate bills.
+
+ {% endif %}
+
+ {# Generate the row data for the table #}
+ {% set tableRows = [] %}
+ {% if preparedLicences.length > 0 %}
+ {% for licence in preparedLicences %}
+ {% if licence.status == 'review' %}
+ {% set colour = "govuk-tag--blue" %}
+ {% else %}
+ {% set colour = "govuk-tag--green" %}
+ {% endif %}
+
+ {% set statusTag %}
+ {{govukTag({
+ text: licence.status,
+ classes: colour
+ })}}
+ {% endset %}
+
+ {% set action = '
View licence matching for licence ' + licence.licenceRef + '' %}
+ {% set tableRow = [
+ {html: action | safe},
+ {text: licence.licenceHolder},
+ {text: licence.issue },
+ {html: statusTag, format: "numeric"}
+ ] %}
+ {% set tableRows = (tableRows.push(tableRow), tableRows) %}
+ {% endfor %}
+ {% endif %}
+
+ {# Table displaying details of the licences in the bill run. These results take into account any filter that is set #}
+ {{ govukTable({
+ caption: "Showing all " + numberOfLicences + " licences",
+ captionClasses: "govuk-table__caption--s govuk-!-margin-bottom-2",
+ firstCellIsHeader: false,
+ head: [
+ {
+ text: 'Licence'
+ },
+ {
+ text: 'Licence holder'
+ },
+ {
+ text: 'Issue'
+ },
+ {
+ text: 'Status',
+ format: "numeric"
+ }
+ ],
+ rows: tableRows
+ }) }}
{% endblock %}
diff --git a/test/controllers/bill-runs.controller.test.js b/test/controllers/bill-runs.controller.test.js
index 500e20dde8..067fd9e3c7 100644
--- a/test/controllers/bill-runs.controller.test.js
+++ b/test/controllers/bill-runs.controller.test.js
@@ -10,6 +10,7 @@ const { expect } = Code
// Things we need to stub
const Boom = require('@hapi/boom')
+const ReviewBillRunService = require('../../app/services/bill-runs/two-part-tariff/review-bill-run.service.js')
const StartBillRunProcessService = require('../../app/services/bill-runs/start-bill-run-process.service.js')
const ViewBillRunService = require('../../app/services/bill-runs/view-bill-run.service.js')
@@ -166,10 +167,17 @@ describe('Bill Runs controller', () => {
})
describe('when a request is valid', () => {
+ beforeEach(() => {
+ Sinon.stub(ReviewBillRunService, 'go').resolves(_reviewBillRunData())
+ })
+
it('returns a 200 response', async () => {
const response = await server.inject(options)
expect(response.statusCode).to.equal(200)
+ expect(response.payload).to.contain('two-part tariff')
+ expect(response.payload).to.contain('Southern (Test replica)')
+ expect(response.payload).to.contain('Showing all 1 licences')
})
})
})
@@ -226,6 +234,28 @@ function _multiGroupBillRun () {
}
}
+function _reviewBillRunData () {
+ return {
+ region: 'Southern (Test replica)',
+ status: 'review',
+ dateCreated: '6 November 2023',
+ financialYear: '2021 to 2022',
+ chargeScheme: 'Current',
+ billRunType: 'two-part tariff',
+ numberOfLicences: 1,
+ licencesToReviewCount: 1,
+ licences: [
+ {
+ licenceId: 'cc4bbb18-0d6a-4254-ac2c-7409de814d7e',
+ licenceRef: '1/11/11/*1/1111',
+ licenceHolder: 'Big Farm Ltd',
+ licenceIssues: 'Multiple Issues',
+ licenceStatus: 'review'
+ }
+ ]
+ }
+}
+
function _singleGroupBillRun () {
return {
billsCount: '1 Annual bill',
diff --git a/test/models/licence.model.test.js b/test/models/licence.model.test.js
index 7d45878ed6..8cfa33735d 100644
--- a/test/models/licence.model.test.js
+++ b/test/models/licence.model.test.js
@@ -27,6 +27,8 @@ const LicenceVersionModel = require('../../app/models/licence-version.model.js')
const RegionHelper = require('../support/helpers/region.helper.js')
const RegionModel = require('../../app/models/region.model.js')
const RegisteredToAndLicenceNameSeeder = require('../support/seeders/registered-to-and-licence-name.seeder.js')
+const ReviewResultHelper = require('../support/helpers/review-result.helper.js')
+const ReviewResultModel = require('../../app/models/review-result.model.js')
const WorkflowHelper = require('../support/helpers/workflow.helper.js')
const WorkflowModel = require('../../app/models/workflow.model.js')
@@ -247,6 +249,41 @@ describe('Licence model', () => {
})
})
+ describe('when linking to review results', () => {
+ let testReviewResults
+
+ beforeEach(async () => {
+ const { id } = testRecord
+
+ testReviewResults = []
+ for (let i = 0; i < 2; i++) {
+ const reviewResult = await ReviewResultHelper.add({ licenceId: id })
+ testReviewResults.push(reviewResult)
+ }
+ })
+
+ it('can successfully run a related query', async () => {
+ const query = await LicenceModel.query()
+ .innerJoinRelated('reviewResults')
+
+ expect(query).to.exist()
+ })
+
+ it('can eager load the workflows', async () => {
+ const result = await LicenceModel.query()
+ .findById(testRecord.id)
+ .withGraphFetched('reviewResults')
+
+ expect(result).to.be.instanceOf(LicenceModel)
+ expect(result.id).to.equal(testRecord.id)
+
+ expect(result.reviewResults).to.be.an.array()
+ expect(result.reviewResults[0]).to.be.an.instanceOf(ReviewResultModel)
+ expect(result.reviewResults).to.include(testReviewResults[0])
+ expect(result.reviewResults).to.include(testReviewResults[1])
+ })
+ })
+
describe('when linking to workflows', () => {
let testWorkflows
diff --git a/test/models/review-result.model.test.js b/test/models/review-result.model.test.js
index 75a2f7b9c2..b442f7c865 100644
--- a/test/models/review-result.model.test.js
+++ b/test/models/review-result.model.test.js
@@ -9,6 +9,8 @@ const { expect } = Code
// Test helpers
const DatabaseHelper = require('../support/helpers/database.helper.js')
+const LicenceHelper = require('../support/helpers/licence.helper.js')
+const LicenceModel = require('../../app/models/licence.model.js')
const ReviewChargeElementResultHelper = require('../support/helpers/review-charge-element-result.helper.js')
const ReviewChargeElementResultModel = require('../../app/models/review-charge-element-result.model.js')
const ReviewResultHelper = require('../support/helpers/review-result.helper.js')
@@ -95,5 +97,34 @@ describe('Review Result model', () => {
expect(result.reviewReturnResults).to.equal(testReviewReturnResult)
})
})
+
+ describe('when linking to licence', () => {
+ let testLicence
+
+ beforeEach(async () => {
+ testLicence = await LicenceHelper.add()
+
+ testRecord = await ReviewResultHelper.add({ licenceId: testLicence.id })
+ })
+
+ it('can successfully run a related query', async () => {
+ const query = await ReviewResultModel.query()
+ .innerJoinRelated('licence')
+
+ expect(query).to.exist()
+ })
+
+ it('can eager load the licence', async () => {
+ const result = await ReviewResultModel.query()
+ .findById(testRecord.id)
+ .withGraphFetched('licence')
+
+ expect(result).to.be.instanceOf(ReviewResultModel)
+ expect(result.id).to.equal(testRecord.id)
+
+ expect(result.licence).to.be.instanceOf(LicenceModel)
+ expect(result.licence).to.equal(testLicence)
+ })
+ })
})
})
diff --git a/test/presenters/bill-runs/two-part-tariff/review-bill-run.presenter.test.js b/test/presenters/bill-runs/two-part-tariff/review-bill-run.presenter.test.js
new file mode 100644
index 0000000000..8d67436848
--- /dev/null
+++ b/test/presenters/bill-runs/two-part-tariff/review-bill-run.presenter.test.js
@@ -0,0 +1,98 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+
+const { describe, it } = exports.lab = Lab.script()
+const { expect } = Code
+
+// Thing under test
+const ReviewBillRunPresenter = require('../../../../app/presenters/bill-runs/two-part-tariff/review-bill-run.presenter.js')
+
+describe('Review Bill Run presenter', () => {
+ describe('when there is data to be presented for review', () => {
+ const testBillRun = _testBillRun()
+ const testLicences = _testLicences()
+
+ it('correctly presents the data', () => {
+ const result = ReviewBillRunPresenter.go(testBillRun, testLicences)
+
+ expect(result).to.equal({
+ region: 'Southern (Test replica)',
+ status: 'review',
+ dateCreated: '17 January 2024',
+ financialYear: '2022 to 2023',
+ billRunType: 'two-part tariff',
+ numberOfLicences: 3,
+ licencesToReviewCount: 1,
+ preparedLicences: [
+ {
+ licenceRef: '1/11/11/*11/1111',
+ licenceHolder: 'Big Farm Ltd',
+ status: 'ready',
+ issue: ''
+ },
+ {
+ licenceRef: '2/22/22/*S2/2222',
+ licenceHolder: 'Bob Bobbles',
+ status: 'ready',
+ issue: 'Abstraction outside period'
+ },
+ {
+ licenceRef: '3/33/33/*3/3333',
+ licenceHolder: 'Farmer Palmer',
+ status: 'review',
+ issue: 'Multiple Issues'
+ }
+ ]
+ })
+ })
+ })
+})
+
+function _testBillRun () {
+ return {
+ id: 'b21bd372-cd04-405d-824e-5180d854121c',
+ createdAt: new Date('2024-01-17'),
+ status: 'review',
+ toFinancialYearEnding: 2023,
+ batchType: 'two_part_tariff',
+ region: {
+ displayName: 'Southern (Test replica)'
+ }
+ }
+}
+
+function _testLicences () {
+ return [
+ // Licence with no issues
+ {
+ id: 'cc4bbb18-0d6a-4254-ac2c-7409de814d7e',
+ licenceRef: '1/11/11/*11/1111',
+ licenceHolder: 'Big Farm Ltd',
+ issues: [],
+ status: 'ready'
+ },
+ // Licence with a single issue
+ {
+ id: '395bdc01-605b-44f5-9d90-5836cc013799',
+ licenceRef: '2/22/22/*S2/2222',
+ licenceHolder: 'Bob Bobbles',
+ issues: ['Abstraction outside period'],
+ status: 'ready'
+ },
+ // Licence with multiple issues
+ {
+ id: 'fdae33da-9195-4b97-976a-9791bc4f6b66',
+ licenceRef: '3/33/33/*3/3333',
+ licenceHolder: 'Farmer Palmer',
+ issues: [
+ 'Abstraction outside period',
+ 'Over abstraction',
+ 'Overlap of charge dates'
+ ],
+ status: 'review'
+ }
+ ]
+}
diff --git a/test/services/bill-runs/two-part-tariff/determine-bill-run-issues.service.test.js b/test/services/bill-runs/two-part-tariff/determine-bill-run-issues.service.test.js
new file mode 100644
index 0000000000..30e669149b
--- /dev/null
+++ b/test/services/bill-runs/two-part-tariff/determine-bill-run-issues.service.test.js
@@ -0,0 +1,163 @@
+'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
+
+// Things we need to stub
+const FetchReviewResultsService = require('../../../../app/services/bill-runs/two-part-tariff/fetch-review-results.service.js')
+
+// Thing under test
+const DetermineBillRunIssuesService = require('../../../../app/services/bill-runs/two-part-tariff/determine-bill-run-issues.service.js')
+
+describe('Determine Bill Run Issues Service', () => {
+ afterEach(() => {
+ Sinon.restore()
+ })
+
+ describe('when given licences', () => {
+ const testLicences = _testLicences()
+
+ beforeEach(() => {
+ Sinon.stub(FetchReviewResultsService, 'go')
+ .onFirstCall().resolves(_readyLicenceReviewResults())
+ .onSecondCall().resolves(_reviewLicenceReviewResults())
+ })
+
+ it('it fetches their review result records', async () => {
+ await DetermineBillRunIssuesService.go(testLicences)
+
+ expect(FetchReviewResultsService.go.called).to.be.true()
+ })
+
+ describe('that have issues', () => {
+ it('adds the status of `review` or `ready` to the licence', async () => {
+ await DetermineBillRunIssuesService.go(testLicences)
+
+ expect(testLicences[0].status).to.equal('ready')
+ expect(testLicences[1].status).to.equal('review')
+ })
+
+ it('adds the specific issues to the licence', async () => {
+ await DetermineBillRunIssuesService.go(testLicences)
+
+ expect(testLicences[0].issues).to.equal([
+ 'Abstraction outside period',
+ 'No returns received',
+ 'Over abstraction',
+ 'Returns received late',
+ 'Some returns not received'
+ ])
+
+ expect(testLicences[1].issues).to.equal([
+ 'Aggregate factor',
+ 'Checking query',
+ 'Overlap of charge dates',
+ 'Returns received but not processed',
+ 'Returns split over charge references',
+ 'Unable to match returns'
+ ])
+ })
+ })
+ })
+})
+
+// The test data in _reviewLicenceReviewResults is set up to trigger all the issues with a status of review
+function _reviewLicenceReviewResults () {
+ return [
+ {
+ reviewChargeElementResultId: '9f60684c-89fc-48d6-8a12-27dd2beaad36',
+ chargeReferenceId: '8b44bdb9-3417-4d20-b9b2-0ca3438b6abb',
+ reviewReturnResultId: '90eceec8-18de-4497-8102-3a7c6259d41a',
+ reviewChargeElementResults: {
+ id: '9f60684c-89fc-48d6-8a12-27dd2beaad36',
+ chargeDatesOverlap: true,
+ aggregate: 0.5
+ },
+ reviewReturnResults: {
+ id: '90eceec8-18de-4497-8102-3a7c6259d41a',
+ underQuery: true,
+ quantity: 0,
+ allocated: 0,
+ abstractionOutsidePeriod: false,
+ status: 'received',
+ dueDate: new Date('2023-04-28'),
+ receivedDate: new Date('2023-04-28')
+ }
+ },
+ {
+ reviewChargeElementResultId: '9f60684c-89fc-48d6-8a12-27dd2beaad36',
+ chargeReferenceId: '90eceec8-18de-4497-8102-3a7c6259d41a',
+ reviewReturnResultId: '90eceec8-18de-4497-8102-3a7c6259d41a',
+ reviewChargeElementResults: {
+ id: '9f60684c-89fc-48d6-8a12-27dd2beaad36',
+ chargeDatesOverlap: true,
+ aggregate: 0.5
+ },
+ reviewReturnResults: {
+ id: '90eceec8-18de-4497-8102-3a7c6259d41a',
+ underQuery: true,
+ quantity: 0,
+ allocated: 0,
+ abstractionOutsidePeriod: false,
+ status: 'received',
+ dueDate: new Date('2023-04-28'),
+ receivedDate: new Date('2023-04-28')
+ }
+ },
+ {
+ reviewChargeElementResultId: '9f60684c-89fc-48d6-8a12-27dd2beaad36',
+ chargeReferenceId: '90eceec8-18de-4497-8102-3a7c6259d41a',
+ reviewReturnResultId: null,
+ reviewChargeElementResults: {
+ id: '9f60684c-89fc-48d6-8a12-27dd2beaad36',
+ chargeDatesOverlap: true,
+ aggregate: 0.5
+ }
+ }
+ ]
+}
+
+// The test data in _readyLicenceReviewResults is set up to trigger all the issues with a status of ready
+function _readyLicenceReviewResults () {
+ return [
+ {
+ reviewChargeElementResultId: '9f60684c-89fc-48d6-8a12-27dd2beaad36',
+ chargeReferenceId: '8b44bdb9-3417-4d20-b9b2-0ca3438b6abb',
+ reviewReturnResultId: '90eceec8-18de-4497-8102-3a7c6259d41a',
+ reviewChargeElementResults: {
+ id: '9f60684c-89fc-48d6-8a12-27dd2beaad36',
+ chargeDatesOverlap: false,
+ aggregate: 1
+ },
+ reviewReturnResults: {
+ id: '90eceec8-18de-4497-8102-3a7c6259d41a',
+ underQuery: false,
+ quantity: 1,
+ allocated: 0.5,
+ abstractionOutsidePeriod: true,
+ status: 'due',
+ dueDate: new Date('2023-04-28'),
+ receivedDate: new Date('2023-04-29')
+ }
+ }
+ ]
+}
+
+function _testLicences () {
+ return [
+ {
+ licenceId: 'cc4bbb18-0d6a-4254-ac2c-7409de814d7e',
+ licenceHolder: 'Big Farm Ltd',
+ licenceRef: '1/11/11/*11/1111'
+ }, {
+ licenceId: 'fdae33da-9195-4b97-976a-9791bc4f6b66',
+ licenceHolder: 'Bob Bobbles',
+ licenceRef: '2/22/22/*S2/2222'
+ }
+ ]
+}
diff --git a/test/services/bill-runs/two-part-tariff/fetch-bill-run-licences.service.test.js b/test/services/bill-runs/two-part-tariff/fetch-bill-run-licences.service.test.js
new file mode 100644
index 0000000000..18a7ee0e6b
--- /dev/null
+++ b/test/services/bill-runs/two-part-tariff/fetch-bill-run-licences.service.test.js
@@ -0,0 +1,70 @@
+'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 BillRunHelper = require('../../../support/helpers/bill-run.helper.js')
+const DatabaseHelper = require('../../../support/helpers/database.helper.js')
+const LicenceHelper = require('../../../support/helpers/licence.helper.js')
+const LicenceHolderSeeder = require('../../../support/seeders/licence-holder.seeder.js')
+const RegionHelper = require('../../../support/helpers/region.helper.js')
+const ReviewResultHelper = require('../../../support/helpers/review-result.helper.js')
+
+// Thing under test
+const FetchBillRunLicencesService = require('../../../../app/services/bill-runs/two-part-tariff/fetch-bill-run-licences.service.js')
+
+describe('Fetch Bill Run Licences service', () => {
+ beforeEach(async () => {
+ await DatabaseHelper.clean()
+ })
+
+ describe('when there is a valid bill run', () => {
+ let billRun
+ let region
+
+ beforeEach(async () => {
+ region = await RegionHelper.add()
+ billRun = await BillRunHelper.add({ regionId: region.id, batchType: 'two_part_tariff' })
+ })
+
+ describe('and there are licences in the bill run', () => {
+ let testLicence
+
+ beforeEach(async () => {
+ testLicence = await LicenceHelper.add()
+ await LicenceHolderSeeder.seed(testLicence.licenceRef)
+ await ReviewResultHelper.add({ billRunId: billRun.id, licenceId: testLicence.id })
+ })
+
+ it('returns details of the bill run and the licences in it', async () => {
+ const result = await FetchBillRunLicencesService.go(billRun.id)
+
+ expect(result.billRun.id).to.equal(billRun.id)
+ expect(result.billRun.createdAt).to.equal(billRun.createdAt)
+ expect(result.billRun.status).to.equal(billRun.status)
+ expect(result.billRun.toFinancialYearEnding).to.equal(billRun.toFinancialYearEnding)
+ expect(result.billRun.batchType).to.equal(billRun.batchType)
+ expect(result.billRun.region.displayName).to.equal(region.displayName)
+
+ expect(result.licences).to.have.length(1)
+ expect(result.licences[0].id).to.equal(testLicence.id)
+ expect(result.licences[0].licenceHolder).to.equal('Licence Holder Ltd')
+ expect(result.licences[0].licenceRef).to.equal(testLicence.licenceRef)
+ })
+ })
+ })
+
+ describe('when there is an invalid bill run id passed to the service', () => {
+ it('returns no results', async () => {
+ const result = await FetchBillRunLicencesService.go('56db85ed-767f-4c83-8174-5ad9c80fd00d')
+
+ expect(result.billRun).to.be.undefined()
+ expect(result.licences).to.have.length(0)
+ })
+ })
+})
diff --git a/test/services/bill-runs/two-part-tariff/fetch-review-results.service.test.js b/test/services/bill-runs/two-part-tariff/fetch-review-results.service.test.js
new file mode 100644
index 0000000000..5461c91ca5
--- /dev/null
+++ b/test/services/bill-runs/two-part-tariff/fetch-review-results.service.test.js
@@ -0,0 +1,136 @@
+'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 DatabaseHelper = require('../../../support/helpers/database.helper.js')
+const ReviewChargeElementsResultsHelper = require('../../..//support/helpers/review-charge-element-result.helper.js')
+const ReviewResultsHelper = require('../../..//support/helpers/review-result.helper.js')
+const ReviewReturnResultsHelper = require('../../../support/helpers/review-return-result.helper.js')
+
+// Thing under test
+const FetchReviewResultsService = require('../../../../app/services/bill-runs/two-part-tariff/fetch-review-results.service.js')
+
+describe('Fetch Review Results Service', () => {
+ const licenceId = '83fe31e7-2f16-4be7-b557-bbada2323d92'
+
+ beforeEach(async () => {
+ await DatabaseHelper.clean()
+ })
+
+ describe('when the licence has data for review', () => {
+ let reviewChargeElementsResults
+ let reviewReturnResults
+ let reviewResults
+
+ describe('with related charge elements and returns', () => {
+ beforeEach(async () => {
+ reviewChargeElementsResults = await ReviewChargeElementsResultsHelper.add()
+ reviewReturnResults = await ReviewReturnResultsHelper.add()
+ reviewResults = await ReviewResultsHelper.add({
+ licenceId,
+ reviewChargeElementResultId: reviewChargeElementsResults.id,
+ reviewReturnResultId: reviewReturnResults.id
+ })
+ })
+
+ it('returns the review result with the related charge element and returns', async () => {
+ const result = await FetchReviewResultsService.go(licenceId)
+
+ expect(result[0]).to.equal({
+ reviewChargeElementResultId: reviewResults.reviewChargeElementResultId,
+ chargeReferenceId: reviewResults.chargeReferenceId,
+ reviewReturnResultId: reviewReturnResults.id,
+ reviewChargeElementResults: {
+ id: reviewChargeElementsResults.id,
+ chargeDatesOverlap: reviewChargeElementsResults.chargeDatesOverlap,
+ aggregate: reviewChargeElementsResults.aggregate
+ },
+ reviewReturnResults: {
+ id: reviewReturnResults.id,
+ underQuery: reviewReturnResults.underQuery,
+ quantity: reviewReturnResults.quantity,
+ allocated: reviewReturnResults.allocated,
+ abstractionOutsidePeriod: reviewReturnResults.abstractionOutsidePeriod,
+ status: reviewReturnResults.status,
+ dueDate: reviewReturnResults.dueDate,
+ receivedDate: reviewReturnResults.receivedDate
+ }
+ })
+ })
+ })
+
+ describe('with related charge elements but no returns', () => {
+ beforeEach(async () => {
+ reviewChargeElementsResults = await ReviewChargeElementsResultsHelper.add()
+ reviewResults = await ReviewResultsHelper.add({
+ licenceId,
+ reviewChargeElementResultId: reviewChargeElementsResults.id,
+ reviewReturnResultId: null
+ })
+ })
+
+ it('returns the review results with the related charge elements', async () => {
+ const result = await FetchReviewResultsService.go(licenceId)
+
+ expect(result[0]).to.equal({
+ reviewChargeElementResultId: reviewResults.reviewChargeElementResultId,
+ chargeReferenceId: reviewResults.chargeReferenceId,
+ reviewReturnResultId: reviewResults.reviewReturnResultId,
+ reviewChargeElementResults: {
+ id: reviewChargeElementsResults.id,
+ chargeDatesOverlap: reviewChargeElementsResults.chargeDatesOverlap,
+ aggregate: reviewChargeElementsResults.aggregate
+ },
+ reviewReturnResults: null
+ })
+ })
+ })
+
+ describe('with related returns but no charge elements', () => {
+ beforeEach(async () => {
+ reviewReturnResults = await ReviewReturnResultsHelper.add()
+
+ reviewResults = await ReviewResultsHelper.add({
+ licenceId,
+ reviewChargeElementResultId: null,
+ reviewReturnResultId: reviewReturnResults.id
+ })
+ })
+
+ it('returns the review results with the related returns', async () => {
+ const result = await FetchReviewResultsService.go(licenceId)
+
+ expect(result[0]).to.equal({
+ reviewChargeElementResultId: reviewResults.reviewChargeElementResultId,
+ chargeReferenceId: reviewResults.chargeReferenceId,
+ reviewReturnResultId: reviewResults.reviewReturnResultId,
+ reviewChargeElementResults: null,
+ reviewReturnResults: {
+ id: reviewReturnResults.id,
+ underQuery: reviewReturnResults.underQuery,
+ quantity: reviewReturnResults.quantity,
+ allocated: reviewReturnResults.allocated,
+ abstractionOutsidePeriod: reviewReturnResults.abstractionOutsidePeriod,
+ status: reviewReturnResults.status,
+ dueDate: reviewReturnResults.dueDate,
+ receivedDate: reviewReturnResults.receivedDate
+ }
+ })
+ })
+ })
+ })
+
+ describe('when there are no applicable review results', () => {
+ it('returns no records', async () => {
+ const result = await FetchReviewResultsService.go(licenceId)
+
+ expect(result).to.be.empty()
+ })
+ })
+})
diff --git a/test/services/bill-runs/two-part-tariff/match-and-allocate.service.test.js b/test/services/bill-runs/two-part-tariff/match-and-allocate.service.test.js
index 2c4c9a6be9..41a88ca117 100644
--- a/test/services/bill-runs/two-part-tariff/match-and-allocate.service.test.js
+++ b/test/services/bill-runs/two-part-tariff/match-and-allocate.service.test.js
@@ -8,8 +8,6 @@ const Sinon = require('sinon')
const { describe, it, beforeEach, afterEach } = exports.lab = Lab.script()
const { expect } = Code
-// Test helpers
-
// Things we need to stub
const AllocateReturnsToChargeElementService = require('../../../../app/services/bill-runs/two-part-tariff/allocate-returns-to-charge-element.service.js')
const FetchLicencesService = require('../../../../app/services/bill-runs/two-part-tariff/fetch-licences.service.js')
diff --git a/test/services/bill-runs/two-part-tariff/review-bill-run.service.test.js b/test/services/bill-runs/two-part-tariff/review-bill-run.service.test.js
new file mode 100644
index 0000000000..7679949c2b
--- /dev/null
+++ b/test/services/bill-runs/two-part-tariff/review-bill-run.service.test.js
@@ -0,0 +1,49 @@
+'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
+
+// Things we need to stub
+const DetermineBillRunIssuesService = require('../../../../app/services/bill-runs/two-part-tariff/determine-bill-run-issues.service.js')
+const FetchBillRunLicencesService = require('../../../../app/services/bill-runs/two-part-tariff/fetch-bill-run-licences.service.js')
+const ReviewBillRunPresenter = require('../../../../app/presenters/bill-runs/two-part-tariff/review-bill-run.presenter.js')
+
+// Thing under test
+const ReviewBillRunService = require('../../../../app/services/bill-runs/two-part-tariff/review-bill-run.service.js')
+
+describe('Review Bill Run Service', () => {
+ afterEach(() => {
+ Sinon.restore()
+ })
+
+ describe('when a bill run with a matching ID exists', () => {
+ const billRunId = '2c80bd22-a005-4cf4-a2a2-73812a9861de'
+
+ beforeEach(() => {
+ Sinon.stub(FetchBillRunLicencesService, 'go').resolves({ billRun: 'bill data', billRunLicences: 'licence data' })
+ Sinon.stub(DetermineBillRunIssuesService, 'go').resolves()
+ Sinon.stub(ReviewBillRunPresenter, 'go').returns('page data')
+ })
+
+ it('will fetch the bill run data for the review page and return it once formatted by the presenter', async () => {
+ const result = await ReviewBillRunService.go(billRunId)
+
+ expect(result).to.equal('page data')
+
+ expect(FetchBillRunLicencesService.go.called).to.be.true()
+ expect(DetermineBillRunIssuesService.go.called).to.be.true()
+ expect(ReviewBillRunPresenter.go.called).to.be.true()
+ })
+ })
+
+ describe('when a bill run with a matching ID does not exist', () => {
+ it('throws an exception', async () => {
+ await expect(ReviewBillRunService.go('718c01a2-04bc-40f1-8e06-36c0ee50bb3a')).to.reject()
+ })
+ })
+})