Skip to content

Commit

Permalink
Create Completed Return Logs page (#1484)
Browse files Browse the repository at this point in the history
https://eaflood.atlassian.net/browse/WATER-4745

This PR introduces the View Return Logs page. The following changes are made to accommodate this:

- `unitNames` and `returnUnits` are added to `static-lookups.lib.js` for consistency;
- `ReturnSubmissionModel` has methods added `$applyReadings()`, `$meter()`, `$method()` and `$units()`;
- `BasePresenter` has `formatNumber()` which formats a number for display;
- `BasePresenter.titleCase()` is amended to first convert the string to lower-case, which was necessary for us to correctly convert text from `ALL CAPS` to `All Caps`.

Display of the page is gated behind the `enableSystemReturnsView` feature flag.
  • Loading branch information
StuAA78 authored Jan 30, 2025
1 parent 6c8f297 commit a6d886e
Show file tree
Hide file tree
Showing 24 changed files with 2,600 additions and 92 deletions.
28 changes: 28 additions & 0 deletions app/controllers/return-logs.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict'

/**
* Controller for /return-logs endpoints
* @module ReturnLogsController
*/

const Boom = require('@hapi/boom')

const ViewReturnLogService = require('../services/return-logs/view-return-log.service.js')

async function view(request, h) {
const { auth, query } = request

if (!query.id) {
return Boom.badImplementation('Id is required')
}

const version = query.version ? Number(query.version) : 0

const pageData = await ViewReturnLogService.go(query.id, version, auth)

return h.view('return-logs/view.njk', pageData)
}

module.exports = {
view
}
18 changes: 17 additions & 1 deletion app/lib/static-lookups.lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ const returnRequirementReasons = {
'transfer-and-now-chargeable': 'Licence transferred and now chargeable'
}

const unitNames = {
CUBIC_METRES: 'm³',
LITRES: 'l',
MEGALITRES: 'Ml',
GALLONS: 'gal'
}

const returnUnits = {
[unitNames.CUBIC_METRES]: { multiplier: 1, label: 'cubic metres' },
[unitNames.LITRES]: { multiplier: 1000, label: 'litres' },
[unitNames.MEGALITRES]: { multiplier: 0.001, label: 'megalitres' },
[unitNames.GALLONS]: { multiplier: 219.969248299, label: 'gallons' }
}

const sources = ['nald', 'wrls']

const twoPartTariffReviewIssues = {
Expand Down Expand Up @@ -151,7 +165,9 @@ module.exports = {
returnPeriodDates,
returnRequirementFrequencies,
returnRequirementReasons,
returnUnits,
sources,
twoPartTariffReviewIssues,
quarterlyReturnPeriods
quarterlyReturnPeriods,
unitNames
}
62 changes: 62 additions & 0 deletions app/models/return-submission.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
const { Model } = require('objection')

const BaseModel = require('./base.model.js')
const { formatDateObjectToISO } = require('../lib/dates.lib.js')
const { unitNames } = require('../lib/static-lookups.lib.js')

class ReturnSubmissionModel extends BaseModel {
static get tableName() {
Expand Down Expand Up @@ -39,6 +41,66 @@ class ReturnSubmissionModel extends BaseModel {
}
}
}

/**
* Applies meter readings to return submission lines
*/
$applyReadings() {
const meter = this.$meter()

if (!this.returnSubmissionLines || !meter?.readings) {
return
}

for (const line of this.returnSubmissionLines) {
const { startDate, endDate } = line
const key = `${formatDateObjectToISO(startDate)}_${formatDateObjectToISO(endDate)}`

const reading = meter?.readings[key]

line.reading = reading ?? null
}
}

/**
* Returns the first meter from the return submission's metadata, or null if no meters exist.
*
* @returns {object|null} The first meter, or null.
*/
$meter() {
if (!this.metadata?.meters) {
return null
}

return this.metadata.meters[0]
}

/**
* Returns the method of measurement from the return submission's metadata, defaulting to 'abstractionVolumes' if none
* is specified.
*
* @returns {string} The method of measurement.
*/
$method() {
if (!this.metadata?.method) {
return 'abstractionVolumes'
}

return this.metadata.method
}

/**
* Returns the unit of measurement from the return submission's metadata, defaulting to cubic metres if not specified.
*
* @returns {string} The unit of measurement.
*/
$units() {
if (!this.metadata?.units) {
return unitNames.CUBIC_METRES
}

return this.metadata.units
}
}

module.exports = ReturnSubmissionModel
2 changes: 2 additions & 0 deletions app/plugins/router.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const LicenceRoutes = require('../routes/licence.routes.js')
const LicenceEndDatesRoutes = require('../routes/licences-end-dates.routes.js')
const MonitoringStationRoutes = require('../routes/monitoring-station.routes.js')
const ReturnLogSetupRoutes = require('../routes/return-logs-setup.routes.js')
const ReturnLogRoutes = require('../routes/return-logs.routes.js')
const ReturnVersionsSetupRoutes = require('../routes/return-versions-setup.routes.js')
const ReturnVersionsRoutes = require('../routes/return-versions.routes.js')
const RootRoutes = require('../routes/root.routes.js')
Expand All @@ -51,6 +52,7 @@ const routes = [
...JobRoutes,
...MonitoringStationRoutes,
...ReturnLogSetupRoutes,
...ReturnLogRoutes,
...ReturnVersionsRoutes,
...ReturnVersionsSetupRoutes,
...DataRoutes,
Expand Down
46 changes: 35 additions & 11 deletions app/presenters/base.presenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,25 @@ function formatMoney(valueInPence, signed = false) {
return `${sign}£${positiveValueInPounds.toLocaleString('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}

/**
* Formats a number as a string with commas and decimal places, for example, 1000 as '1,000.000'
*
* @param {number} number - The number to format
* @param {number} [minimumFractionDigits=0] - Minimum number of digits after the decimal point
* @param {number} [maximumFractionDigits=3] - Maximum number of digits after the decimal point
*
* @returns {string|null} The formatted number or null if the number is null or undefined
*/
function formatNumber(number, minimumFractionDigits = 0, maximumFractionDigits = 3) {
// NOTE: We don't use !number because that would match 0, which for this helper is a valid number and something we
// want to format
if (number === null) {
return null
}

return number.toLocaleString('en-GB', { minimumFractionDigits, maximumFractionDigits })
}

/**
* Formats a number, which represents a value in pence to pounds, for example, 12776805 as '127768.05'
*
Expand Down Expand Up @@ -260,7 +279,7 @@ function sentenceCase(value) {
}

/**
* Convert a string to title case by capitalizing the first letter of each word
* Convert a string to title case by lowercasing all characters then capitalizing the first letter of each word
*
* Will work for strings containing multiple words or only one.
*
Expand All @@ -269,16 +288,20 @@ function sentenceCase(value) {
* @returns {string} The title cased string
*/
function titleCase(value) {
const words = value.split(' ')
const titleCasedWords = []

words.forEach((word) => {
const titleCasedWord = word.charAt(0).toUpperCase() + word.slice(1)

titleCasedWords.push(titleCasedWord)
})

return titleCasedWords.join(' ')
return (
value
// We convert the entire string to lower case so we can correctly convert all-caps strings like 'TEXT' to 'Text'.
.toLowerCase()
// replace() iterates over a string. We use it to match each word with a regex and apply a function to each match.
// We define a word as:
// - Starts on a word boundary (eg. a space, bracket, dash, etc.). We use \b for this.
// - Has a word character. We use \w for this. We use + to specify there are one or more word chars.
// This regex correctly handles converting cases like '(text)' to '(Text)'.
.replace(/\b\w+/g, (match) => {
// Convert the first char of the matched string to upper case and append the remainder of the string
return match.charAt(0).toUpperCase() + match.slice(1)
})
)
}

module.exports = {
Expand All @@ -293,6 +316,7 @@ module.exports = {
formatLongDate,
formatLongDateTime,
formatMoney,
formatNumber,
formatPounds,
leftPadZeroes,
sentenceCase,
Expand Down
10 changes: 5 additions & 5 deletions app/presenters/bill-runs/review/base-review.presenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ function calculateTotalBillableReturns(reviewChargeElements) {
function determineReturnLink(reviewReturn) {
const { returnId, returnStatus } = reviewReturn

if (FeatureFlagsConfig.enableSystemReturnsView) {
return `/system/return-logs?id=${returnId}`
}

if (['due', 'received'].includes(returnStatus)) {
if (FeatureFlagsConfig.enableSystemReturnsView) {
return `/system/return-logs/setup?returnLogId=${returnId}`
} else {
return `/return/internal?returnId=${returnId}`
}
return `/return/internal?returnId=${returnId}`
}

return `/returns/return?id=${returnId}`
Expand Down
10 changes: 5 additions & 5 deletions app/presenters/licences/view-licence-returns.presenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ function go(returnLogs, hasRequirements, auth) {
}

function _link(status, returnLogId, canManageReturns) {
if (FeatureFlagsConfig.enableSystemReturnsView) {
return `/system/return-logs?id=${returnLogId}`
}

if (['completed', 'void'].includes(status)) {
return `/returns/return?id=${returnLogId}`
}

if (canManageReturns) {
if (FeatureFlagsConfig.enableSystemReturnsView) {
return `/system/return-logs/setup?returnLogId=${returnLogId}`
} else {
return `/return/internal?returnId=${returnLogId}`
}
return `/return/internal?returnId=${returnLogId}`
}

return null
Expand Down
Loading

0 comments on commit a6d886e

Please sign in to comment.