Skip to content

Commit

Permalink
Determine match and allocate issues and status
Browse files Browse the repository at this point in the history
https://eaflood.atlassian.net/browse/WATER-4378

During the refinement of future two-part tariff tickets, it was noted that the status of a licence could be changed by the user from a 'review' status to a 'ready' status. Before this was noted we were working out the licence status based on the issues the licence has with its returns and charge elements. This is done on every page where the licence status is being shown. Now that we know a status can be overwritten regardless of its issues, we now need to persist the licence status during the first time the engine works it out and show the persisted status instead of working it out each time. This will allow users to manually change the status.

The same thing applies to the charge elements. The status of the element is determined by the issues on it. This status at the moment is worked out every time it is displayed on a page. We have since learnt through refinement that a user can allocate volumes on the element and this will change the status from 'review' to 'ready'. This is regardless of the issues that remain on a charge element. So with this new information, it has made it clear that we now need to persist the initial status of the charge element to allow this to be overwritten when needed.

This opened up a lot of discussions about how we could refactor the review tables to work more effectively with how the two-part tariff engine works now. When we first built the tables we did so with the knowledge we had at the time. Now that we have worked on more tickets and started building actual pages we realised we could persist the data better.

TLDR: Review tables updated to add status and issues
  • Loading branch information
Beckyrose200 authored Feb 29, 2024
1 parent 561cb4f commit 875f478
Show file tree
Hide file tree
Showing 8 changed files with 532 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,33 @@ function _matchLines (chargeElement, returnSubmissionLines) {
})
}

/**
* Checks a return record for potential issues based on specific criteria and flags it accordingly
*/
function _checkReturnForIssues (matchedReturn) {
if (matchedReturn.nilReturn) {
return true
}

if (matchedReturn.underQuery) {
return true
}

if (matchedReturn.status !== 'completed') {
return true
}

if (matchedReturn.returnSubmissions.length === 0 || matchedReturn.returnSubmissions[0].returnSubmissionLines.length === 0) {
return true
}

return false
}

function _processCompletedReturns (chargeElement, matchingReturns, chargePeriod, chargeReference) {
matchingReturns.forEach((matchedReturn, i) => {
// We don't allocate returns with issues
if (matchedReturn.issues) {
if (_checkReturnForIssues(matchedReturn)) {
return
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
'use strict'

/**
* Determines the issues on a licences for a two-part tariff bill run
* @module DetermineLicenceIssuesService
*/

// 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'
]

/**
* Determines the issues on a licence charge elements and return logs and sets the status based on them
*
* @param {module:LicenceModel} licence - the two-part tariff licence included in the bill run
*/
async function go (licence) {
const { returnLogs: licenceReturnLogs, chargeVersions } = licence

const allReturnIssues = _determineReturnLogsIssues(licenceReturnLogs, licence)
const allElementIssues = _determineChargeElementsIssues(chargeVersions, licenceReturnLogs)

licence.status = _determineLicenceStatus(allElementIssues, allReturnIssues)
}

function _determineChargeElementsIssues (chargeVersions, licenceReturnLogs) {
const allElementIssues = []

chargeVersions.forEach((chargeVersion) => {
const { chargeReferences } = chargeVersion

chargeReferences.forEach((chargeReference) => {
const { chargeElements } = chargeReference

chargeElements.forEach((chargeElement) => {
const { returnLogs } = chargeElement

const { elementIssues, status } = _elementIssues(chargeReference, chargeElement, licenceReturnLogs, returnLogs)

chargeElement.issues = elementIssues
chargeElement.status = status
allElementIssues.push(...elementIssues)
})
})
})

return allElementIssues
}

function _determineLicenceStatus (allElementIssues, allReturnIssues) {
const allLicenceIssues = [...allElementIssues, ...allReturnIssues]

// If a licence has more than one issue, or has 1 issue that is in the `REVIEW_STATUSES` array the licence status is
// set to 'Review' otherwise its 'Ready'
if (allLicenceIssues.length > 1 || REVIEW_STATUSES.includes(allLicenceIssues[0])) {
return 'review'
} else {
return 'ready'
}
}

function _determineReturnLogsIssues (returnLogs, licence) {
const allReturnsIssues = []

returnLogs.forEach((returnLog) => {
const returnLogIssues = _returnLogIssues(returnLog, licence)

returnLog.issues = returnLogIssues
allReturnsIssues.push(...returnLogIssues)
})

return allReturnsIssues
}

function _determineReturnSplitOverChargeReference (licence, returnLog) {
let chargeReferenceCounter = 0
const { chargeVersions } = licence

chargeVersions.forEach((chargeVersion) => {
const { chargeReferences } = chargeVersion

chargeReferences.forEach((chargeReference) => {
const { chargeElements } = chargeReference

// We do a .some here as we only care if the returnLog is present in the chargeReference at least once. If the
// return is present we increase our chargeReference counter by 1 to tally up how many unique chargeReference have
// matched to the return
const returnLogInChargeReference = chargeElements.some((chargeElement) => {
const { returnLogs: chargeElementReturnLogs } = chargeElement

return chargeElementReturnLogs.some((chargeElementReturnLog) => {
return chargeElementReturnLog.returnId === returnLog.id
})
})

if (returnLogInChargeReference) {
chargeReferenceCounter++
}
})
})

return chargeReferenceCounter > 1
}

function _elementIssues (chargeReference, chargeElement, licenceReturnLogs, returnLogs) {
let status = 'ready'
const elementIssues = []

// Issue Aggregate factor
if (chargeReference.aggregate !== 1) {
elementIssues.push('Aggregate factor')
status = 'review'
}

// Issue Overlap of charge dates
if (chargeElement.chargeDatesOverlap) {
elementIssues.push('Overlap of charge dates')
status = 'review'
}

// Issue Some returns not received
if (_someReturnsNotReceived(returnLogs, licenceReturnLogs)) {
elementIssues.push('Some returns not received')
}

// Unable to match return
if (returnLogs.length < 1) {
elementIssues.push('Unable to match return')
status = 'review'
}

return { elementIssues, status }
}

function _returnLogIssues (returnLog, licence) {
const returnLogIssues = []

// Abstraction outside period issue
if (returnLog.abstractionOutsidePeriod) {
returnLogIssues.push('Abstraction outside period')
}

// Checking query issue
if (returnLog.underQuery) {
returnLogIssues.push('Checking query')
}

// No returns received
if (returnLog.status === 'due') {
returnLogIssues.push('No returns received')
}

// Over abstraction
if (returnLog.quantity > returnLog.allocatedQuantity) {
returnLogIssues.push('Over abstraction')
}

// Returns received but not processed
if (returnLog.status === 'received') {
returnLogIssues.push('Returns received but not processed')
}

// Returns received late
if (returnLog.receivedDate > returnLog.dueDate) {
returnLogIssues.push('Returns received late')
}

// Returns split over charge references
if (_determineReturnSplitOverChargeReference(licence, returnLog)) {
returnLogIssues.push('Return split over charge references')
}

return returnLogIssues
}

function _someReturnsNotReceived (returnLogs, licenceReturnLogs) {
const returnLogIds = returnLogs.map((returnLog) => {
return returnLog.returnId
})

return licenceReturnLogs.some((licenceReturnLog) => {
return returnLogIds.includes(licenceReturnLog.id) && licenceReturnLog.status === 'due'
})
}

module.exports = {
go
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const MatchReturnsToChargeElementService = require('./match-returns-to-charge-el
const PrepareChargeVersionService = require('./prepare-charge-version.service.js')
const PrepareReturnLogsService = require('./prepare-return-logs.service.js')
const PersistAllocatedLicenceToResultsService = require('./persist-allocated-licence-to-results.service.js')
const DetermineLicenceIssuesService = require('./determine-licence-issues.service.js')

/**
* Performs the two-part tariff matching and allocating
Expand Down Expand Up @@ -36,7 +37,7 @@ async function go (billRun, billingPeriod) {
}

async function _process (licences, billingPeriod, billRun) {
for (const licence of licences) {
licences.forEach(async (licence) => {
await PrepareReturnLogsService.go(licence, billingPeriod)

const { chargeVersions, returnLogs } = licence
Expand Down Expand Up @@ -64,8 +65,9 @@ async function _process (licences, billingPeriod, billRun) {
})
})

await DetermineLicenceIssuesService.go(licence)
await PersistAllocatedLicenceToResultsService.go(billRun.id, licence)
}
})
}

module.exports = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,6 @@ function _abstractionOutsidePeriod (returnAbstractionPeriods, returnLine) {
return !periodsOverlap(returnAbstractionPeriods, [{ startDate, endDate }])
}

/**
* Checks a return record for potential issues based on specific criteria and flags it accordingly
*/
function _checkReturnForIssues (returnLog) {
if (returnLog.nilReturn) {
return true
}

if (returnLog.underQuery) {
return true
}

if (returnLog.status !== 'completed') {
return true
}

if (returnLog.returnSubmissions.length === 0 || returnLog.returnSubmissions[0].returnSubmissionLines.length === 0) {
return true
}

return false
}

async function _prepareReturnLogs (licence, billingPeriod) {
licence.returnLogs = await FetchReturnLogsForLicenceService.go(licence.licenceRef, billingPeriod)

Expand Down Expand Up @@ -86,8 +63,6 @@ function _prepReturnsForMatching (returnLogs, billingPeriod) {
returnLog.abstractionPeriods = abstractionPeriods
returnLog.abstractionOutsidePeriod = abstractionOutsidePeriod
returnLog.matched = false

returnLog.issues = (_checkReturnForIssues(returnLog))
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ describe('Allocate Returns to Charge Element Service', () => {
describe('with a return that has issues', () => {
beforeEach(() => {
testData = _generateTestData()
testData.matchingReturns[0].issues = true
testData.matchingReturns[0].underQuery = true
})

it('does not allocate anything from the return log', () => {
Expand Down Expand Up @@ -428,7 +428,7 @@ describe('Allocate Returns to Charge Element Service', () => {
})
})

function _generateTestData (returnStatus = 'complete') {
function _generateTestData (returnStatus = 'completed') {
// Data not required for the tests has been excluded from the generated data
const chargeElement = {
authorisedAnnualQuantity: 32,
Expand Down Expand Up @@ -505,9 +505,8 @@ function _generateTestData (returnStatus = 'complete') {
// If a returns status isn't `complete` it won't have any return submission lines and the `issues` will be `true`
const matchingReturns = [
{
returnSubmissions: returnStatus === 'complete' ? returnSubmissions : [],
returnSubmissions: returnStatus === 'completed' ? returnSubmissions : [],
allocatedQuantity: 0,
issues: returnStatus !== 'complete',
status: returnStatus
}
]
Expand Down
Loading

0 comments on commit 875f478

Please sign in to comment.