diff --git a/app/closure/process-closure-data.js b/app/closure/process-closure-data.js index 8de1e2c..4d17172 100644 --- a/app/closure/process-closure-data.js +++ b/app/closure/process-closure-data.js @@ -1,12 +1,12 @@ const parsedSchema = require('../routes/schemas/parsed-closure') - +const maxClosureDataLength = 3 const processClosureData = async (data) => { const uploadData = [] const splitData = data.split(/\r?\n|\r|\n/g) const closureLines = splitData.filter((str) => str !== '') for (const closureLine of closureLines) { const clData = closureLine.split(',') - if (clData.length !== 3) { + if (clData.length !== maxClosureDataLength) { return { errors: { details: [{ message: 'The file is not in the expected format' }] } } diff --git a/app/config/index.js b/app/config/index.js index cd57f9b..d9917ff 100644 --- a/app/config/index.js +++ b/app/config/index.js @@ -2,13 +2,18 @@ const joi = require('joi') const authConfig = require('./auth') const storageConfig = require('./storage') const messageConfig = require('./message') +const portNumber = 3007 +const staticCacheTimeout = 604800000 // Define config schema const schema = joi.object({ serviceName: joi.string().default('Payment management'), - port: joi.number().default(3007), - env: joi.string().valid('development', 'test', 'production').default('development'), - staticCacheTimeoutMillis: joi.number().default(7 * 24 * 60 * 60 * 1000), + port: joi.number().default(portNumber), + env: joi + .string() + .valid('development', 'test', 'production') + .default('development'), + staticCacheTimeoutMillis: joi.number().default(staticCacheTimeout), googleTagManagerKey: joi.string().default(''), paymentsEndpoint: joi.string().uri().required(), trackingEndpoint: joi.string().uri().required(), diff --git a/app/helpers/render-error-page.js b/app/helpers/render-error-page.js index c14d33e..8a98234 100644 --- a/app/helpers/render-error-page.js +++ b/app/helpers/render-error-page.js @@ -1,4 +1,5 @@ const { getSchemes } = require('./get-schemes') +const HTTP_BAD_REQUEST = 400 const renderErrorPage = async (view, request, h, err) => { request.log(['error', 'validation'], err) @@ -11,7 +12,7 @@ const renderErrorPage = async (view, request, h, err) => { }) : [] const schemes = await getSchemes() - return h.view(view, { schemes, errors }).code(400).takeover() + return h.view(view, { schemes, errors }).code(HTTP_BAD_REQUEST).takeover() } module.exports = { diff --git a/app/hold/handle-bulk-post.js b/app/hold/handle-bulk-post.js index e23c0e6..70dc7ba 100644 --- a/app/hold/handle-bulk-post.js +++ b/app/hold/handle-bulk-post.js @@ -2,17 +2,18 @@ const { post } = require('../api') const { getHoldCategories } = require('../holds') const { readFileContent } = require('./read-file-content') const { processHoldData } = require('./process-hold-data') +const HTTP_BAD_REQUEST = 400 const handleBulkPost = async (request, h) => { const data = readFileContent(request.payload.file.path) if (!data) { const { schemes, paymentHoldCategories } = await getHoldCategories() - return h.view('payment-holds/bulk', { schemes, paymentHoldCategories, errors: { details: [{ message: 'An error occurred whilst reading the file' }] } }).code(400).takeover() + return h.view('payment-holds/bulk', { schemes, paymentHoldCategories, errors: { details: [{ message: 'An error occurred whilst reading the file' }] } }).code(HTTP_BAD_REQUEST).takeover() } const { uploadData, errors } = await processHoldData(data) if (errors) { const { schemes, paymentHoldCategories } = await getHoldCategories() - return h.view('payment-holds/bulk', { schemes, paymentHoldCategories, errors }).code(400).takeover() + return h.view('payment-holds/bulk', { schemes, paymentHoldCategories, errors }).code(HTTP_BAD_REQUEST).takeover() } if (request.payload.remove) { await post('/payment-holds/bulk/remove', { data: uploadData, holdCategoryId: request.payload.holdCategoryId }, null) diff --git a/app/hold/process-hold-data.js b/app/hold/process-hold-data.js index b3f78c0..e752a90 100644 --- a/app/hold/process-hold-data.js +++ b/app/hold/process-hold-data.js @@ -3,8 +3,8 @@ const parsedSchema = require('../routes/schemas/parsed-hold') const processHoldData = async (data) => { const uploadData = [] const splitData = data.split(',') - for (const data of splitData) { - const result = parsedSchema.validate({ frn: data }, { + for (const frn of splitData) { + const result = parsedSchema.validate({ frn }, { abortEarly: false }) if (result.error) { @@ -12,7 +12,7 @@ const processHoldData = async (data) => { errors: result.error } } else { - uploadData.push(data) + uploadData.push(frn) } } return { diff --git a/app/payments/get-data.js b/app/payments/get-data.js index 9fc63f0..8110b90 100644 --- a/app/payments/get-data.js +++ b/app/payments/get-data.js @@ -7,20 +7,36 @@ const { sendMessage, receiveMessage } = require('../messaging') const getData = async (category, value) => { const messageId = uuidv4() const request = { category, value } - await sendMessage(request, TYPE, config.messageConfig.dataTopic, { messageId }) + + await sendMessage(request, TYPE, config.messageConfig.dataTopic, { + messageId + }) console.info('Data request sent:', util.inspect(request, false, null, true)) - const response = await receiveMessage(messageId, config.messageConfig.dataQueue) - if (response) { - console.info('Data response received:', util.inspect(response, false, null, true)) - if (Array.isArray(response.data)) { - for (let i = 0; i < response.data.length; i++) { - if (response.data[i].scheme === 'SFI') { - response.data[i].scheme = 'SFI22' - } - } - } + + const response = await receiveMessage( + messageId, + config.messageConfig.dataQueue + ) + + if (!response) { + return null + } + + console.info( + 'Data response received:', + util.inspect(response, false, null, true) + ) + + if (!Array.isArray(response.data)) { return response.data } + + const transformedData = response.data.map(item => ({ + ...item, + scheme: item.scheme === 'SFI' ? 'SFI22' : item.scheme + })) + + return transformedData } module.exports = { diff --git a/app/plugins/auth.js b/app/plugins/auth.js index dfd730c..6539442 100644 --- a/app/plugins/auth.js +++ b/app/plugins/auth.js @@ -2,15 +2,17 @@ const config = require('../config') const authCookie = require('@hapi/cookie') const auth = require('../auth') +const SESSION_AUTH = 'session-auth' + module.exports = { plugin: { name: 'auth', - register: async (server) => { + register: async server => { await server.register(authCookie) - server.auth.strategy('session-auth', 'cookie', { + server.auth.strategy(SESSION_AUTH, 'cookie', { cookie: { - name: 'session-auth', + name: SESSION_AUTH, password: config.authConfig.cookie.password, ttl: config.authConfig.cookie.ttl, path: '/', @@ -21,11 +23,14 @@ module.exports = { redirectTo: '/login' }) - server.auth.default('session-auth') + server.auth.default(SESSION_AUTH) server.ext('onPreAuth', async (request, h) => { if (request.auth.credentials) { - await auth.refresh(request.auth.credentials.account, request.cookieAuth) + await auth.refresh( + request.auth.credentials.account, + request.cookieAuth + ) } return h.continue }) diff --git a/app/plugins/error-pages.js b/app/plugins/error-pages.js index b0c378e..e0ef5a4 100644 --- a/app/plugins/error-pages.js +++ b/app/plugins/error-pages.js @@ -1,6 +1,7 @@ -/* -* Add an `onPreResponse` listener to return error pages -*/ +const HTTP_NOT_AUTHORIZED = 401 +const HTTP_FORBIDDEN = 403 +const HTTP_NOT_FOUND = 404 +const HTTP_SERVER_ERROR = 500 module.exports = { plugin: { @@ -10,18 +11,16 @@ module.exports = { const response = request.response if (response.isBoom) { - // An error was raised during - // processing the request const statusCode = response.output.statusCode - // if not authorised then request login - if (statusCode === 401 || statusCode === 403) { + if ( + statusCode === HTTP_NOT_AUTHORIZED || + statusCode === HTTP_FORBIDDEN + ) { return h.view('unauthorized').code(statusCode) } - // In the event of 404 - // return the `404` view - if (statusCode === 404) { + if (statusCode === HTTP_NOT_FOUND) { return h.view('404').code(statusCode) } @@ -31,8 +30,11 @@ module.exports = { message: response.message }) - // The return the `500` view - return h.view('500').code(statusCode) + if (statusCode >= HTTP_SERVER_ERROR) { + return h.view('500').code(statusCode) + } + + return h.continue } return h.continue }) diff --git a/app/plugins/view-context.js b/app/plugins/view-context.js index 9360229..16d8d96 100644 --- a/app/plugins/view-context.js +++ b/app/plugins/view-context.js @@ -1,4 +1,6 @@ const { mapAuth, getUser } = require('../auth') +const HTTP_NOT_FOUND = 404 +const HTTP_INTERNAL_SERVER_ERROR = 500 module.exports = { plugin: { @@ -6,7 +8,12 @@ module.exports = { register: (server, _options) => { server.ext('onPreResponse', (request, h) => { const statusCode = request.response.statusCode - if (request.response.variety === 'view' && statusCode !== 404 && statusCode !== 500 && request.response.source.context) { + if ( + request.response.variety === 'view' && + statusCode !== HTTP_NOT_FOUND && + statusCode !== HTTP_INTERNAL_SERVER_ERROR && + request.response.source.context + ) { request.response.source.context.auth = mapAuth(request) request.response.source.context.user = getUser(request) } diff --git a/app/routes/ap-ar-report-listing.js b/app/routes/ap-ar-report-listing.js index 48045b6..1d0b92b 100644 --- a/app/routes/ap-ar-report-listing.js +++ b/app/routes/ap-ar-report-listing.js @@ -4,168 +4,200 @@ const convertToCSV = require('../helpers/convert-to-csv') const apListingSchema = require('./schemas/ap-listing-schema') const config = require('../config/storage') -function generateRoutes (reportName, reportDataUrl, reportDataKey) { - return [ - { - method: 'GET', - path: `/report-list/${reportName}`, - options: { - auth: { scope: [holdAdmin, schemeAdmin, dataView] }, - handler: async (request, h) => { - return h.view(`reports-list/${reportName}`) - } - } +const REPORT_TYPES = { + REQUEST_EDITOR: 'request-editor-report', + CLAIM_LEVEL: 'claim-level-report', + AP_LISTING: 'ap-listing', + AR_LISTING: 'ar-listing', + AP_AR_LISTING: 'ap-ar-listing' +} + +const AUTH_SCOPE = { scope: [holdAdmin, schemeAdmin, dataView] } +const DEFAULT_START_DATE = '2015-01-01' +const HTTP_STATUS = { BAD_REQUEST: 400, NOT_FOUND: 404 } +const startsAt = 0 +const removeFromEnd = -4 + +const mapRequestEditorData = data => ({ + FRN: data.frn, + deltaAmount: data.deltaAmount, + SourceSystem: data.sourceSystem, + agreementNumber: data.agreementNumber, + invoiceNumber: data.invoiceNumber, + PaymentRequestNumber: data.paymentRequestNumber, + year: data.year, + receivedInRequestEditor: data.receivedInRequestEditor, + enriched: data.enriched, + debtType: data.debtType, + ledgerSplit: data.ledgerSplit, + releasedFromRequestEditor: data.releasedFromRequestEditor +}) + +const mapClaimLevelData = data => ({ + FRN: data.frn, + claimID: data.claimNumber, + revenueOrCapital: data.revenueOrCapital, + agreementNumber: data.agreementNumber, + year: data.year, + paymentCurrency: data.currency, + latestFullClaimAmount: data.value, + latestSitiPR: data.paymentRequestNumber, + latestInDAXAmount: data.daxValue, + latestInDAXPR: data.daxPaymentRequestNumber, + overallStatus: data.overallStatus, + crossBorderFlag: data.crossBorderFlag, + latestTransactionStatus: data.status, + valueStillToProcess: data.valueStillToProcess, + PRsStillToProcess: data.prStillToProcess +}) + +const mapBaseAPARData = data => ({ + Filename: data.daxFileName, + 'Date Time': data.lastUpdated, + Event: data.status, + FRN: data.frn, + 'Original Invoice Number': data.originalInvoiceNumber, + 'Original Invoice Value': data.value, + 'Invoice Number': data.invoiceNumber, + 'Invoice Delta Amount': data.deltaAmount, + 'D365 Invoice Imported': data.routedToRequestEditor, + 'D365 Invoice Payment': data.settledValue, + 'PH Error Status': data.phError, + 'D365 Error Status': data.daxError +}) + +const mapAPData = data => ({ + ...mapBaseAPARData(data) +}) + +const mapARData = data => { + const mapped = { ...mapBaseAPARData(data) } + delete mapped['D365 Invoice Payment'] + return mapped +} + +const getDataMapper = reportName => { + switch (reportName) { + case REPORT_TYPES.AR_LISTING: + return mapARData + case REPORT_TYPES.REQUEST_EDITOR: + return mapRequestEditorData + case REPORT_TYPES.CLAIM_LEVEL: + return mapClaimLevelData + default: + return mapAPData + } +} + +const formatDate = (day, month, year) => { + if (!day || !month || !year) { + return null + } + return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart( + 2, + '0' + )}` +} + +const getCurrentDate = () => { + const now = new Date() + return formatDate(now.getDate(), now.getMonth() + 1, now.getFullYear()) +} + +const getBaseFilename = reportName => { + const fileNames = { + [REPORT_TYPES.AR_LISTING]: config.arListingReportName, + [REPORT_TYPES.REQUEST_EDITOR]: config.requestEditorReportName, + [REPORT_TYPES.CLAIM_LEVEL]: config.claimLevelReportName, + default: config.apListingReportName + } + return (fileNames[reportName] || fileNames.default).slice( + startsAt, + removeFromEnd + ) +} + +const handleValidationError = async (request, h, err, reportName) => { + request.log(['error', 'validation'], err) + const errors = + err.details?.map(detail => ({ + text: detail.message, + href: '#' + detail.path[0] + })) || [] + return h + .view(`reports-list/${reportName}`, { errors }) + .code(HTTP_STATUS.BAD_REQUEST) + .takeover() +} + +const createGetRoute = reportName => ({ + method: 'GET', + path: `/report-list/${reportName}`, + options: { + auth: AUTH_SCOPE, + handler: async (_request, h) => h.view(`reports-list/${reportName}`) + } +}) + +const createDownloadRoute = (reportName, reportDataUrl, reportDataKey) => ({ + method: 'GET', + path: `/report-list/${reportName}/download`, + options: { + auth: AUTH_SCOPE, + validate: { + query: apListingSchema, + failAction: async (request, h, err) => + handleValidationError(request, h, err, reportName) }, - { - method: 'GET', - path: `/report-list/${reportName}/download`, - options: { - auth: { scope: [holdAdmin, schemeAdmin, dataView] }, - validate: { - query: apListingSchema, - failAction: async (request, h, err) => { - request.log(['error', 'validation'], err) - const errors = err.details - ? err.details.map(detail => { - return { - text: detail.message, - href: '#' + detail.path[0] - } - }) - : [] - const data = { errors } - if (reportName === 'ar-listing' || reportName === 'ap-ar-listing') { - return h.view('reports-list/ap-ar-listing', data).code(400).takeover() - } - if (reportName === 'request-editor-report') { - return h.view('reports-list/request-editor-report', data).code(400).takeover() - } - if (reportName === 'claim-level-report') { - return h.view('reports-list/claim-level-report', data).code(400).takeover() - } else { - return h.view('404', data).code(404).takeover() - } - } - }, - handler: async (request, h) => { - const { 'start-date-day': startDay, 'start-date-month': startMonth, 'start-date-year': startYear, 'end-date-day': endDay, 'end-date-month': endMonth, 'end-date-year': endYear } = request.query - - let url = reportDataUrl - let startDate, endDate - - if (startDay && startMonth && startYear) { - startDate = `${startYear}-${String(startMonth).padStart(2, '0')}-${String(startDay).padStart(2, '0')}` - } else { - startDate = '2015-01-01' - } - - if (endDay && endMonth && endYear) { - endDate = `${endYear}-${String(endMonth).padStart(2, '0')}-${String(endDay).padStart(2, '0')}` - } else if (startDate) { - const now = new Date() - endDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` - } - - if (startDate && endDate) { - url += `?startDate=${startDate}&endDate=${endDate}` - } - - try { - const response = await api.getTrackingData(url) - const trackingData = response.payload - const selectedData = trackingData[reportDataKey].map(data => { - let mappedData - if (reportName === 'request-editor-report') { - mappedData = { - FRN: data.frn, - deltaAmount: data.deltaAmount, - SourceSystem: data.sourceSystem, - agreementNumber: data.agreementNumber, - invoiceNumber: data.invoiceNumber, - PaymentRequestNumber: data.paymentRequestNumber, - year: data.year, - receivedInRequestEditor: data.receivedInRequestEditor, - enriched: data.enriched, - debtType: data.debtType, - ledgerSplit: data.ledgerSplit, - releasedFromRequestEditor: data.releasedFromRequestEditor - } - } else if (reportName === 'claim-level-report') { - mappedData = { - FRN: data.frn, - claimID: data.claimNumber, - revenueOrCapital: data.revenueOrCapital, - agreementNumber: data.agreementNumber, - year: data.year, - paymentCurrency: data.currency, - latestFullClaimAmount: data.value, - latestSitiPR: data.paymentRequestNumber, - latestInDAXAmount: data.daxValue, - latestInDAXPR: data.daxPaymentRequestNumber, - overallStatus: data.overallStatus, - crossBorderFlag: data.crossBorderFlag, - latestTransactionStatus: data.status, - valueStillToProcess: data.valueStillToProcess, - PRsStillToProcess: data.prStillToProcess - } - } else { - mappedData = { - Filename: data.daxFileName, - 'Date Time': data.lastUpdated, - Event: data.status, - FRN: data.frn, - 'Original Invoice Number': data.originalInvoiceNumber, - 'Original Invoice Value': data.value, - 'Invoice Number': data.invoiceNumber, - 'Invoice Delta Amount': data.deltaAmount, - 'D365 Invoice Imported': data.routedToRequestEditor, - 'D365 Invoice Payment': data.settledValue, - 'PH Error Status': data.phError, - 'D365 Error Status': data.daxError - } - if (reportName === 'ar-listing') { - delete mappedData['D365 Invoice Payment'] - } - } - return mappedData - }) - - if (selectedData.length === 0) { - return h.view(`reports-list/${reportName}`, { - errors: [{ - text: 'No data available for the selected date range' - }] - }) - } - - const csv = convertToCSV(selectedData) - - let baseFilename - switch (reportName) { - case 'ar-listing': - baseFilename = config.arListingReportName.slice(0, -4) - break - case 'request-editor-report': - baseFilename = config.requestEditorReportName.slice(0, -4) - break - case 'claim-level-report': - baseFilename = config.claimLevelReportName.slice(0, -4) - break - default: - baseFilename = config.apListingReportName.slice(0, -4) - } - const filename = `${baseFilename}-from-${startDate}-to-${endDate}.csv` - return h.response(csv) - .header('Content-Type', 'text/csv') - .header('Content-Disposition', `attachment; filename=${filename}`) - } catch (error) { - console.error('Failed to fetch tracking data:', error) - return h.view(`reports-list/${reportName}`, { errorMessage: 'Failed to fetch tracking data' }) - } + handler: async (request, h) => { + const { query } = request + const startDate = + formatDate( + query['start-date-day'], + query['start-date-month'], + query['start-date-year'] + ) || DEFAULT_START_DATE + const endDate = + formatDate( + query['end-date-day'], + query['end-date-month'], + query['end-date-year'] + ) || getCurrentDate() + + try { + const url = `${reportDataUrl}?startDate=${startDate}&endDate=${endDate}` + const response = await api.getTrackingData(url) + const mapper = getDataMapper(reportName) + const selectedData = response.payload[reportDataKey].map(mapper) + + if (selectedData.length === 0) { + return h.view(`reports-list/${reportName}`, { + errors: [{ text: 'No data available for the selected date range' }] + }) } + + const csv = convertToCSV(selectedData) + const filename = `${getBaseFilename( + reportName + )}-from-${startDate}-to-${endDate}.csv` + + return h + .response(csv) + .header('Content-Type', 'text/csv') + .header('Content-Disposition', `attachment; filename=${filename}`) + } catch (error) { + console.error('Failed to fetch tracking data:', error) + return h.view(`reports-list/${reportName}`, { + errorMessage: 'Failed to fetch tracking data' + }) } } - ] -} + } +}) + +const generateRoutes = (reportName, reportDataUrl, reportDataKey) => [ + createGetRoute(reportName), + createDownloadRoute(reportName, reportDataUrl, reportDataKey) +] module.exports = generateRoutes diff --git a/app/routes/authenticate.js b/app/routes/authenticate.js index c4846da..f182c5c 100644 --- a/app/routes/authenticate.js +++ b/app/routes/authenticate.js @@ -1,4 +1,5 @@ const auth = require('../auth') +const HTTP_SERVER_ERROR = 500 module.exports = { method: 'GET', @@ -14,6 +15,6 @@ module.exports = { console.error('Error authenticating:', err) } - return h.view('500').code(500) + return h.view('500').code(HTTP_SERVER_ERROR) } } diff --git a/app/routes/closure.js b/app/routes/closure.js index 1b38762..8df0ab1 100644 --- a/app/routes/closure.js +++ b/app/routes/closure.js @@ -5,79 +5,130 @@ const bulkSchema = require('./schemas/bulk-closure') const { post } = require('../api') const { processClosureData } = require('../closure') -module.exports = [{ - method: 'GET', - path: '/closure/add', - options: { - auth: { scope: [closureAdmin] }, - handler: async (_request, h) => { - return h.view('closure/add') - } - } -}, -{ - method: 'GET', - path: '/closure/bulk', - options: { - auth: { scope: [closureAdmin] }, - handler: async (_request, h) => { - return h.view('closure/bulk') - } - } -}, -{ - method: 'POST', - path: '/closure/add', - options: { - auth: { scope: [closureAdmin] }, - validate: { - payload: schema, - failAction: async (request, h, error) => { - return h.view('closure/add', { errors: error, frn: request.payload.frn, agreement: request.payload.agreement, day: request.payload.day, month: request.payload.month, year: request.payload.year }).code(400).takeover() - } - }, - handler: async (request, h) => { - let day = request.payload.day.toString() - if (day.length !== 2) { - day = `0${request.payload.day}` - } - let month = request.payload.month.toString() - if (month.length !== 2) { - month = `0${request.payload.month}` +const ROUTES = { + BASE: '/closure', + ADD: '/closure/add', + BULK: '/closure/bulk' +} + +const VIEWS = { + ADD: 'closure/add', + BULK: 'closure/bulk' +} + +const HTTP = { + BAD_REQUEST: 400 +} + +const CONFIG = { + MAX_BYTES: 1048576 +} + +module.exports = [ + { + method: 'GET', + path: ROUTES.ADD, + options: { + auth: { scope: [closureAdmin] }, + handler: async (_request, h) => { + return h.view(VIEWS.ADD) } - const date = `${request.payload.year}-${month}-${day}T00:00:00` - await post('/closure/add', { frn: request.payload.frn, agreement: request.payload.agreement, date }, null) - return h.redirect('/closure') } - } -}, -{ - method: 'POST', - path: '/closure/bulk', - handler: async (request, h) => { - const data = fs.readFileSync(request.payload.file.path, 'utf8') - if (!data) { - return h.view('closure/bulk', { errors: { details: [{ message: 'An error occurred whilst reading the file' }] } }).code(400).takeover() + }, + { + method: 'GET', + path: ROUTES.BULK, + options: { + auth: { scope: [closureAdmin] }, + handler: async (_request, h) => { + return h.view(VIEWS.BULK) + } } - const { uploadData, errors } = await processClosureData(data) - if (errors) { - return h.view('closure/bulk', { errors }).code(400).takeover() + }, + { + method: 'POST', + path: ROUTES.ADD, + options: { + auth: { scope: [closureAdmin] }, + validate: { + payload: schema, + failAction: async (request, h, error) => { + return h + .view(VIEWS.ADD, { + errors: error, + frn: request.payload.frn, + agreement: request.payload.agreement, + day: request.payload.day, + month: request.payload.month, + year: request.payload.year + }) + .code(HTTP.BAD_REQUEST) + .takeover() + } + }, + handler: async (request, h) => { + let day = request.payload.day.toString() + if (day.length !== 2) { + day = `0${request.payload.day}` + } + let month = request.payload.month.toString() + if (month.length !== 2) { + month = `0${request.payload.month}` + } + const date = `${request.payload.year}-${month}-${day}T00:00:00` + await post( + ROUTES.ADD, + { + frn: request.payload.frn, + agreement: request.payload.agreement, + date + }, + null + ) + return h.redirect(ROUTES.BASE) + } } - await post('/closure/bulk', { data: uploadData }, null) - return h.redirect('/closure') }, - options: { - auth: { scope: [closureAdmin] }, - validate: { - payload: bulkSchema, - failAction: async (request, h, error) => { - return h.view('closure/bulk', { errors: error }).code(400).takeover() + { + method: 'POST', + path: ROUTES.BULK, + handler: async (request, h) => { + const data = fs.readFileSync(request.payload.file.path, 'utf8') + if (!data) { + return h + .view(VIEWS.BULK, { + errors: { + details: [ + { message: 'An error occurred whilst reading the file' } + ] + } + }) + .code(HTTP.BAD_REQUEST) + .takeover() } + const { uploadData, errors } = await processClosureData(data) + if (errors) { + return h.view(VIEWS.BULK, { errors }).code(HTTP.BAD_REQUEST).takeover() + } + await post(ROUTES.BULK, { data: uploadData }, null) + return h.redirect(ROUTES.BASE) }, - payload: { - output: 'file', - maxBytes: 1048576, - multipart: true + options: { + auth: { scope: [closureAdmin] }, + validate: { + payload: bulkSchema, + failAction: async (request, h, error) => { + return h + .view(VIEWS.BULK, { errors: error }) + .code(HTTP.BAD_REQUEST) + .takeover() + } + }, + payload: { + output: 'file', + maxBytes: CONFIG.MAX_BYTES, + multipart: true + } } } -}] +] diff --git a/app/routes/dev-auth.js b/app/routes/dev-auth.js index fe41229..987c0aa 100644 --- a/app/routes/dev-auth.js +++ b/app/routes/dev-auth.js @@ -1,4 +1,5 @@ const auth = require('../auth') +const HTTP_SERVER_ERROR = 500 module.exports = { method: 'GET', @@ -13,6 +14,6 @@ module.exports = { } catch (err) { console.error('Error authenticating', err) } - return h.view('500').code(500) + return h.view('500').code(HTTP_SERVER_ERROR) } } diff --git a/app/routes/healthy.js b/app/routes/healthy.js index 1093e89..f5ea60f 100644 --- a/app/routes/healthy.js +++ b/app/routes/healthy.js @@ -1,3 +1,5 @@ +const HTTP_OK = 200 + module.exports = { method: 'GET', path: '/healthy', @@ -5,6 +7,6 @@ module.exports = { auth: false }, handler: (_request, h) => { - return h.response('ok').code(200) + return h.response('ok').code(HTTP_OK) } } diff --git a/app/routes/healthz.js b/app/routes/healthz.js index a04243b..5743127 100644 --- a/app/routes/healthz.js +++ b/app/routes/healthz.js @@ -1,3 +1,5 @@ +const HTTP_OK = 200 + module.exports = { method: 'GET', path: '/healthz', @@ -5,6 +7,6 @@ module.exports = { auth: false }, handler: (_request, h) => { - return h.response('ok').code(200) + return h.response('ok').code(HTTP_OK) } } diff --git a/app/routes/holds.js b/app/routes/holds.js index d99d304..959cce9 100644 --- a/app/routes/holds.js +++ b/app/routes/holds.js @@ -7,111 +7,177 @@ const { holdAdmin } = require('../auth/permissions') const { getHolds, getHoldCategories } = require('../holds') const { handleBulkPost } = require('../hold') const searchLabelText = 'Search for a hold by FRN number' +const ROUTES = { + HOLDS: '/payment-holds', + ADD: '/add-payment-hold', + BULK: '/payment-holds/bulk', + REMOVE: '/remove-payment-hold' +} -module.exports = [{ - method: 'GET', - path: '/payment-holds', - options: { - auth: { scope: [holdAdmin] }, - handler: async (request, h) => { - const page = parseInt(request.query.page) || 1 - const perPage = parseInt(request.query.perPage || 100) - const paymentHolds = await getHolds(page, perPage) - return h.view('payment-holds', { paymentHolds, page, perPage, ...new ViewModel(searchLabelText) }) - } - } -}, { - method: 'POST', - path: '/payment-holds', - options: { - auth: { scope: [holdAdmin] }, - validate: { - payload: searchSchema, - failAction: async (request, h, error) => { - const paymentHolds = await getHolds() - return h.view('payment-holds', { paymentHolds, ...new ViewModel(searchLabelText, request.payload.frn, error) }).code(400).takeover() - } - }, - handler: async (request, h) => { - const frn = request.payload.frn - const paymentHolds = await getHolds(undefined, undefined, false) - const filteredPaymentHolds = paymentHolds.filter(x => x.frn === String(frn)) +const VIEWS = { + HOLDS: 'payment-holds', + ADD: 'add-payment-hold', + BULK: 'payment-holds/bulk', + REMOVE: 'remove-payment-hold' +} - if (filteredPaymentHolds.length) { - return h.view('payment-holds', { paymentHolds: filteredPaymentHolds, ...new ViewModel(searchLabelText, frn) }) - } +const HTTP = { + BAD_REQUEST: 400 +} + +const CONFIG = { + MAX_BYTES: 1048576 +} - return h.view('payment-holds', new ViewModel(searchLabelText, frn, { message: 'No holds match the FRN provided.' })).code(400) +module.exports = [ + { + method: 'GET', + path: ROUTES.HOLDS, + options: { + auth: { scope: [holdAdmin] }, + handler: async (request, h) => { + const page = parseInt(request.query.page) || 1 + const perPage = parseInt(request.query.perPage || 100) + const paymentHolds = await getHolds(page, perPage) + return h.view(VIEWS.HOLDS, { + paymentHolds, + page, + perPage, + ...new ViewModel(searchLabelText) + }) + } } - } -}, { - method: 'GET', - path: '/add-payment-hold', - options: { - auth: { scope: [holdAdmin] }, - handler: async (_request, h) => { - const { schemes, paymentHoldCategories } = await getHoldCategories() - return h.view('add-payment-hold', { schemes, paymentHoldCategories }) + }, + { + method: 'POST', + path: ROUTES.HOLDS, + options: { + auth: { scope: [holdAdmin] }, + validate: { + payload: searchSchema, + failAction: async (request, h, error) => { + const paymentHolds = await getHolds() + return h + .view(VIEWS.HOLDS, { + paymentHolds, + ...new ViewModel(searchLabelText, request.payload.frn, error) + }) + .code(HTTP.BAD_REQUEST) + .takeover() + } + }, + handler: async (request, h) => { + const frn = request.payload.frn + const paymentHolds = await getHolds(undefined, undefined, false) + const filteredPaymentHolds = paymentHolds.filter( + x => x.frn === String(frn) + ) + + if (filteredPaymentHolds.length) { + return h.view(VIEWS.HOLDS, { + paymentHolds: filteredPaymentHolds, + ...new ViewModel(searchLabelText, frn) + }) + } + + return h + .view( + VIEWS.HOLDS, + new ViewModel(searchLabelText, frn, { + message: 'No holds match the FRN provided.' + }) + ) + .code(HTTP.BAD_REQUEST) + } } - } -}, -{ - method: 'GET', - path: '/payment-holds/bulk', - options: { - auth: { scope: [holdAdmin] }, - handler: async (_request, h) => { - const { schemes, paymentHoldCategories } = await getHoldCategories() - return h.view('payment-holds/bulk', { schemes, paymentHoldCategories }) + }, + { + method: 'GET', + path: ROUTES.ADD, + options: { + auth: { scope: [holdAdmin] }, + handler: async (_request, h) => { + const { schemes, paymentHoldCategories } = await getHoldCategories() + return h.view(VIEWS.ADD, { schemes, paymentHoldCategories }) + } } - } -}, -{ - method: 'POST', - path: '/add-payment-hold', - options: { - auth: { scope: [holdAdmin] }, - validate: { - payload: schema, - failAction: async (request, h, error) => { + }, + { + method: 'GET', + path: ROUTES.BULK, + options: { + auth: { scope: [holdAdmin] }, + handler: async (_request, h) => { const { schemes, paymentHoldCategories } = await getHoldCategories() - return h.view('add-payment-hold', { schemes, paymentHoldCategories, errors: error, frn: request.payload.frn }).code(400).takeover() + return h.view(VIEWS.BULK, { schemes, paymentHoldCategories }) } - }, - handler: async (request, h) => { - await post('/add-payment-hold', { holdCategoryId: request.payload.holdCategoryId, frn: request.payload.frn }, null) - return h.redirect('/payment-holds') } - } -}, -{ - method: 'POST', - path: '/remove-payment-hold', - options: { - auth: { scope: [holdAdmin] }, - handler: async (request, h) => { - await post('/remove-payment-hold', { holdId: request.payload.holdId }) - return h.redirect('/') + }, + { + method: 'POST', + path: ROUTES.ADD, + options: { + auth: { scope: [holdAdmin] }, + validate: { + payload: schema, + failAction: async (request, h, error) => { + const { schemes, paymentHoldCategories } = await getHoldCategories() + return h + .view(VIEWS.ADD, { + schemes, + paymentHoldCategories, + errors: error, + frn: request.payload.frn + }) + .code(HTTP.BAD_REQUEST) + .takeover() + } + }, + handler: async (request, h) => { + await post( + ROUTES.ADD, + { + holdCategoryId: request.payload.holdCategoryId, + frn: request.payload.frn + }, + null + ) + return h.redirect(ROUTES.HOLDS) + } } - } -}, -{ - method: 'POST', - path: '/payment-holds/bulk', - handler: handleBulkPost, - options: { - auth: { scope: [holdAdmin] }, - validate: { - payload: bulkSchema, - failAction: async (request, h, error) => { - const { schemes, paymentHoldCategories } = await getHoldCategories() - return h.view('payment-holds/bulk', { schemes, paymentHoldCategories, errors: error }).code(400).takeover() + }, + { + method: 'POST', + path: ROUTES.REMOVE, + options: { + auth: { scope: [holdAdmin] }, + handler: async (request, h) => { + await post(VIEWS.REMOVE, { holdId: request.payload.holdId }) + return h.redirect('/') + } + } + }, + { + method: 'POST', + path: ROUTES.BULK, + handler: handleBulkPost, + options: { + auth: { scope: [holdAdmin] }, + validate: { + payload: bulkSchema, + failAction: async (request, h, error) => { + const { schemes, paymentHoldCategories } = await getHoldCategories() + return h + .view(VIEWS.BULK, { schemes, paymentHoldCategories, errors: error }) + .code(HTTP.BAD_REQUEST) + .takeover() + } + }, + payload: { + output: 'file', + maxBytes: CONFIG.MAX_BYTES, + multipart: true } - }, - payload: { - output: 'file', - maxBytes: 1048576, - multipart: true } } -}] +] diff --git a/app/routes/login.js b/app/routes/login.js index 107cd67..4eec8d3 100644 --- a/app/routes/login.js +++ b/app/routes/login.js @@ -1,4 +1,5 @@ const auth = require('../auth') +const HTTP_SERVER_ERROR = 500 module.exports = { method: 'GET', @@ -13,6 +14,6 @@ module.exports = { } catch (err) { console.log('Error authenticating', err) } - return h.view('500').code(500) + return h.view('500').code(HTTP_SERVER_ERROR) } } diff --git a/app/routes/models/monitoring.js b/app/routes/models/monitoring.js index c5adce0..524ab63 100644 --- a/app/routes/models/monitoring.js +++ b/app/routes/models/monitoring.js @@ -1,4 +1,4 @@ -function ViewModel () { +function viewModel () { this.model = { searchByFrn: searchByFrn(), searchByBatch: searchByBatch() @@ -46,4 +46,4 @@ const searchByBatch = () => { } } -module.exports = ViewModel +module.exports = viewModel diff --git a/app/routes/models/search.js b/app/routes/models/search.js index 38565f8..d133cff 100644 --- a/app/routes/models/search.js +++ b/app/routes/models/search.js @@ -1,4 +1,4 @@ -function ViewModel (labelText, value, error) { +function viewModel (labelText, value, error) { this.model = { id: 'user-search', name: 'frn', @@ -23,4 +23,4 @@ function ViewModel (labelText, value, error) { } } -module.exports = ViewModel +module.exports = viewModel diff --git a/app/routes/models/update-scheme.js b/app/routes/models/update-scheme.js index e223b41..5f608da 100644 --- a/app/routes/models/update-scheme.js +++ b/app/routes/models/update-scheme.js @@ -1,5 +1,4 @@ -function ViewModel (value, error) { - // Constructor function to create logic dependent nunjucks page +function viewModel (value, error) { this.model = { id: 'confirm', name: 'confirm', @@ -25,7 +24,6 @@ function ViewModel (value, error) { ] } - // If error is passed to model then this error property is added to the model if (error) { this.model.errorMessage = { text: 'Please select yes or no to update.' @@ -40,4 +38,4 @@ const getText = (name, active) => { return `Would you like to enable ${name}?` } -module.exports = ViewModel +module.exports = viewModel diff --git a/app/routes/monitoring.js b/app/routes/monitoring.js index 2ad885c..04aba2f 100644 --- a/app/routes/monitoring.js +++ b/app/routes/monitoring.js @@ -1,60 +1,70 @@ const config = require('../config') const { schemeAdmin, holdAdmin, dataView } = require('../auth/permissions') -const { getPaymentsByFrn, getPaymentsByCorrelationId, getPaymentsByBatch } = require('../payments') +const { + getPaymentsByFrn, + getPaymentsByCorrelationId, + getPaymentsByBatch +} = require('../payments') const ViewModel = require('./models/monitoring') +const HTTP_NOT_FOUND = 404 -module.exports = [{ - method: 'GET', - path: '/monitoring', - options: { - auth: { scope: [schemeAdmin, holdAdmin, dataView] } - }, - handler: async (request, h) => { - if (!config.useV2Events) { - return h.view('404').code(404) +module.exports = [ + { + method: 'GET', + path: '/monitoring', + options: { + auth: { scope: [schemeAdmin, holdAdmin, dataView] } + }, + handler: async (_request, h) => { + if (!config.useV2Events) { + return h.view('404').code(HTTP_NOT_FOUND) + } + return h.view('monitoring/monitoring', new ViewModel()) } - return h.view('monitoring/monitoring', new ViewModel()) - } -}, { - method: 'GET', - path: '/monitoring/payments/frn', - options: { - auth: { scope: [schemeAdmin, holdAdmin, dataView] } }, - handler: async (request, h) => { - if (!config.useV2Events) { - return h.view('404').code(404) + { + method: 'GET', + path: '/monitoring/payments/frn', + options: { + auth: { scope: [schemeAdmin, holdAdmin, dataView] } + }, + handler: async (request, h) => { + if (!config.useV2Events) { + return h.view('404').code(HTTP_NOT_FOUND) + } + const frn = request.query.frn + const payments = await getPaymentsByFrn(frn) + return h.view('monitoring/frn', { frn, payments }) } - const frn = request.query.frn - const payments = await getPaymentsByFrn(frn) - return h.view('monitoring/frn', { frn, payments }) - } -}, { - method: 'GET', - path: '/monitoring/payments/correlation-id', - options: { - auth: { scope: [schemeAdmin, holdAdmin, dataView] } }, - handler: async (request, h) => { - if (!config.useV2Events) { - return h.view('404').code(404) + { + method: 'GET', + path: '/monitoring/payments/correlation-id', + options: { + auth: { scope: [schemeAdmin, holdAdmin, dataView] } + }, + handler: async (request, h) => { + if (!config.useV2Events) { + return h.view('404').code(HTTP_NOT_FOUND) + } + const correlationId = request.query.correlationId + const events = await getPaymentsByCorrelationId(correlationId) + return h.view('monitoring/correlation-id', { correlationId, events }) } - const correlationId = request.query.correlationId - const events = await getPaymentsByCorrelationId(correlationId) - return h.view('monitoring/correlation-id', { correlationId, events }) - } -}, { - method: 'GET', - path: '/monitoring/batch/name', - options: { - auth: { scope: [schemeAdmin, holdAdmin, dataView] } }, - handler: async (request, h) => { - if (!config.useV2Events) { - return h.view('404').code(404) + { + method: 'GET', + path: '/monitoring/batch/name', + options: { + auth: { scope: [schemeAdmin, holdAdmin, dataView] } + }, + handler: async (request, h) => { + if (!config.useV2Events) { + return h.view('404').code(HTTP_NOT_FOUND) + } + const batch = request.query.batch + const payments = await getPaymentsByBatch(batch) + return h.view('monitoring/batch', { batch, payments }) } - const batch = request.query.batch - const payments = await getPaymentsByBatch(batch) - return h.view('monitoring/batch', { batch, payments }) } -}] +] diff --git a/app/routes/payment-requests.js b/app/routes/payment-requests.js index 7cda58e..415b8c1 100644 --- a/app/routes/payment-requests.js +++ b/app/routes/payment-requests.js @@ -1,46 +1,76 @@ const { post } = require('../api') const schema = require('./schemas/invoice-number') const { schemeAdmin } = require('../auth/permissions') +const ROUTES = { + RESET: '/payment-request/reset', + RESET_SUCCESS: '/payment-request/reset-success' +} +const VIEWS = { + RESET: 'reset-payment-request', + RESET_SUCCESS: 'reset-payment-request-success' +} +const HTTP = { + BAD_REQUEST: 400, + PRECONDITION_FAILED: 412 +} -module.exports = [{ - method: 'GET', - path: '/payment-request/reset', - options: { - auth: { scope: [schemeAdmin] }, - handler: async (_request, h) => { - return h.view('reset-payment-request') - } - } -}, -{ - method: 'POST', - path: '/payment-request/reset', - options: { - auth: { scope: [schemeAdmin] }, - validate: { - payload: schema, - failAction: async (request, h, error) => { - return h.view('reset-payment-request', { error, invoiceNumber: request.payload.invoiceNumber }).code(400).takeover() +module.exports = [ + { + method: 'GET', + path: ROUTES.RESET, + options: { + auth: { scope: [schemeAdmin] }, + handler: async (_request, h) => { + return h.view(VIEWS.RESET) } - }, - handler: async (request, h) => { - const { invoiceNumber } = request.payload - try { - await post('/payment-request/reset', { invoiceNumber }) - return h.redirect(`/payment-request/reset-success?invoiceNumber=${invoiceNumber}`) - } catch (err) { - return h.view('reset-payment-request', { error: err.data?.payload?.message ?? err.message, invoiceNumber }).code(412) + } + }, + { + method: 'POST', + path: ROUTES.RESET, + options: { + auth: { scope: [schemeAdmin] }, + validate: { + payload: schema, + failAction: async (request, h, error) => { + return h + .view(VIEWS.RESET, { + error, + invoiceNumber: request.payload.invoiceNumber + }) + .code(HTTP.BAD_REQUEST) + .takeover() + } + }, + handler: async (request, h) => { + const { invoiceNumber } = request.payload + try { + await post(ROUTES.RESET, { invoiceNumber }) + return h.redirect( + `/payment-request/reset-success?invoiceNumber=${invoiceNumber}` + ) + } catch (err) { + return h + .view(VIEWS.RESET, { + error: err.data?.payload?.message ?? err.message, + invoiceNumber + }) + .code(HTTP.PRECONDITION_FAILED) + .takeover() + } } } - } -}, -{ - method: 'GET', - path: '/payment-request/reset-success', - options: { - auth: { scope: [schemeAdmin] }, - handler: async (request, h) => { - return h.view('reset-payment-request-success', { invoiceNumber: request.query.invoiceNumber }) + }, + { + method: 'GET', + path: ROUTES.RESET_SUCCESS, + options: { + auth: { scope: [schemeAdmin] }, + handler: async (request, h) => { + return h.view(VIEWS.RESET_SUCCESS, { + invoiceNumber: request.query.invoiceNumber + }) + } } } -}] +] diff --git a/app/routes/report.js b/app/routes/report.js index dae0b3b..2f49950 100644 --- a/app/routes/report.js +++ b/app/routes/report.js @@ -4,124 +4,196 @@ const { holdAdmin, schemeAdmin, dataView } = require('../auth/permissions') const formatDate = require('../helpers/format-date') const storageConfig = require('../config/storage') const schema = require('./schemas/report-schema') -const { addDetailsToFilename, createReportHandler, handleCSVResponse, renderErrorPage, getView, handleStreamResponse } = require('../helpers') +const { + addDetailsToFilename, + createReportHandler, + handleCSVResponse, + renderErrorPage, + getView, + handleStreamResponse +} = require('../helpers') const transactionSummaryFields = require('../constants/transaction-summary-fields') const claimLevelReportFields = require('../constants/claim-level-report-fields') const requestEditorReportFields = require('../constants/request-editor-report-fields') +const REPORT_LIST = { + PAYMENT_REQUESTS: '/report-list/payment-requests', + TRANSACTION_SUMMARY: '/report-list/transaction-summary', + CLAIM_LEVEL_REPORT: '/report-list/claim-level-report', + CLAIM_LEVEL_REPORT_DOWNLOAD: '/report-list/claim-level-report/download', + REQUEST_EDITOR_REPORT: '/report-list/request-editor-report', + TRANSACTION_SUMMARY_DOWNLOAD: '/report-list/transaction-summary/download', + SUPPRESSED_PAYMENTS: '/report-list/suppressed-payments', + HOLDS: '/report-list/holds' +} +const REPORTS_VIEWS = { + TRANSACTION_SUMMARY: 'reports-list/transaction-summary', + CLAIM_LEVEL_REPORT: 'reports-list/claim-level-report', + REQUEST_EDITOR_REPORT: 'reports-list/request-editor-report', + HOLD_REPORT_UNAVAILABLE: 'hold-report-unavailable' +} +const REPORTS_HANDLER = { + TRANSACTION_SUMMARY: '/transaction-summary', + CLAIM_LEVEL_REPORT: '/claim-level-report', + REQUEST_EDITOR_REPORT: '/request-editor-report' +} const authOptions = { scope: [schemeAdmin, holdAdmin, dataView] } const getTransactionSummaryHandler = createReportHandler( - '/transaction-summary', + REPORTS_HANDLER.TRANSACTION_SUMMARY, transactionSummaryFields, - (schemeId, year, revenueOrCapital, prn, frn) => addDetailsToFilename(storageConfig.summaryReportName, schemeId, year, prn, revenueOrCapital, frn), - 'reports-list/transaction-summary' + (schemeId, year, revenueOrCapital, prn, frn) => + addDetailsToFilename( + storageConfig.summaryReportName, + schemeId, + year, + prn, + revenueOrCapital, + frn + ), + REPORTS_VIEWS.TRANSACTION_SUMMARY ) const getClaimLevelReportHandler = createReportHandler( - '/claim-level-report', + REPORTS_HANDLER.CLAIM_LEVEL_REPORT, claimLevelReportFields, - (schemeId, year, revenueOrCapital, prn, frn) => addDetailsToFilename(storageConfig.claimLevelReportName, schemeId, year, prn, revenueOrCapital, frn), - 'reports-list/claim-level-report' + (schemeId, year, revenueOrCapital, prn, frn) => + addDetailsToFilename( + storageConfig.claimLevelReportName, + schemeId, + year, + prn, + revenueOrCapital, + frn + ), + REPORTS_VIEWS.CLAIM_LEVEL_REPORT ) const getRequestEditorReportHandler = createReportHandler( - '/request-editor-report', + REPORTS_HANDLER.REQUEST_EDITOR_REPORT, requestEditorReportFields, () => storageConfig.requestEditorReportName, - 'payment-report-unavailable' + REPORTS_VIEWS.REQUEST_EDITOR_REPORT ) -module.exports = [{ - method: 'GET', - path: '/report-list/payment-requests', - options: { - auth: authOptions, - handler: async (_request, h) => handleStreamResponse(getMIReport, storageConfig.miReportName, h) - } -}, { - method: 'GET', - path: '/report-list/transaction-summary', - options: { - auth: authOptions, - handler: async (request, h) => { - return getView('reports-list/transaction-summary', h) +module.exports = [ + { + method: 'GET', + path: REPORT_LIST.PAYMENT_REQUESTS, + options: { + auth: authOptions, + handler: async (_request, h) => + handleStreamResponse(getMIReport, storageConfig.miReportName, h) } - } -}, { - method: 'GET', - path: '/report-list/transaction-summary/download', - options: { - auth: authOptions, - validate: { - query: schema, - failAction: async (request, h, err) => { - return renderErrorPage('reports-list/transaction-summary', request, h, err) + }, + { + method: 'GET', + path: REPORT_LIST.TRANSACTION_SUMMARY, + options: { + auth: authOptions, + handler: async (_request, h) => { + return getView(REPORTS_VIEWS.TRANSACTION_SUMMARY, h) } - }, - handler: getTransactionSummaryHandler - } -}, { - method: 'GET', - path: '/report-list/request-editor-report', - options: { - auth: authOptions, - handler: getRequestEditorReportHandler - } -}, { - method: 'GET', - path: '/report-list/claim-level-report', - options: { - auth: authOptions, - handler: async (request, h) => { - return getView('reports-list/claim-level-report', h) } - } -}, { - method: 'GET', - path: '/report-list/claim-level-report/download', - options: { - auth: authOptions, - validate: { - query: schema, - failAction: async (request, h, err) => { - return renderErrorPage('reports-list/claim-level-report', request, h, err) + }, + { + method: 'GET', + path: REPORT_LIST.TRANSACTION_SUMMARY_DOWNLOAD, + options: { + auth: authOptions, + validate: { + query: schema, + failAction: async (request, h, err) => { + return renderErrorPage( + REPORTS_VIEWS.TRANSACTION_SUMMARY, + request, + h, + err + ) + } + }, + handler: getTransactionSummaryHandler + } + }, + { + method: 'GET', + path: REPORT_LIST.REQUEST_EDITOR_REPORT, + options: { + auth: authOptions, + handler: getRequestEditorReportHandler + } + }, + { + method: 'GET', + path: REPORT_LIST.CLAIM_LEVEL_REPORT, + options: { + auth: authOptions, + handler: async (_request, h) => { + return getView(REPORTS_VIEWS.CLAIM_LEVEL_REPORT, h) } - }, - handler: getClaimLevelReportHandler - } -}, -{ - method: 'GET', - path: '/report-list/suppressed-payments', - options: { - auth: authOptions, - handler: async (_request, h) => handleStreamResponse(getSuppressedReport, storageConfig.suppressedReportName, h) - } -}, -{ - method: 'GET', - path: '/report-list/holds', - options: { - auth: authOptions, - handler: async (request, h) => { - try { - const paymentHolds = await getHolds(undefined, undefined, false) - if (paymentHolds) { - const paymentHoldsData = paymentHolds.map(hold => { - return { - frn: hold.frn, - scheme: hold.holdCategorySchemeName, - marketingYear: hold.marketingYear ?? 'All', - holdCategory: hold.holdCategoryName, - dateAdded: formatDate(hold.dateTimeAdded) - } - }) - return handleCSVResponse(paymentHoldsData, storageConfig.holdReportName)(h) + } + }, + { + method: 'GET', + path: REPORT_LIST.CLAIM_LEVEL_REPORT_DOWNLOAD, + options: { + auth: authOptions, + validate: { + query: schema, + failAction: async (request, h, err) => { + return renderErrorPage( + REPORTS_VIEWS.CLAIM_LEVEL_REPORT, + request, + h, + err + ) + } + }, + handler: getClaimLevelReportHandler + } + }, + { + method: 'GET', + path: REPORT_LIST.SUPPRESSED_PAYMENTS, + options: { + auth: authOptions, + handler: async (_request, h) => + handleStreamResponse( + getSuppressedReport, + storageConfig.suppressedReportName, + h + ) + } + }, + { + method: 'GET', + path: REPORT_LIST.HOLDS, + options: { + auth: authOptions, + handler: async (_request, h) => { + try { + const paymentHolds = await getHolds(undefined, undefined, false) + + if (!paymentHolds) { + return h.view(REPORTS_VIEWS.HOLD_REPORT_UNAVAILABLE) + } + + const paymentHoldsData = paymentHolds.map(hold => ({ + frn: hold.frn, + scheme: hold.holdCategorySchemeName, + marketingYear: hold.marketingYear ?? 'All', + holdCategory: hold.holdCategoryName, + dateAdded: formatDate(hold.dateTimeAdded) + })) + + return handleCSVResponse( + paymentHoldsData, + storageConfig.holdReportName + )(h) + } catch { + return h.view(REPORTS_VIEWS.HOLD_REPORT_UNAVAILABLE) } - } catch { - return h.view('hold-report-unavailable') } } } -}] +] diff --git a/app/routes/schemas/ap-listing-schema.js b/app/routes/schemas/ap-listing-schema.js index 40f1bc5..e3479e1 100644 --- a/app/routes/schemas/ap-listing-schema.js +++ b/app/routes/schemas/ap-listing-schema.js @@ -1,74 +1,102 @@ const Joi = require('joi') -const getSchema = () => { - const yearNow = new Date().getFullYear() - const datePartDaySchema = Joi.number().integer().min(1).max(31).allow('').optional() - const datePartMonthSchema = Joi.number().integer().min(1).max(12).allow('').optional() - const datePartYearSchema = Joi.number().integer().min(2015).max(yearNow).allow('').optional() +const minDate = 1 +const maxDateDay = 31 +const maxDateMonth = 12 +const minYear = 2015 - return Joi.object({ - 'start-date-day': datePartDaySchema.messages({ - 'number.base': 'Start date day must be a number', - 'number.integer': 'Start date day must be an integer', - 'number.min': 'Start date day cannot be less than 1', - 'number.max': 'Start date day cannot be more than 31' - }), - 'start-date-month': datePartMonthSchema.messages({ - 'number.base': 'Start date month must be a number', - 'number.integer': 'Start date month must be an integer', - 'number.min': 'Start date month cannot be less than 1', - 'number.max': 'Start date month cannot be more than 12' - }), - 'start-date-year': datePartYearSchema.messages({ - 'number.base': 'Start date year must be a number', - 'number.integer': 'Start date year must be an integer', - 'number.min': 'Start date year cannot be less than 2015', - 'number.max': 'Start date year cannot be more than current year' - }), - 'end-date-day': datePartDaySchema.messages({ - 'number.base': 'End date day must be a number', - 'number.integer': 'End date day must be an integer', - 'number.min': 'End date day cannot be less than 1', - 'number.max': 'End date day cannot be more than 31' - }), - 'end-date-month': datePartMonthSchema.messages({ - 'number.base': 'End date month must be a number', - 'number.integer': 'End date month must be an integer', - 'number.min': 'End date month cannot be less than 1', - 'number.max': 'End date month cannot be more than 12' - }), - 'end-date-year': datePartYearSchema.messages({ - 'number.base': 'End date year must be a number', - 'number.integer': 'End date year must be an integer', - 'number.min': 'End date year cannot be less than current year', - 'number.max': 'End date year cannot be more than current year' - }) - }).options({ abortEarly: false }) - .custom((value, helpers) => { - const startDateParts = ['start-date-day', 'start-date-month', 'start-date-year'] - const endDateParts = ['end-date-day', 'end-date-month', 'end-date-year'] +const createDatePartSchema = (min, max) => { + return Joi.number().integer().min(min).max(max).allow('').optional() +} + +const validateStartDate = (startDay, startMonth, startYear, helpers) => { + if ( + (startDay || startMonth || startYear) && + (!startDay || !startMonth || !startYear) + ) { + return helpers.message('Start date must include day, month, and year') + } + return null +} + +const validateEndDate = (endDay, endMonth, endYear, helpers) => { + if ((endDay || endMonth || endYear) && (!endDay || !endMonth || !endYear)) { + return helpers.message('End date must include day, month, and year') + } + return null +} - const startDateProvided = startDateParts.every(part => value[part] !== '') - const endDateProvided = endDateParts.every(part => value[part] !== '') +const validateDateRange = ( + startDay, + startMonth, + startYear, + endDay, + endMonth, + endYear, + helpers +) => { + if (startYear && endYear) { + const startDate = new Date(startYear, startMonth - 1, startDay) + const endDate = new Date(endYear, endMonth - 1, endDay) + if (endDate < startDate) { + return helpers.message('End date cannot be less than start date') + } + } + return null +} + +const validateCompleteDate = (value, helpers) => { + const { + 'start-date-day': startDay, + 'start-date-month': startMonth, + 'start-date-year': startYear, + 'end-date-day': endDay, + 'end-date-month': endMonth, + 'end-date-year': endYear + } = value - if (!startDateProvided && startDateParts.some(part => value[part] !== '')) { - return helpers.message('Start date must include day, month, and year') - } + const startDateError = validateStartDate( + startDay, + startMonth, + startYear, + helpers + ) + if (startDateError) { + return startDateError + } - if (!endDateProvided && endDateParts.some(part => value[part] !== '')) { - return helpers.message('End date must include day, month, and year') - } + const endDateError = validateEndDate(endDay, endMonth, endYear, helpers) + if (endDateError) { + return endDateError + } - if (startDateProvided && endDateProvided) { - const startDate = new Date(value['start-date-year'], value['start-date-month'] - 1, value['start-date-day']) - const endDate = new Date(value['end-date-year'], value['end-date-month'] - 1, value['end-date-day']) + const rangeError = validateDateRange( + startDay, + startMonth, + startYear, + endDay, + endMonth, + endYear, + helpers + ) + if (rangeError) { + return rangeError + } + + return value +} - if (endDate < startDate) { - return helpers.message('End date cannot be less than start date') - } - } +const getSchema = () => { + const yearNow = new Date().getFullYear() - return value - }) + return Joi.object({ + 'start-date-day': createDatePartSchema(minDate, maxDateDay), + 'start-date-month': createDatePartSchema(minDate, maxDateMonth), + 'start-date-year': createDatePartSchema(minYear, yearNow), + 'end-date-day': createDatePartSchema(minDate, maxDateDay), + 'end-date-month': createDatePartSchema(minDate, maxDateMonth), + 'end-date-year': createDatePartSchema(minYear, yearNow) + }).custom(validateCompleteDate) } + module.exports = getSchema() diff --git a/app/routes/schemas/closure.js b/app/routes/schemas/closure.js index c2139b4..3fa3446 100644 --- a/app/routes/schemas/closure.js +++ b/app/routes/schemas/closure.js @@ -1,24 +1,65 @@ const Joi = require('joi') +const minFRN = 1000000000 +const maxFRN = 9999999999 +const maxAgreement = 50 +const minDayMonth = 1 +const maxDay = 31 +const maxMonth = 12 +const minYear = 2023 +const maxYear = 2099 module.exports = Joi.object({ - frn: Joi.number().integer().min(1000000000).max(9999999999).required().error(errors => { - errors.forEach(err => { err.message = 'Enter a 10-digit FRN' }) - return errors - }), - agreement: Joi.string().required().max(50).error(errors => { - errors.forEach(err => { err.message = 'Enter a valid agreement number' }) - return errors - }), - day: Joi.number().integer().min(1).max(31).required().error(errors => { - errors.forEach(err => { err.message = 'Enter a valid day' }) - return errors - }), - month: Joi.number().integer().min(1).max(12).required().error(errors => { - errors.forEach(err => { err.message = 'Enter a valid month' }) - return errors - }), - year: Joi.number().integer().min(2023).max(2099).required().error(errors => { - errors.forEach(err => { err.message = 'Enter a valid year' }) - return errors - }) + frn: Joi.number() + .integer() + .min(minFRN) + .max(maxFRN) + .required() + .error(errors => { + errors.forEach(err => { + err.message = 'Enter a 10-digit FRN' + }) + return errors + }), + agreement: Joi.string() + .required() + .max(maxAgreement) + .error(errors => { + errors.forEach(err => { + err.message = 'Enter a valid agreement number' + }) + return errors + }), + day: Joi.number() + .integer() + .min(minDayMonth) + .max(maxDay) + .required() + .error(errors => { + errors.forEach(err => { + err.message = 'Enter a valid day' + }) + return errors + }), + month: Joi.number() + .integer() + .min(minDayMonth) + .max(maxMonth) + .required() + .error(errors => { + errors.forEach(err => { + err.message = 'Enter a valid month' + }) + return errors + }), + year: Joi.number() + .integer() + .min(minYear) + .max(maxYear) + .required() + .error(errors => { + errors.forEach(err => { + err.message = 'Enter a valid year' + }) + return errors + }) }) diff --git a/app/routes/schemas/frn.js b/app/routes/schemas/frn.js index 878947e..0af3aa8 100644 --- a/app/routes/schemas/frn.js +++ b/app/routes/schemas/frn.js @@ -1,9 +1,17 @@ const Joi = require('joi') +const frnGreaterThan = 999999999 +const frnLessThan = 10000000000 module.exports = { - frn: Joi.number().integer().greater(999999999).less(10000000000).required() + frn: Joi.number() + .integer() + .greater(frnGreaterThan) + .less(frnLessThan) + .required() .error(errors => { - errors.forEach(err => { err.message = 'The FRN must be 10 digits' }) + errors.forEach(err => { + err.message = 'The FRN must be 10 digits' + }) return errors }) } diff --git a/app/routes/schemas/hold-search.js b/app/routes/schemas/hold-search.js index dcc5abd..34996de 100644 --- a/app/routes/schemas/hold-search.js +++ b/app/routes/schemas/hold-search.js @@ -1,8 +1,17 @@ const Joi = require('joi') +const minFRN = 1000000000 +const maxFRN = 9999999999 module.exports = Joi.object({ - frn: Joi.number().integer().min(1000000000).max(9999999999).required().error(errors => { - errors.forEach(err => { err.message = 'Enter a 10-digit FRN' }) - return errors - }) + frn: Joi.number() + .integer() + .min(minFRN) + .max(maxFRN) + .required() + .error(errors => { + errors.forEach(err => { + err.message = 'Enter a 10-digit FRN' + }) + return errors + }) }) diff --git a/app/routes/schemas/hold.js b/app/routes/schemas/hold.js index a900b06..6b69dc4 100644 --- a/app/routes/schemas/hold.js +++ b/app/routes/schemas/hold.js @@ -1,12 +1,26 @@ const Joi = require('joi') +const minFRN = 1000000000 +const maxFRN = 9999999999 module.exports = Joi.object({ - frn: Joi.number().integer().min(1000000000).max(9999999999).required().error(errors => { - errors.forEach(err => { err.message = 'Enter a 10-digit FRN' }) - return errors - }), - holdCategoryId: Joi.number().integer().required().error(errors => { - errors.forEach(err => { err.message = 'Category is required' }) - return errors - }) + frn: Joi.number() + .integer() + .min(minFRN) + .max(maxFRN) + .required() + .error(errors => { + errors.forEach(err => { + err.message = 'Enter a 10-digit FRN' + }) + return errors + }), + holdCategoryId: Joi.number() + .integer() + .required() + .error(errors => { + errors.forEach(err => { + err.message = 'Category is required' + }) + return errors + }) }) diff --git a/app/routes/schemas/parsed-closure.js b/app/routes/schemas/parsed-closure.js index 7164271..b67c12e 100644 --- a/app/routes/schemas/parsed-closure.js +++ b/app/routes/schemas/parsed-closure.js @@ -1,16 +1,36 @@ const Joi = require('joi').extend(require('@joi/date')) +const minFRN = 1000000000 +const maxFRN = 9999999999 +const maxAgreementNumberLength = 50 module.exports = Joi.object({ - frn: Joi.number().integer().min(1000000000).max(9999999999).required().error(errors => { - errors.forEach(err => { err.message = 'Enter a 10-digit FRN' }) - return errors - }), - agreementNumber: Joi.string().required().max(50).error(errors => { - errors.forEach(err => { err.message = 'Enter a valid agreement number' }) - return errors - }), - closureDate: Joi.date().format('YYYY-MM-DD').required().error(errors => { - errors.forEach(err => { err.message = 'Enter a valid date' }) - return errors - }) + frn: Joi.number() + .integer() + .min(minFRN) + .max(maxFRN) + .required() + .error(errors => { + errors.forEach(err => { + err.message = 'Enter a 10-digit FRN' + }) + return errors + }), + agreementNumber: Joi.string() + .required() + .max(maxAgreementNumberLength) + .error(errors => { + errors.forEach(err => { + err.message = 'Enter a valid agreement number' + }) + return errors + }), + closureDate: Joi.date() + .format('YYYY-MM-DD') + .required() + .error(errors => { + errors.forEach(err => { + err.message = 'Enter a valid date' + }) + return errors + }) }) diff --git a/app/routes/schemas/parsed-hold.js b/app/routes/schemas/parsed-hold.js index 5324865..c264bc8 100644 --- a/app/routes/schemas/parsed-hold.js +++ b/app/routes/schemas/parsed-hold.js @@ -1,8 +1,17 @@ const Joi = require('joi').extend(require('@joi/date')) +const minFRN = 1000000000 +const maxFRN = 9999999999 module.exports = Joi.object({ - frn: Joi.number().integer().min(1000000000).max(9999999999).required().error(errors => { - errors.forEach(err => { err.message = 'A provided FRN is not in the required format' }) - return errors - }) + frn: Joi.number() + .integer() + .min(minFRN) + .max(maxFRN) + .required() + .error(errors => { + errors.forEach(err => { + err.message = 'A provided FRN is not in the required format' + }) + return errors + }) }) diff --git a/app/routes/schemas/report-schema.js b/app/routes/schemas/report-schema.js index 7f9126e..c966242 100644 --- a/app/routes/schemas/report-schema.js +++ b/app/routes/schemas/report-schema.js @@ -1,56 +1,81 @@ const Joi = require('joi') const { CS, BPS } = require('../../constants/schemes') +const frnGreaterThan = 999999999 +const frnLessThan = 10000000000 +const yearGreaterThan = 1993 +const yearLessThan = 2099 module.exports = Joi.object({ - frn: Joi.number().integer().greater(999999999).less(10000000000).empty('').optional() + frn: Joi.number() + .integer() + .greater(frnGreaterThan) + .less(frnLessThan) + .empty('') + .optional() .error(errors => { - errors.forEach(err => { err.message = 'The FRN, if present, must be 10 digits' }) + errors.forEach(err => { + err.message = 'The FRN, if present, must be 10 digits' + }) return errors }), - year: Joi.number().integer().greater(1993).less(2099) + year: Joi.number() + .integer() + .greater(yearGreaterThan) + .less(yearLessThan) .when('schemeId', { is: Joi.number().integer().valid(CS), then: Joi.optional().allow(''), - otherwise: Joi.required() - .error(errors => { - errors.forEach(err => { err.message = 'A valid year must be provided' }) - return errors + otherwise: Joi.required().error(errors => { + errors.forEach(err => { + err.message = 'A valid year must be provided' }) + return errors + }) }), - prn: Joi.number().integer() + prn: Joi.number() + .integer() .when('schemeId', { is: Joi.number().integer().valid(BPS), - then: Joi.required() - .error(errors => { - errors.forEach(err => { err.message = 'Provide a payment request number' }) - return errors - }), - otherwise: Joi.allow('') - .error(errors => { - return errors + then: Joi.required().error(errors => { + errors.forEach(err => { + err.message = 'Provide a payment request number' }) + return errors + }), + otherwise: Joi.allow('').error(errors => { + return errors + }) }), - revenueOrCapital: Joi.string().allow('', 'Revenue', 'Capital') + revenueOrCapital: Joi.string() + .allow('', 'Revenue', 'Capital') .when('schemeId', { is: Joi.number().integer().valid(CS), - then: Joi.required().valid('Revenue', 'Capital') + then: Joi.required() + .valid('Revenue', 'Capital') .error(errors => { - errors.forEach(err => { err.message = 'Select Revenue or Capital' }) + errors.forEach(err => { + err.message = 'Select Revenue or Capital' + }) return errors }), - otherwise: Joi.valid('') - .error(errors => { - errors.forEach(err => { err.message = 'Revenue/Capital should not be selected for this scheme' }) - return errors + otherwise: Joi.valid('').error(errors => { + errors.forEach(err => { + err.message = 'Revenue/Capital should not be selected for this scheme' }) + return errors + }) }), - schemeId: Joi.number().integer().required() + schemeId: Joi.number() + .integer() + .required() .error(errors => { - errors.forEach(err => { err.message = 'A scheme must be selected' }) + errors.forEach(err => { + err.message = 'A scheme must be selected' + }) return errors }) }) diff --git a/app/routes/schemes.js b/app/routes/schemes.js index 563d0fc..7e41e87 100644 --- a/app/routes/schemes.js +++ b/app/routes/schemes.js @@ -2,74 +2,91 @@ const { get, post } = require('../api') const Joi = require('joi') const ViewModel = require('./models/update-scheme') const { schemeAdmin } = require('../auth/permissions') +const PAYMENT_SCHEMES = '/payment-schemes' +const UPDATE_PAYMENT_SCHEME = '/update-payment-scheme' +const HTTP_BAD_REQUEST = 400 -module.exports = [{ - method: 'GET', - path: '/payment-schemes', - options: { - auth: { scope: [schemeAdmin] }, - handler: async (_request, h) => { - const schemes = await get('/payment-schemes') - const schemesPayload = schemes.payload.paymentSchemes - for (const scheme of schemesPayload) { - if (scheme.name === 'SFI') { - scheme.name = 'SFI22' +module.exports = [ + { + method: 'GET', + path: PAYMENT_SCHEMES, + options: { + auth: { scope: [schemeAdmin] }, + handler: async (_request, h) => { + const schemes = await get(PAYMENT_SCHEMES) + const schemesPayload = schemes.payload.paymentSchemes + for (const scheme of schemesPayload) { + if (scheme.name === 'SFI') { + scheme.name = 'SFI22' + } } + return h.view('payment-schemes', { schemes: schemesPayload }) } - return h.view('payment-schemes', { schemes: schemesPayload }) } - } -}, -{ - method: 'POST', - path: '/payment-schemes', - options: { - auth: { scope: [schemeAdmin] }, - handler: async (request, h) => { - const active = request.payload.active - const schemeId = request.payload.schemeId - const name = request.payload.name - return h.redirect(`/update-payment-scheme?schemeId=${schemeId}&active=${active}&name=${name}`) - } - } -}, { - method: 'GET', - path: '/update-payment-scheme', - options: { - auth: { scope: [schemeAdmin] }, - validate: { - query: Joi.object({ - schemeId: Joi.number().required(), - name: Joi.string().required(), - active: Joi.boolean().required() - }) - }, - handler: async (request, h) => { - return h.view('update-payment-scheme', new ViewModel(request.query)) + }, + { + method: 'POST', + path: PAYMENT_SCHEMES, + options: { + auth: { scope: [schemeAdmin] }, + handler: async (request, h) => { + const active = request.payload.active + const schemeId = request.payload.schemeId + const name = request.payload.name + return h.redirect( + `/update-payment-scheme?schemeId=${schemeId}&active=${active}&name=${name}` + ) + } } - } -}, -{ - method: 'POST', - path: '/update-payment-scheme', - options: { - auth: { scope: [schemeAdmin] }, - validate: { - payload: Joi.object({ - confirm: Joi.boolean().required(), - schemeId: Joi.number().required(), - name: Joi.string().required(), - active: Joi.boolean().required() - }), - failAction: async (request, h, error) => { - return h.view('update-payment-scheme', new ViewModel(request.payload, error)).code(400).takeover() + }, + { + method: 'GET', + path: UPDATE_PAYMENT_SCHEME, + options: { + auth: { scope: [schemeAdmin] }, + validate: { + query: Joi.object({ + schemeId: Joi.number().required(), + name: Joi.string().required(), + active: Joi.boolean().required() + }) + }, + handler: async (request, h) => { + return h.view('update-payment-scheme', new ViewModel(request.query)) } - }, - handler: async (request, h) => { - if (request.payload.confirm) { - await post('/change-payment-status', { schemeId: request.payload.schemeId, active: !request.payload.active }) + } + }, + { + method: 'POST', + path: UPDATE_PAYMENT_SCHEME, + options: { + auth: { scope: [schemeAdmin] }, + validate: { + payload: Joi.object({ + confirm: Joi.boolean().required(), + schemeId: Joi.number().required(), + name: Joi.string().required(), + active: Joi.boolean().required() + }), + failAction: async (request, h, error) => { + return h + .view( + 'update-payment-scheme', + new ViewModel(request.payload, error) + ) + .code(HTTP_BAD_REQUEST) + .takeover() + } + }, + handler: async (request, h) => { + if (request.payload.confirm) { + await post('/change-payment-status', { + schemeId: request.payload.schemeId, + active: !request.payload.active + }) + } + return h.redirect(PAYMENT_SCHEMES) } - return h.redirect('/payment-schemes') } } -}] +] diff --git a/app/routes/view-processed-payment-requests.js b/app/routes/view-processed-payment-requests.js index 73df106..733496b 100644 --- a/app/routes/view-processed-payment-requests.js +++ b/app/routes/view-processed-payment-requests.js @@ -2,36 +2,50 @@ const config = require('../config') const { schemeAdmin, holdAdmin, dataView } = require('../auth/permissions') const { getPaymentsByScheme } = require('../payments') const { get } = require('../api') +const HTTP_PRECONDITION_FAILED = 412 +const HTTP_NOT_FOUND = 404 -module.exports = [{ - method: 'GET', - path: '/monitoring/schemes', - options: { - auth: { scope: [schemeAdmin, holdAdmin, dataView] } - }, - handler: async (request, h) => { - if (!config.useV2Events) { - return h.view('404').code(404) +module.exports = [ + { + method: 'GET', + path: '/monitoring/schemes', + options: { + auth: { scope: [schemeAdmin, holdAdmin, dataView] } + }, + handler: async (_request, h) => { + if (!config.useV2Events) { + return h.view('404').code(HTTP_NOT_FOUND) + } + const schemes = await get('/payment-schemes') + return h.view('monitoring/schemes', { + data: schemes?.payload?.paymentSchemes + }) } - const schemes = await get('/payment-schemes') - return h.view('monitoring/schemes', { data: schemes?.payload?.paymentSchemes }) - } -}, { - method: 'GET', - path: '/monitoring/view-processed-payment-requests', - options: { - auth: { scope: [schemeAdmin, holdAdmin, dataView] } }, - handler: async (request, h) => { - if (!config.useV2Events) { - return h.view('404').code(404) - } - const { schemeId } = request.query - try { - const processedPaymentRequests = await getPaymentsByScheme(schemeId) - return h.view('monitoring/view-processed-payment-requests', { data: processedPaymentRequests }) - } catch (err) { - return h.view('monitoring/schemes', { error: err.data?.payload?.message ?? err.message, schemeId }).code(412) + { + method: 'GET', + path: '/monitoring/view-processed-payment-requests', + options: { + auth: { scope: [schemeAdmin, holdAdmin, dataView] } + }, + handler: async (request, h) => { + if (!config.useV2Events) { + return h.view('404').code(HTTP_NOT_FOUND) + } + const { schemeId } = request.query + try { + const processedPaymentRequests = await getPaymentsByScheme(schemeId) + return h.view('monitoring/view-processed-payment-requests', { + data: processedPaymentRequests + }) + } catch (err) { + return h + .view('monitoring/schemes', { + error: err.data?.payload?.message ?? err.message, + schemeId + }) + .code(HTTP_PRECONDITION_FAILED) + } } } -}] +] diff --git a/helm/ffc-pay-web/templates/_container.yaml b/helm/ffc-pay-web/templates/_container.yaml index a5b7292..15803a1 100644 --- a/helm/ffc-pay-web/templates/_container.yaml +++ b/helm/ffc-pay-web/templates/_container.yaml @@ -1,6 +1,6 @@ {{- define "ffc-pay-web.container" -}} -livenessProbe: {{ include "ffc-helm-library.http-get-probe" (list . .Values.livenessProbe) | nindent 4}} -readinessProbe: {{ include "ffc-helm-library.http-get-probe" (list . .Values.readinessProbe) | nindent 4}} +livenessProbe: {{ include "ffc-helm-library.http-get-probe" (list . .Values.livenessProbe) | nindent 4 }} +readinessProbe: {{ include "ffc-helm-library.http-get-probe" (list . .Values.readinessProbe) | nindent 4 }} ports: - containerPort: {{ .Values.container.port }} {{- end -}} diff --git a/jest.config.js b/jest.config.js index 08e796e..0246197 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,25 +1,18 @@ module.exports = { collectCoverage: true, - collectCoverageFrom: [ - '**/*.js', - '!**/*.test.js' - ], + collectCoverageFrom: ['**/*.js', '!**/*.test.js'], coverageDirectory: 'test-output', - coverageReporters: [ - 'text-summary', - 'lcov' - ], + coverageReporters: ['text-summary', 'lcov'], coveragePathIgnorePatterns: [ '/node_modules/', '/test-output/', '/test/', '/jest.config.js', '/webpack.config.js', - '/app/frontend/' - ], - modulePathIgnorePatterns: [ - 'node_modules' + '/app/frontend/', + '/app/dist/' ], + modulePathIgnorePatterns: ['node_modules'], reporters: [ 'default', [ diff --git a/package-lock.json b/package-lock.json index 0aae01e..4aacb3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ffc-pay-web", - "version": "1.20.20", + "version": "1.20.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ffc-pay-web", - "version": "1.20.20", + "version": "1.20.21", "license": "OGL-UK-3.0", "dependencies": { "@azure/identity": "4.3.0", diff --git a/package.json b/package.json index dae4fd8..f445f0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ffc-pay-web", - "version": "1.20.20", + "version": "1.20.21", "description": "FFC payment management service", "homepage": "https://github.com/DEFRA/ffc-pay-web", "main": "app/index.js", diff --git a/test/integration/narrow/routes/report-listing.test.js b/test/integration/narrow/routes/report-listing.test.js index 0e2ab32..b1104ba 100644 --- a/test/integration/narrow/routes/report-listing.test.js +++ b/test/integration/narrow/routes/report-listing.test.js @@ -116,14 +116,16 @@ describe('AP Listing Report tests', () => { test('GET /report-list/ap-ar-listing/download route sets endDate to current date if only startDate is provided', async () => { const now = new Date() - const expectedEndDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` + const expectedEndDate = `${now.getFullYear()}-${String( + now.getMonth() + 1 + ).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` const options = { method: 'GET', url: '/report-list/ap-ar-listing/download?start-date-day=01&start-date-month=01&start-date-year=2022', auth } - getTrackingData.mockImplementation((url) => { + getTrackingData.mockImplementation(url => { const urlObj = new URL(url, 'http://localhost') const requestEndDate = urlObj.searchParams.get('endDate') expect(requestEndDate).toBe(expectedEndDate) @@ -161,7 +163,7 @@ describe('AP Listing Report tests', () => { url: '/report-list/ap-ar-listing/download', auth } - getTrackingData.mockImplementation((url) => { + getTrackingData.mockImplementation(url => { expect(url).toContain('startDate=2015-01-01') return Promise.resolve({ payload: { @@ -186,9 +188,16 @@ describe('AP Listing Report tests', () => { const h = { view: jest.fn() } - const handler = generateRoutes('ap-ar-listing', '/ap-report-data', 'apReportData')[1].options.handler + const handler = generateRoutes( + 'ap-ar-listing', + '/ap-report-data', + 'apReportData' + )[1].options.handler await handler(options, h) - expect(h.view).toHaveBeenCalledWith('reports-list/ap-ar-listing', expect.anything()) + expect(h.view).toHaveBeenCalledWith( + 'reports-list/ap-ar-listing', + expect.anything() + ) }) test('filename starts with correct reportName', async () => { const options = { @@ -202,7 +211,9 @@ describe('AP Listing Report tests', () => { } }) const response = await server.inject(options) - expect(response.headers['content-disposition']).toContain(config.apListingReportName.slice(0, -4)) + expect(response.headers['content-disposition']).toContain( + config.apListingReportName.slice(0, -4) + ) }) test('CSV content is correct when reportName is ar-listing', async () => { const options = { @@ -216,7 +227,9 @@ describe('AP Listing Report tests', () => { } }) const response = await server.inject(options) - expect(response.headers['content-disposition']).toContain(config.arListingReportName.slice(0, -4)) + expect(response.headers['content-disposition']).toContain( + config.arListingReportName.slice(0, -4) + ) }) test('GET /report-list/ap-ar-listing/download returns 400 for invalid request', async () => { @@ -229,8 +242,6 @@ describe('AP Listing Report tests', () => { expect(response.statusCode).toBe(400) }) test('GET /report-list/invalid-report-name returns 404 for invalid report name and query parameters', async () => { - const routes = generateRoutes('invalid-report-name', '/invalid-report-data', 'invalidReportData') - server.route(routes) const options = { method: 'GET', url: '/report-list/invalid-report-name/download?start-date-day=32', diff --git a/test/integration/narrow/routes/report.test.js b/test/integration/narrow/routes/report.test.js index 9175b06..77843da 100644 --- a/test/integration/narrow/routes/report.test.js +++ b/test/integration/narrow/routes/report.test.js @@ -1,4 +1,8 @@ -const { schemeAdmin, holdAdmin, dataView } = require('../../../../app/auth/permissions') +const { + schemeAdmin, + holdAdmin, + dataView +} = require('../../../../app/auth/permissions') const { getHolds } = require('../../../../app/holds') const api = require('../../../../app/api') const { getMIReport, getSuppressedReport } = require('../../../../app/storage') @@ -41,7 +45,10 @@ describe('Report test', () => { mockDownload = jest.fn().mockReturnValue({ readableStreamBody: 'Hello' }) - auth = { strategy: 'session-auth', credentials: { scope: [schemeAdmin, holdAdmin, dataView] } } + auth = { + strategy: 'session-auth', + credentials: { scope: [schemeAdmin, holdAdmin, dataView] } + } server = await createServer() await server.initialize() }) @@ -86,12 +93,14 @@ describe('Report test', () => { }) test('GET /report-list/holds returns stream if report available', async () => { - getHolds.mockResolvedValue([{ - frn: '123', - holdCategorySchemeName: 'Scheme 1', - holdCategoryName: 'Category 1', - dateTimeAdded: new Date() - }]) + getHolds.mockResolvedValue([ + { + frn: '123', + holdCategorySchemeName: 'Scheme 1', + holdCategoryName: 'Category 1', + dateTimeAdded: new Date() + } + ]) const options = { method: 'GET', @@ -177,7 +186,9 @@ describe('Report test', () => { const response = await server.inject(options) expect(response.statusCode).toBe(200) - expect(response.payload).toContain('No data available for the selected filters') + expect(response.payload).toContain( + 'No data available for the selected filters' + ) }) test('GET /report-list/claim-level-report renders with schemes', async () => { @@ -197,7 +208,9 @@ describe('Report test', () => { test('GET /report-list/claim-level-report/download returns CSV', async () => { api.getTrackingData.mockResolvedValue({ payload: { - claimLevelReportData: [{ frn: '789', claimNumber: '123', status: 'Completed' }] + claimLevelReportData: [ + { frn: '789', claimNumber: '123', status: 'Completed' } + ] } }) @@ -233,19 +246,18 @@ describe('Report test', () => { }) test('GET /report-list/request-editor-report returns unavailable page if no data', async () => { - api.getTrackingData.mockResolvedValue({ - payload: { reReportData: [] } - }) + api.getTrackingData.mockResolvedValue({}) const options = { method: 'GET', url: '/report-list/request-editor-report', auth } - const response = await server.inject(options) expect(response.statusCode).toBe(200) - expect(response.payload).toContain('

Payment report unavailable

') + expect(response.payload).toContain( + '

Payment report unavailable

' + ) }) test('GET /report-list/claim-level-report/download shows error when no data', async () => { @@ -263,7 +275,9 @@ describe('Report test', () => { const response = await server.inject(options) expect(response.statusCode).toBe(200) - expect(response.payload).toContain('No data available for the selected filters') + expect(response.payload).toContain( + 'No data available for the selected filters' + ) }) test('Validation error renders with validation messages', async () => { diff --git a/test/integration/narrow/routes/schemas/ap-listing-schema.test.js b/test/integration/narrow/routes/schemas/ap-listing-schema.test.js index fb316f4..fce1441 100644 --- a/test/integration/narrow/routes/schemas/ap-listing-schema.test.js +++ b/test/integration/narrow/routes/schemas/ap-listing-schema.test.js @@ -1,41 +1,44 @@ const schema = require('../../../../../app/routes/schemas/ap-listing-schema') -jest.mock('../../../../../app/auth') -test('should return error when start date is partially provided', () => { - const data = { - 'start-date-day': 1, - 'start-date-month': '', - 'start-date-year': 2022 - } - - const { error } = schema.validate(data) - - expect(error.details[0].message).toEqual('Start date must include day, month, and year') -}) - -test('should return error when end date is partially provided', () => { - const data = { - 'end-date-day': 1, - 'end-date-month': '', - 'end-date-year': 2022 - } - - const { error } = schema.validate(data) - - expect(error.details[0].message).toEqual('End date must include day, month, and year') -}) - -test('should return error when end date is less than start date', () => { - const data = { - 'start-date-day': 2, - 'start-date-month': 2, - 'start-date-year': 2022, - 'end-date-day': 1, - 'end-date-month': 2, - 'end-date-year': 2022 - } - - const { error } = schema.validate(data) - - expect(error.details[0].message).toEqual('End date cannot be less than start date') +describe('AP Listing Schema', () => { + test('should return error when start date is partially provided', () => { + const data = { + 'start-date-day': 1, + 'start-date-month': '', + 'start-date-year': 2022 + } + + const { error } = schema.validate(data) + expect(error).toBeDefined() + expect(error.message).toEqual( + 'Start date must include day, month, and year' + ) + }) + + test('should return error when end date is partially provided', () => { + const data = { + 'end-date-day': 1, + 'end-date-month': '', + 'end-date-year': 2022 + } + + const { error } = schema.validate(data) + expect(error).toBeDefined() + expect(error.message).toEqual('End date must include day, month, and year') + }) + + test('should return error when end date is less than start date', () => { + const data = { + 'start-date-day': 2, + 'start-date-month': 2, + 'start-date-year': 2022, + 'end-date-day': 1, + 'end-date-month': 2, + 'end-date-year': 2022 + } + + const { error } = schema.validate(data) + expect(error).toBeDefined() + expect(error.message).toEqual('End date cannot be less than start date') + }) }) diff --git a/test/unit/index.test.js b/test/unit/index.test.js new file mode 100644 index 0000000..789edf6 --- /dev/null +++ b/test/unit/index.test.js @@ -0,0 +1,45 @@ +const createServer = require('../../app/server') + +jest.mock('../../app/insights', () => ({ + setup: jest.fn() +})) +jest.mock('../../app/server') +jest.mock('log-timestamp') + +describe('App index', () => { + let mockServer + let processExitSpy + + beforeEach(() => { + jest.clearAllMocks() + processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}) + mockServer = { start: jest.fn() } + createServer.mockResolvedValue(mockServer) + console.log = jest.fn() + }) + + afterEach(() => { + processExitSpy.mockRestore() + }) + + test('starts server', async () => { + require('../../app/index') + await Promise.resolve() + expect(createServer).toHaveBeenCalled() + expect(mockServer.start).toHaveBeenCalled() + }) + + test('exits on error', async () => { + const error = new Error('test error') + createServer.mockRejectedValue(error) + + jest.isolateModules(() => { + require('../../app/index') + }) + + await new Promise(process.nextTick) + + expect(console.log).toHaveBeenCalledWith(error) + expect(processExitSpy).toHaveBeenCalledWith(1) + }) +}) diff --git a/test/unit/payments/get-data.test.js b/test/unit/payments/get-data.test.js index a14ebf0..4a12cdb 100644 --- a/test/unit/payments/get-data.test.js +++ b/test/unit/payments/get-data.test.js @@ -2,7 +2,10 @@ jest.mock('uuid') const { v4: mockUuid } = require('uuid') jest.mock('../../../app/messaging') -const { sendMessage: mockSendMessage, receiveMessage: mockReceiveMessage } = require('../../../app/messaging') +const { + sendMessage: mockSendMessage, + receiveMessage: mockReceiveMessage +} = require('../../../app/messaging') const { TYPE } = require('../../../app/constants/type') @@ -23,7 +26,10 @@ describe('get data', () => { test('should send message with category and value', async () => { await getData(CATEGORY, VALUE) - expect(mockSendMessage.mock.calls[0][0]).toMatchObject({ category: CATEGORY, value: VALUE }) + expect(mockSendMessage.mock.calls[0][0]).toMatchObject({ + category: CATEGORY, + value: VALUE + }) }) test('should send message with type', async () => { @@ -46,19 +52,15 @@ describe('get data', () => { expect(result).toBe(DATA) }) - test('should return undefined if no response received', async () => { - mockReceiveMessage.mockResolvedValue(undefined) + test('should return null if no response received', async () => { + mockReceiveMessage.mockResolvedValue(null) const result = await getData(CATEGORY, VALUE) - expect(result).toBe(undefined) + expect(result).toBe(null) }) test('should change scheme from SFI to SFI22 in response data', async () => { const responseMock = { - data: [ - { scheme: 'SFI' }, - { scheme: 'OTHER' }, - { scheme: 'SFI' } - ] + data: [{ scheme: 'SFI' }, { scheme: 'OTHER' }, { scheme: 'SFI' }] } mockReceiveMessage.mockResolvedValue(responseMock)