Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Determine other factors for SROC transaction line #108

Merged
merged 34 commits into from
Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
182f9ac
Determine other factors for SROC transaction line
StuAA78 Feb 1, 2023
aaa1d97
Initial service and working skeleton test
StuAA78 Feb 6, 2023
4c97cb0
Remove stray `console.log()`
StuAA78 Feb 6, 2023
dd7aa55
Test currently-developed fields
StuAA78 Feb 6, 2023
4ef6a25
Migration to add abstraction period cols to `charge_elements` table
StuAA78 Feb 6, 2023
295f4de
Add default abstraction period dates to `ChargeElementHelper`
StuAA78 Feb 6, 2023
d0fb356
Add `billableDays` to output
StuAA78 Feb 6, 2023
862f253
Add `authorisedDays` to output
StuAA78 Feb 7, 2023
65f7f00
Refactor `authorisedDays` and `billableDays`
StuAA78 Feb 7, 2023
93ff013
Fix `chargeElementId`
StuAA78 Feb 7, 2023
8afae32
Remove `Sinon` dependency from tests
StuAA78 Feb 7, 2023
2678dd5
Small unit test refactor
StuAA78 Feb 7, 2023
d023c13
Add `chargeFactor` to output
StuAA78 Feb 7, 2023
8fb4551
Add `isWaterUndertaker` to output
StuAA78 Feb 8, 2023
3e9886c
Add `isNewLicence` to output
StuAA78 Feb 8, 2023
3870bd3
Add missing `isNewLicence` field to standard data test
StuAA78 Feb 8, 2023
0817b82
Add `isTwoPartSecondPartCharge` to output
StuAA78 Feb 8, 2023
e9b2c94
Add `isWaterCompanyCharge` to output
StuAA78 Feb 9, 2023
922fe22
Add optional chaining to `isWaterCompanyCharge`
StuAA78 Feb 9, 2023
ccc50d9
Add `isSupportedSource` and `supportedSourceName` to output
StuAA78 Feb 9, 2023
eae3904
Add `description` to output
StuAA78 Feb 9, 2023
56aba74
Better docs for authorised and billable days calculations
StuAA78 Feb 9, 2023
dab2b36
Remove `.only` from unit tests
StuAA78 Feb 9, 2023
4771949
Merge branch 'main' into determine-factors-for-sroc-transaction-line
StuAA78 Feb 10, 2023
1a2cec0
Add all `billableDays` returned by `AbstractionBillingPeriodService`
StuAA78 Feb 10, 2023
1eeac75
Start documenting `go`
StuAA78 Feb 10, 2023
0b11225
Improved charge purpose testing
StuAA78 Feb 15, 2023
e41bd63
Correctly calculate billable days
StuAA78 Feb 15, 2023
921d1be
Add `isTwoPartSecondPartCharge` option
StuAA78 Feb 16, 2023
fd52162
Extract charge period from chargeVersion
StuAA78 Feb 16, 2023
9e13c1b
Remove `.only`
StuAA78 Feb 16, 2023
7e0e27b
Document service
StuAA78 Feb 16, 2023
79e6ad1
Merge branch 'main' into determine-factors-for-sroc-transaction-line
StuAA78 Feb 20, 2023
cc22798
Code tidy
StuAA78 Feb 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
'use strict'

/**
* Transforms provided data into the format required for an sroc transaction line
*
* @module FormatSrocTransactionLineService
*/

const AbstractionBillingPeriodService = require('./abstraction-billing-period.service.js')
const ConsolidateDateRangesService = require('./consolidate-date-ranges.service.js')

/**
* Takes a charge element, charge version and financial year and returns an object representing an sroc transaction
* line, formatted ready to be inserted into the db.
*
* @param {Object} chargeElement The charge element the transaction is to be created for.
* @param {Object} chargeVersion The charge version the transaction is to be created for.
* @param {Integer} financialYearEnding The year that the financial year of the transaction ends.
* @param {Object} [options] Object of options to set for the transaction. All options default to `false`
* @param {Boolean} [options.isCompensationCharge] Is this transaction a compensation charge?
* @param {Boolean} [options.isWaterUndertaker] Is this transaction for a water undertaker?
* @param {Boolean} [options.isNewLicence] Is this transaction for a new licence?
* @param {Boolean} [options.isTwoPartSecondPartCharge] Is this the second part charge for a two-part tariff?
*
* @returns {Object} The formatted transaction line data.
*/
function go (chargeElement, chargeVersion, financialYearEnding, options) {
const optionsData = _optionsDefaults(options)

const chargePeriod = _determineChargePeriod(chargeVersion, financialYearEnding)

return {
chargeElementId: chargeElement.chargeElementId,
startDate: chargePeriod.startDate,
endDate: chargePeriod.endDate,
source: chargeElement.source,
season: 'all year',
loss: chargeElement.loss,
isCredit: false,
chargeType: optionsData.isCompensationCharge ? 'compensation' : 'standard',
authorisedQuantity: chargeElement.volume,
billableQuantity: chargeElement.volume,
authorisedDays: _calculateAuthorisedDaysField(financialYearEnding, chargeElement),
billableDays: _calculateBillableDaysField(chargePeriod, chargeElement, financialYearEnding),
status: 'candidate',
description: _generateDescription(chargeElement, optionsData),
volume: chargeElement.volume,
section126Factor: chargeElement.adjustments.s126 || 1,
section127Agreement: !!chargeElement.adjustments.s127,
section130Agreement: !!chargeElement.adjustments.s130,
isNewLicence: optionsData.isNewLicence,
isTwoPartSecondPartCharge: optionsData.isTwoPartSecondPartCharge,
scheme: 'sroc',
aggregateFactor: chargeElement.adjustments.aggregate || 1,
adjustmentFactor: chargeElement.adjustments.charge || 1,
chargeCategoryCode: chargeElement.billingChargeCategory.reference,
chargeCategoryDescription: chargeElement.billingChargeCategory.shortDescription,
isSupportedSource: !!chargeElement.additionalCharges?.supportedSource?.name,
supportedSourceName: chargeElement.additionalCharges?.supportedSource?.name || null,
isWaterCompanyCharge: !!chargeElement.additionalCharges?.isSupplyPublicWater,
isWinterOnly: !!chargeElement.adjustments.winter,
isWaterUndertaker: optionsData.isWaterUndertaker,
purposes: _generatePurposes(chargeElement)
}
}

function _optionsDefaults (options) {
const defaults = {
isCompensationCharge: false,
isWaterUndertaker: false,
isNewLicence: false,
isTwoPartSecondPartCharge: false
}

return {
...defaults,
...options
}
}

/**
* Returns a charge period, which is an object comprising `startDate` and `endDate`
*
* The charge period is determined as the overlap between the charge version's start and end dates, and the financial
* year. So the charge period's start date is the later of the two's start dates, and the charge period end date is the
* earlier of the two's end dates.
*
* Note that charge versions may not have an end date; in this case, we simply use the financial year end date.
*/
function _determineChargePeriod (chargeVersion, financialYearEnding) {
const financialYearStartDate = new Date(financialYearEnding - 1, 3, 1)
const chargeVersionStartDate = chargeVersion.startDate
const latestStartDateTimestamp = Math.max(financialYearStartDate, chargeVersionStartDate)

const financialYearEndDate = new Date(financialYearEnding, 2, 31)
// If the charge version has no end date then use the financial year end date instead
const chargeVersionEndDate = chargeVersion.endDate || financialYearEndDate
const earliestEndDateTimestamp = Math.min(financialYearEndDate, chargeVersionEndDate)

return {
startDate: new Date(latestStartDateTimestamp),
endDate: new Date(earliestEndDateTimestamp)
}
}

/**
* Calculates the number of authorised days, ie. the number of overlapping days of the financial year and the charge
* element's abstraction periods
*/
function _calculateAuthorisedDaysField (financialYearEnding, chargeElement) {
// Define an abstraction period for the financial year, ie. 1/4 to 31/3
const abstractionPeriodToCalculateFor = {
abstractionPeriodStartDay: 1,
abstractionPeriodStartMonth: 4,
abstractionPeriodEndDay: 31,
abstractionPeriodEndMonth: 3
}

return _calculateNumberOfOverlappingDays(abstractionPeriodToCalculateFor, chargeElement, financialYearEnding)
}

/**
* Calculates the number of authorised days , ie. the number of overlapping days of the charge period and the charge
* element's abstraction periods
*/
function _calculateBillableDaysField (chargePeriod, chargeElement, financialYearEnding) {
// Note that we add 1 to the month as JavaScript months are zero-index (ie. 0 = Jan, 1 = Feb etc.)
const abstractionPeriodToCalculateFor = {
abstractionPeriodStartDay: chargePeriod.startDate.getDate(),
abstractionPeriodStartMonth: chargePeriod.startDate.getMonth() + 1,
abstractionPeriodEndDay: chargePeriod.endDate.getDate(),
abstractionPeriodEndMonth: chargePeriod.endDate.getMonth() + 1
}

return _calculateNumberOfOverlappingDays(abstractionPeriodToCalculateFor, chargeElement, financialYearEnding)
}

/**
* Takes an abstraction period and uses AbstractionBillingPeriodService to calculate the number of overlapping days of
* the provided period and the charge element's abstraction periods
*/
function _calculateNumberOfOverlappingDays (abstractionPeriodToCalculateFor, chargeElement, financialYearEnding) {
// Convert the abstraction periods of the charge element's charge purposes into actual dates
const chargePurposeDateRanges = _mapAbstractionPeriodsToDateRanges(chargeElement.chargePurposes, financialYearEnding)

// Consolidate the resulting date ranges to avoid counting the overlapping dates more than once
const consolidatedDateRanges = ConsolidateDateRangesService.go(chargePurposeDateRanges)

// AbstractionBillingPeriodService returns an array of abstraction periods, each of which has a `billableDays`
// property which is the number of overlapping days of the date range and the period we provide it with. Mapping the
// array we get back would give us an array of arrays; we therefore flatten this after mapping to give us just a flat
// array of abstraction periods
const abstractionPeriods = consolidatedDateRanges.map((dateRange) => {
return AbstractionBillingPeriodService.go(dateRange, abstractionPeriodToCalculateFor)
}).flat(Infinity)

// We now sum all the `billableDays` properties of the returned abstraction periods to give us the total number of
// overlapping days. Note that this could have been done in the previous step by summing as we go but we do it as a
// separate step for clarity
const totalBillableDays = abstractionPeriods.reduce((acc, abstractionPeriod) => {
return acc + abstractionPeriod.billableDays
}, 0)

return totalBillableDays
}

/**
* Takes an array of charge purposes and converts their abstraction periods (which are defined as the start and end
* day and months) into actual dates in the given financial year, returning them as an array of date ranges (ie.
* objects with a `startDate` and `endDate` property)
*/
function _mapAbstractionPeriodsToDateRanges (chargePurposes, financialYearEnding) {
const abstractionPeriods = chargePurposes.map(chargePurpose => {
const {
abstractionPeriodStartDay: startDay,
abstractionPeriodStartMonth: startMonth,
abstractionPeriodEndDay: endDay,
abstractionPeriodEndMonth: endMonth
} = chargePurpose

const abstractionPeriod = {}
// Reminder! Because of the unique qualities of Javascript, Year and Day are literal values, month is an index! So,
// January is actually 0, February is 1 etc. This is why we are always deducting 1 from the months.
if (endMonth === startMonth) {
if (endDay >= startDay) {
abstractionPeriod.startDate = new Date(financialYearEnding, startMonth - 1, startDay)
abstractionPeriod.endDate = new Date(financialYearEnding, endMonth - 1, endDay)
} else {
abstractionPeriod.startDate = new Date(financialYearEnding - 1, startMonth - 1, startDay)
abstractionPeriod.endDate = new Date(financialYearEnding, endMonth - 1, endDay)
}
} else if (endMonth >= startMonth) {
abstractionPeriod.startDate = new Date(financialYearEnding, startMonth - 1, startDay)
abstractionPeriod.endDate = new Date(financialYearEnding, endMonth - 1, endDay)
} else {
abstractionPeriod.startDate = new Date(financialYearEnding - 1, startMonth - 1, startDay)
abstractionPeriod.endDate = new Date(financialYearEnding, endMonth - 1, endDay)
}
return abstractionPeriod
})

return abstractionPeriods
}

/**
* Returns a json representation of all charge purposes in a charge element
*/
function _generatePurposes (chargeElement) {
const jsonChargePurposes = chargeElement.chargePurposes.map((chargePurpose) => {
return chargePurpose.toJSON()
})

return JSON.stringify(jsonChargePurposes)
}

function _generateDescription (chargeElement, options) {
if (options.isCompensationCharge) {
return 'Compensation charge: calculated from the charge reference, activity description and regional environmental improvement charge; excludes any supported source additional charge and two-part tariff charge agreement'
}

return `Water abstraction charge: ${chargeElement.description}`
}

module.exports = {
go
}
29 changes: 29 additions & 0 deletions db/migrations/20230206172349_alter-charge-elements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict'

const tableName = 'charge_elements'

exports.up = async function (knex) {
await knex
.schema
.withSchema('water')
.alterTable(tableName, table => {
table.smallint('abstraction_period_start_day')
table.smallint('abstraction_period_start_month')
table.smallint('abstraction_period_end_day')
table.smallint('abstraction_period_end_month')
})
}

exports.down = async function (knex) {
return knex
.schema
.withSchema('water')
.alterTable(tableName, table => {
table.dropColumns(
'abstraction_period_start_day',
'abstraction_period_start_month',
'abstraction_period_end_day',
'abstraction_period_end_month'
)
})
}
Loading