Skip to content

Commit

Permalink
Merge branch 'main' into fix-reissue-great-rename-errors
Browse files Browse the repository at this point in the history
  • Loading branch information
Cruikshanks authored Jan 17, 2024
2 parents 80bc97e + 657dc29 commit 7c658b8
Show file tree
Hide file tree
Showing 23 changed files with 1,569 additions and 20 deletions.
15 changes: 14 additions & 1 deletion app/controllers/licences.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

const InitiateReturnRequirementSessionService = require('../services/return-requirements/initiate-return-requirement-session.service.js')
const ViewLicenceService = require('../services/licences/view-licence.service.js')

async function noReturnsRequired (request, h) {
const { id } = request.params
Expand All @@ -23,7 +24,19 @@ async function returnsRequired (request, h) {
return h.redirect(`/system/return-requirements/${session.id}/start-date`)
}

async function view (request, h) {
const { id } = request.params

const data = await ViewLicenceService.go(id)

return h.view('licences/view.njk', {
activeNavBar: 'search',
...data
})
}

module.exports = {
noReturnsRequired,
returnsRequired
returnsRequired,
view
}
60 changes: 60 additions & 0 deletions app/models/licence-document-header.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict'

/**
* Model for licence_document_headers (crm.document_header)
* @module LicenceDocumentHeaderModel
*/

const { Model } = require('objection')

const BaseModel = require('./base.model.js')

/**
* Represents an instance of a licence document header record
*
* For reference, the licence document header record is a 'nothing' record! It doesn't hold anything not already stored
* in other licence tables. We only need to obtain the licence holder name which matches what the legacy UI displays.
*
* We think the reason for the table being there is because of the original ambition to have a generic permit
* repository that could be used for all types of permits. Along with that a 'tactical' CRM that interfaced with the
* permit repository was built. Though the permit repository referred to them as licences, the CRM chose to refer to
* them as 'documents' hence the `crm.document_header` table.
*
* The previous team then tried to refactor the CRM schema but never completed it. Hence we have the `crm_v2` schema
* and more licence duplication. Finally, at a later date the previous team then opted to create a `licences` table in
* the `water` schema we think to support charging.
*
* So, if you see the phrase 'Document' you can assume the instance is one of these older copies of a licence.
* `water.licences` is the primary licence record. But we often have to dip into this older tables for other bits of
* information, for example, the licence holder name currently displayed in the legacy UI. This is why we have models
* like this one.
*
* Welcome to dealing with the legacy database schema! ¯\_(ツ)_/¯
*/
class LicenceDocumentHeaderModel extends BaseModel {
static get tableName () {
return 'licenceDocumentHeaders'
}

// Defining which fields contain json allows us to insert an object without needing to stringify it first
static get jsonAttributes () {
return [
'metadata'
]
}

static get relationMappings () {
return {
licence: {
relation: Model.BelongsToOneRelation,
modelClass: 'licence.model',
join: {
from: 'licenceDocumentHeaders.licenceRef',
to: 'licences.licenceRef'
}
}
}
}
}

module.exports = LicenceDocumentHeaderModel
8 changes: 8 additions & 0 deletions app/models/licence.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ class LicenceModel extends BaseModel {
to: 'licenceDocuments.licenceRef'
}
},
licenceDocumentHeader: {
relation: Model.BelongsToOneRelation,
modelClass: 'licence-document-header.model',
join: {
from: 'licences.licenceRef',
to: 'licenceDocumentHeaders.licenceRef'
}
},
licenceVersions: {
relation: Model.HasManyRelation,
modelClass: 'licence-version.model',
Expand Down
44 changes: 44 additions & 0 deletions app/presenters/licences/view-licence.presenter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict'

/**
* Formats data for the view `/licences/{id}/` page
* @module ViewLicencePresenter
*/

const { formatLongDate } = require('../base.presenter.js')

/**
* Formats data for the `/licences/{id}/` page
*
* @param {module:LicenceModel} licence - The licence where the data will be extracted for from
*
* @returns {Object} The data formatted for the view template
*/
function go (licence) {
const { expiredDate, id, licenceRef, region, startDate } = licence

return {
id,
endDate: _endDate(expiredDate),
licenceRef,
region: region.displayName,
startDate: formatLongDate(startDate)
}
}

/**
* Formats the expired date of the licence as the end date for the view
*
* @module ViewLicencePresenter
*/
function _endDate (expiredDate) {
if (!expiredDate || expiredDate < Date.now()) {
return null
}

return formatLongDate(expiredDate)
}

module.exports = {
go
}
12 changes: 12 additions & 0 deletions app/routes/licence.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ const LicencesController = require('../controllers/licences.controller.js')

const routes = [
{
method: 'GET',
path: '/licences/{id}',
handler: LicencesController.view,
options: {
auth: {
access: {
scope: ['billing']
}
},
description: 'View a licence page'
}
}, {
method: 'GET',
path: '/licences/{id}/no-returns-required',
handler: LicencesController.noReturnsRequired,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use strict'

/**
* Allocates where applicable the abstracted volumes on the return log with the appropriate charge element
* @module AllocateReturnsToChargeElementService
*/

const { periodsOverlap } = require('../../../lib/general.lib.js')

/**
* For a chargeElement with matching returns any abstracted volume recorded on the return log will be allocated to the
* charge element up to a maximum of the charge elements authorised volume, or the remaining authorised volume on the
* charge reference, whichever is lower.
*
* @param {module:ChargeElementModel} chargeElement - The charge element to allocate return logs against
* @param {module:ReturnLogModel[]} matchingReturns - Return logs that matched to the charge element
* @param {module:ChargeVersionModel} chargePeriod - The charge period taken from the charge version
* @param {module:ChargeReferenceModel} chargeReference - The charge reference the element belongs to
*/
function go (chargeElement, matchingReturns, chargePeriod, chargeReference) {
matchingReturns.forEach((matchedReturn, i) => {
// We don't allocate returns with issues
if (matchedReturn.issues) {
return
}

// We can only allocate up to the authorised volume on the charge reference, even if there is charge elements
// unallocated and returns to be allocated
if (chargeReference.allocatedQuantity >= chargeReference.volume) {
return
}

// Finally, we can only allocate to the charge element if there is unallocated volume left!
if (chargeElement.allocatedQuantity >= chargeElement.authorisedAnnualQuantity) {
return
}

const matchedLines = _matchLines(chargeElement, matchedReturn.returnSubmissions[0].returnSubmissionLines)

if (matchedLines.length > 0) {
_allocateReturns(chargeElement, matchedReturn, chargePeriod, chargeReference, i, matchedLines)
}
})
}

function _allocateReturns (chargeElement, matchedReturn, chargePeriod, chargeReference, i, matchedLines) {
matchedLines.forEach((matchedLine) => {
const remainingAllocation = chargeElement.authorisedAnnualQuantity - chargeElement.allocatedQuantity
if (remainingAllocation > 0) {
// We default how much to allocate to what is unallocated on the line i.e. remaining >= line.unallocated
let qtyToAllocate = matchedLine.unallocated

// If what remains is actually less than the line we instead set qtyToAllocate to what remains. We check this
// on both the chargeReference and the element
const chargeReferenceRemainingAllocation = chargeReference.volume - chargeReference.allocatedQuantity

if (qtyToAllocate > chargeReferenceRemainingAllocation) {
qtyToAllocate = chargeReferenceRemainingAllocation
} else if (remainingAllocation < matchedLine.unallocated) {
qtyToAllocate = remainingAllocation
}

// We do this check to prevent overwriting the value with false once it's been set to true as it only requires
// a single `matchedLine` to overlap the charge period
if (!chargeElement.chargeDatesOverlap) {
chargeElement.chargeDatesOverlap = _chargeDatesOverlap(matchedLine, chargePeriod)
}

chargeElement.allocatedQuantity += qtyToAllocate
chargeElement.returnLogs[i].allocatedQuantity += qtyToAllocate

matchedLine.unallocated -= qtyToAllocate
matchedReturn.allocatedQuantity += qtyToAllocate
chargeReference.allocatedQuantity += qtyToAllocate
}
})
}

function _chargeDatesOverlap (matchedLine, chargePeriod) {
const { startDate: chargePeriodStartDate, endDate: chargePeriodEndDate } = chargePeriod
const { startDate: lineStartDate, endDate: lineEndDate } = matchedLine

if (lineStartDate < chargePeriodEndDate && lineEndDate > chargePeriodEndDate) {
return true
}

return (lineStartDate < chargePeriodStartDate && lineEndDate > chargePeriodStartDate)
}

function _matchLines (chargeElement, returnSubmissionLines) {
return returnSubmissionLines.filter((returnSubmissionLine) => {
if (returnSubmissionLine.unallocated === 0) {
return false
}

const { startDate, endDate } = returnSubmissionLine
return periodsOverlap(chargeElement.abstractionPeriods, [{ startDate, endDate }])
})
}

module.exports = {
go
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Match and allocate licences to returns for a two-part tariff bill run for the given billing periods
* @module MatchAndAllocateService
*/

const AllocateReturnsToChargeElementService = require('./allocate-returns-to-charge-element.service.js')
const FetchLicencesService = require('./fetch-licences.service.js')
const MatchReturnsToChargeElementService = require('./match-returns-to-charge-element.service.js')
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')

/**
* Performs the two-part tariff matching and allocating
*
* This function initiates the processing of matching and allocating by fetching licenses within the specified region and billing period.
* Each license undergoes individual processing, including fetching and preparing return logs, charge versions, and
* charge references. The allocated quantity for each charge reference is set to 0, and matching return logs are allocated
* to the corresponding charge elements.
*
* After processing each license, the results are persisted using PersistAllocatedLicenceToResultsService.
*
* @param {module:BillRunModel} billRun - The bill run object containing billing information
* @param {Object[]} billingPeriods - An array of billing periods each containing a `startDate` and `endDate`
*
* @returns {Array} - An array of processed licences associated with the bill run
*/
async function go (billRun, billingPeriods) {
const startTime = process.hrtime.bigint()

const licences = await FetchLicencesService.go(billRun.regionId, billingPeriods[0])

if (licences.length > 0) {
await _process(licences, billingPeriods, billRun)
}

_calculateAndLogTime(startTime)

return licences
}

function _calculateAndLogTime (startTime) {
const endTime = process.hrtime.bigint()
const timeTakenNs = endTime - startTime
const timeTakenMs = timeTakenNs / 1000000n

global.GlobalNotifier.omg('Two part tariff matching complete', { timeTakenMs })
}

async function _process (licences, billingPeriods, billRun) {
for (const licence of licences) {
await PrepareReturnLogsService.go(licence, billingPeriods[0])

const { chargeVersions, returnLogs } = licence
chargeVersions.forEach((chargeVersion) => {
PrepareChargeVersionService.go(chargeVersion, billingPeriods[0])

const { chargeReferences } = chargeVersion
chargeReferences.forEach((chargeReference) => {
chargeReference.allocatedQuantity = 0

const { chargeElements } = chargeReference

chargeElements.forEach((chargeElement) => {
const matchingReturns = MatchReturnsToChargeElementService.go(chargeElement, returnLogs)

if (matchingReturns.length > 0) {
AllocateReturnsToChargeElementService.go(chargeElement, matchingReturns, chargeVersion.chargePeriod, chargeReference)
}
})
})
})

await PersistAllocatedLicenceToResultsService.go(billRun.billingBatchId, licence)
}
}

module.exports = {
go
}
Loading

0 comments on commit 7c658b8

Please sign in to comment.