diff --git a/app/controllers/jobs.controller.js b/app/controllers/jobs.controller.js index c584df2aea..91628bb050 100644 --- a/app/controllers/jobs.controller.js +++ b/app/controllers/jobs.controller.js @@ -9,6 +9,10 @@ const ExportService = require('../services/jobs/export/export.service.js') const ProcessLicenceUpdates = require('../services/jobs/licence-updates/process-licence-updates.js') const ProcessSessionStorageCleanupService = require('../services/jobs/session-cleanup/process-session-storage-cleanup.service.js') const ProcessTimeLimitedLicencesService = require('../services/jobs/time-limited/process-time-limited-licences.service.js') +const ProcessReturnLogsService = require('../services/jobs/return-logs/process-return-logs.service.js') + +const redirectStatusCode = 204 +const notFoundStatusCode = 404 /** * Triggers export of all relevant tables to CSV and then uploads them to S3 @@ -20,30 +24,49 @@ const ProcessTimeLimitedLicencesService = require('../services/jobs/time-limited async function exportDb (_request, h) { ExportService.go() - return h.response().code(204) + return h.response().code(redirectStatusCode) } async function licenceUpdates (_request, h) { ProcessLicenceUpdates.go() - return h.response().code(204) + return h.response().code(redirectStatusCode) } async function sessionCleanup (_request, h) { ProcessSessionStorageCleanupService.go() - return h.response().code(204) + return h.response().code(redirectStatusCode) } async function timeLimited (_request, h) { ProcessTimeLimitedLicencesService.go() - return h.response().code(204) + return h.response().code(redirectStatusCode) +} + +async function returnLogs (request, h) { + const { cycle } = request.params + + if (!['summer', 'all-year'].includes(cycle)) { + return h.response().code(notFoundStatusCode) + } + + let licenceReference + + if (h.request.payload !== null && h.request.payload.licenceReference) { + licenceReference = h.request.payload.licenceReference + } + + ProcessReturnLogsService.go(cycle, licenceReference) + + return h.response().code(redirectStatusCode) } module.exports = { exportDb, licenceUpdates, + returnLogs, sessionCleanup, timeLimited } diff --git a/app/routes/jobs.routes.js b/app/routes/jobs.routes.js index e9b236aa5c..ed1e978fa3 100644 --- a/app/routes/jobs.routes.js +++ b/app/routes/jobs.routes.js @@ -58,6 +58,20 @@ const routes = [ crumb: false } } + }, + { + method: 'POST', + path: '/jobs/return-logs/{cycle}', + options: { + handler: JobsController.returnLogs, + app: { + plainOutput: true + }, + auth: false, + plugins: { + crumb: false + } + } } ] diff --git a/app/services/jobs/return-logs/fetch-return-logs.service.js b/app/services/jobs/return-logs/fetch-return-logs.service.js new file mode 100644 index 0000000000..282b22e0d6 --- /dev/null +++ b/app/services/jobs/return-logs/fetch-return-logs.service.js @@ -0,0 +1,243 @@ +'use strict' + +/** + * Fetches data needed for the generating return logs + * @module FetchLicenceWithoutReturnsService + */ + +const ReturnLogModel = require('../../../models/return-log.model.js') +const ReturnRequirementModel = require('../../../models/return-requirement.model.js') +const ReturnVersionModel = require('../../../models/return-version.model.js') + +const { db } = require('../../../../db/db.js') + +const allYearDueDateDay = 28 +const allYearDueDateMonth = 3 +const allYearEndDay = 31 +const allYearEndMonth = 2 +const allYearStartDay = 1 +const allYearStartMonth = 3 + +const summerDueDateDay = 28 +const summerDueDateMonth = 10 +const summerEndDay = 31 +const summerEndMonth = 9 +const summerStartDay = 1 +const summerStartMonth = 10 + +const endOfSummerCycle = new Date(new Date().getFullYear() + 1, summerEndMonth, summerEndDay) +const endOfWinterAndAllYearCycle = new Date(new Date().getFullYear() + 1, allYearEndMonth, allYearEndDay) + +/** + * Fetch all return requirements that need return logs created. + * + * @returns {Promise} the list of return requirement ids + */ +async function go (isSummer, licenceReference) { + const requirementsForReturns = await _fetchReturnRequirements(isSummer, licenceReference) + const data = await _generateReturnLogPayload(isSummer, requirementsForReturns) + + return data +} + +async function _createMetaData (isSummer, endDate, requirements) { + return { + description: requirements.siteDescription, + isCurrent: requirements.returnVersion.reason !== 'succession-or-transfer-of-licence', + isFinal: _isFinal(endDate, isSummer), + isSummer, + isTwoPartTariff: requirements.twoPartTariff, + isUpload: requirements.upload, + nald: { + regionCode: requirements.returnVersion.licence.region.naldRegionId, + areaCode: requirements.returnVersion.licence.areacode, + formatId: requirements.legacyId, + periodStartDay: requirements.abstractionPeriodStartDay, + periodStartMonth: requirements.abstractionPeriodStartMonth, + periodEndDay: requirements.abstractionPeriodEndDay, + periodEndMonth: requirements.abstractionPeriodEndMonth + }, + points: requirements.returnRequirementPoints, + purposes: requirements.returnRequirementPurposes, + version: 1 + } +} + +function _createReturnLogId (requirements, startDate, endDate) { + const regionCode = requirements.returnVersion.licence.region.naldRegionId + const licenceReference = requirements.returnVersion.licence.licenceRef + const legacyId = requirements.legacyId + + return `v1:${regionCode}:${licenceReference}:${legacyId}:${startDate}:${endDate}` +} + +async function _fetchExternalIds (cycleStartDate) { + const externalIds = await ReturnLogModel.query() + .select(['licenceRef', + db.raw("concat(ret.metadata->'nald'->>'regionCode', ':', ret.return_requirement) as externalid") + ]) + .from('returns.returns as ret') + .where('startDate', '>=', cycleStartDate) + + const externalIdsArray = externalIds.map((item) => { + return item.externalid + }) + + return externalIdsArray +} + +async function _fetchReturnRequirements (isSummer, licenceReference) { + const cycleStartDate = _getCycleStartDate(isSummer) + const externalIds = await _fetchExternalIds(cycleStartDate) + + const results = await ReturnRequirementModel.query() + .whereNotIn('returnRequirements.externalId', externalIds) + .whereExists(_whereExistsClause(licenceReference, cycleStartDate)) + .where('returnRequirements.summer', isSummer) + .withGraphFetched('returnVersion') + .modifyGraph('returnVersion', (builder) => { + builder.select(['endDate', + 'id', + 'startDate', + 'reason']) + }) + .withGraphFetched('returnVersion.licence') + .modifyGraph('returnVersion.licence', (builder) => { + builder.select(['expiredDate', + 'id', + 'lapsedDate', + 'licenceRef', + 'revokedDate', + db.raw('regions->>\'historicalAreaCode\' as areacode')]) + }) + .withGraphFetched('returnVersion.licence.region') + .modifyGraph('returnVersion.licence.region', (builder) => { + builder.select(['id', 'naldRegionId']) + }) + .withGraphFetched('returnRequirementPoints') + .withGraphFetched('returnRequirementPurposes') + + return results +} + +function _formatDate (date) { + return date.toISOString().split('T')[0] +} + +async function _generateReturnLogPayload (isSummer, requirementsForReturns) { + const returnLogs = requirementsForReturns.map(async (requirements) => { + const startDate = _getCycleStartDate(isSummer) + const endDate = _getCycleEndDate(isSummer, requirements.returnVersion) + const id = _createReturnLogId(requirements, startDate, endDate) + const metadata = await _createMetaData(isSummer, endDate, requirements) + + return { + createdAt: new Date(), + dueDate: _getCycleDueDate(isSummer), + endDate, + id, + licenceRef: requirements.returnVersion.licence.licenceRef, + metadata, + returnsFrequency: requirements.reportingFrequency, + startDate, + status: 'due', + source: 'WRLS', + returnReference: requirements.legacyId.toString() + } + }) + + const results = await Promise.all(returnLogs) + + return results +} + +function _getCycleDueDate (isSummer) { + return isSummer + ? _formatDate(new Date(new Date().getFullYear() + 1, summerDueDateMonth, summerDueDateDay)) + : _formatDate(new Date(new Date().getFullYear() + 1, allYearDueDateMonth, allYearDueDateDay)) +} + +function _getCycleEndDate (isSummer, returnVersion) { + const dates = [returnVersion.licence.expiredDate, + returnVersion.licence.lapsedDate, + returnVersion.licence.revokedDate, + returnVersion.endDate] + .filter((date) => { return date !== null }) + .map((date) => { return new Date(date) }) + + if (dates.length === 0) { + return isSummer + ? _formatDate(endOfSummerCycle) + : _formatDate(endOfWinterAndAllYearCycle) + } + + dates.map((date) => { return date.getTime() }) + const earliestEndDate = new Date(Math.min(...dates)) + + if (isSummer) { + if (earliestEndDate < endOfSummerCycle) { + return _formatDate(earliestEndDate) + } + + return _formatDate(endOfSummerCycle) + } + + if (earliestEndDate < endOfWinterAndAllYearCycle) { + return _formatDate(earliestEndDate) + } + + return _formatDate(endOfWinterAndAllYearCycle) +} + +function _getCycleStartDate (isSummer) { + return isSummer + ? _formatDate(new Date(new Date().getFullYear(), summerStartMonth, summerStartDay)) + : _formatDate(new Date(new Date().getFullYear(), allYearStartMonth, allYearStartDay)) +} + +function _isFinal (endDateString, isSummer) { + const endDate = new Date(endDateString) + + return ((isSummer && endDate < endOfSummerCycle) || (!isSummer && endDate < endOfWinterAndAllYearCycle)) +} + +function _whereExistsClause (licenceReference, cycleStartDate) { + const query = ReturnVersionModel.query().select(1) + + query.select(1) + .innerJoinRelated('licence') + .where('returnVersions.startDate', '<=', cycleStartDate) + .where('returnVersions.status', 'current') + .where((builder) => { + builder + .whereNull('returnVersions.endDate') + .orWhere('returnVersions.endDate', '>=', cycleStartDate) + }) + .where((builder) => { + builder + .whereNull('licence.expiredDate') + .orWhere('licence.expiredDate', '>=', cycleStartDate) + }) + .where((builder) => { + builder + .whereNull('licence.lapsedDate') + .orWhere('licence.lapsedDate', '>=', cycleStartDate) + }) + .where((builder) => { + builder + .whereNull('licence.revokedDate') + .orWhere('licence.revokedDate', '>=', cycleStartDate) + }) + + query.whereColumn('returnVersions.id', 'returnRequirements.returnVersionId') + + if (licenceReference) { + query.where('licence.licenceRef', licenceReference) + } + + return query +} + +module.exports = { + go +} diff --git a/app/services/jobs/return-logs/process-return-logs.service.js b/app/services/jobs/return-logs/process-return-logs.service.js new file mode 100644 index 0000000000..2fc566b3a9 --- /dev/null +++ b/app/services/jobs/return-logs/process-return-logs.service.js @@ -0,0 +1,58 @@ +'use strict' + +/** + * Process the return logs for the next cycle + * @module ProcessReturnLogsService + */ + +const { calculateAndLogTimeTaken, currentTimeInNanoseconds } = require('../../../lib/general.lib.js') +const FetchReturnLogsService = require('./fetch-return-logs.service.js') +const ReturnLogModel = require('../../../models/return-log.model.js') + +/** + * Creates the return logs for the next cycle + * The return requirement is the information held against the licence that defines how and when an abstractor needs to + * submit their returns. + * + * The return log is the 'header' record generated each return cycle from the requirement that an abstractor submits + * their returns against. + * + * When users make changes to return requirements, the service will determine if any new return logs need to be + * created depending on the current cycle. + * + * But if no changes are ever made to a licence's return requirements, and this job didn't exist, no new return logs + * would be created. + * + * So, this job will run twice yearly: once for each cycle. The job determines which return requirements need a return + * log generated for the selected cycle and then creates them. + * + * > Because the job creates _all_ return logs in a cycle, it makes it difficult to test what it is generating is + * > correct. So, to support testing and validation, we can pass a licence ref in the job request to limit the creation + * > to just a single licence. + * @param {string} cycle - the return cycle to create logs for (summer or all-year) + * @param {string} [licenceReference] - An optional argument to limit return log creation to just the specific licence + */ +async function go (cycle, licenceReference = null) { + try { + const startTime = currentTimeInNanoseconds() + const isSummer = cycle === 'summer' + const returnLogs = await FetchReturnLogsService.go(isSummer, licenceReference) + + await _createReturnLogs(returnLogs) + + calculateAndLogTimeTaken(startTime, 'Create return logs job complete', { cycle, licenceReference }) + } catch (error) { + global.GlobalNotifier.omfg('Create return logs job failed', { cycle, error }) + } +} + +async function _createReturnLogs (returnLogs) { + for (const returnLog of returnLogs) { + await ReturnLogModel.query() + .insert(returnLog) + } +} + +module.exports = { + go +} diff --git a/db/migrations/public/20240807132127_alter-return-logs-view-include-source.js b/db/migrations/public/20240807132127_alter-return-logs-view-include-source.js new file mode 100644 index 0000000000..ec97ab3e29 --- /dev/null +++ b/db/migrations/public/20240807132127_alter-return-logs-view-include-source.js @@ -0,0 +1,63 @@ +'use strict' + +const viewName = 'return_logs' + +exports.up = function (knex) { + return knex + .schema + .dropViewIfExists(viewName) + .createView(viewName, (view) => { + // NOTE: We have commented out unused columns from the source table + view.as(knex('returns').withSchema('returns').select([ + 'return_id AS id', + // 'regime', // always 'water' + // 'licence_type', // always 'abstraction' + 'licence_ref', + 'start_date', + 'end_date', + 'returns_frequency', + 'status', + 'source', + 'metadata', + 'received_date', + 'return_requirement as return_reference', + 'due_date', + 'under_query', + // 'under_query_comment', + // 'is_test', + // 'return_cycle_id' // is populated but links to a table that does not appear to be used + 'created_at', + 'updated_at' + ])) + }) +} + +exports.down = function (knex) { + return knex + .schema + .dropViewIfExists(viewName) + .createView(viewName, (view) => { + // NOTE: We have commented out unused columns from the source table + view.as(knex('returns').withSchema('returns').select([ + 'return_id AS id', + // 'regime', // always 'water' + // 'licence_type', // always 'abstraction' + 'licence_ref', + 'start_date', + 'end_date', + 'returns_frequency', + 'status', + // 'source', // always 'NALD' + 'metadata', + 'received_date', + 'return_requirement', + 'due_date', + 'under_query', + // 'under_query_comment', + // 'is_test', + // 'return_cycle_id' // is populated but links to a table that does not appear to be used + 'created_at', + 'updated_at' + ])) + }) +} diff --git a/test/controllers/jobs.controller.test.js b/test/controllers/jobs.controller.test.js index ea66d385ae..2cd3105bfd 100644 --- a/test/controllers/jobs.controller.test.js +++ b/test/controllers/jobs.controller.test.js @@ -13,6 +13,7 @@ const ExportService = require('../../app/services/jobs/export/export.service.js' const ProcessLicenceUpdatesService = require('../../app/services/jobs/licence-updates/process-licence-updates.js') const ProcessSessionStorageCleanupService = require('../../app/services/jobs/session-cleanup/process-session-storage-cleanup.service.js') const ProcessTimeLimitedLicencesService = require('../../app/services/jobs/time-limited/process-time-limited-licences.service.js') +const ProcessReturnLogsService = require('../../app/services/jobs/return-logs/process-return-logs.service.js') // For running our service const { init } = require('../../app/server.js') @@ -116,4 +117,60 @@ describe('Jobs controller', () => { }) }) }) + + describe('/jobs/return-logs/{cycle}', () => { + describe('when the requested cycle is summer', () => { + describe('POST', () => { + beforeEach(() => { + options = { method: 'POST', url: '/jobs/return-logs/summer' } + }) + + describe('when the request succeeds', () => { + beforeEach(async () => { + Sinon.stub(ProcessReturnLogsService, 'go').resolves() + }) + + it('returns a 204 response', async () => { + const response = await server.inject(options) + + expect(response.statusCode).to.equal(204) + }) + }) + }) + }) + + describe('when the requested cycle is all-year', () => { + describe('POST', () => { + beforeEach(() => { + options = { method: 'POST', url: '/jobs/return-logs/all-year' } + }) + + describe('when the request succeeds', () => { + beforeEach(async () => { + Sinon.stub(ProcessReturnLogsService, 'go').resolves() + }) + + it('returns a 204 response', async () => { + const response = await server.inject(options) + + expect(response.statusCode).to.equal(204) + }) + }) + }) + }) + + describe('when the requested cycel is unknown', () => { + describe('POST', () => { + beforeEach(() => { + options = { method: 'POST', url: '/jobs/return-logs/winter' } + }) + + it('returns a 404 response', async () => { + const response = await server.inject(options) + + expect(response.statusCode).to.equal(404) + }) + }) + }) + }) }) diff --git a/test/services/jobs/return-logs/fetch-return-logs.service.test.js b/test/services/jobs/return-logs/fetch-return-logs.service.test.js new file mode 100644 index 0000000000..a25f8599d9 --- /dev/null +++ b/test/services/jobs/return-logs/fetch-return-logs.service.test.js @@ -0,0 +1,568 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') + +const { describe, it, before } = exports.lab = Lab.script() +const { expect } = Code + +// Test helpers +const LicenceHelper = require('../../../support/helpers/licence.helper.js') +const RegionHelper = require('../../../support/helpers/region.helper.js') +const ReturnLogHelper = require('../../../support/helpers/return-log.helper.js') +const ReturnRequirementHelper = require('../../../support/helpers/return-requirement.helper.js') +const ReturnRequirementPointHelper = require('../../../support/helpers/return-requirement-point.helper.js') +const ReturnRequirementPurposeHelper = require('../../../support/helpers/return-requirement-purpose.helper.js') +const ReturnVersionHelper = require('../../../support/helpers/return-version.helper.js') + +// Thing under test +const FetchReturnLogsService = require('../../../../app/services/jobs/return-logs/fetch-return-logs.service.js') + +describe('Fetch return logs service', () => { + const allYearDueDate = new Date(new Date().getFullYear() + 1, 3, 28).toISOString().split('T')[0] + const summerDueDate = new Date(new Date().getFullYear() + 1, 10, 28).toISOString().split('T')[0] + const allYearEndDate = new Date(new Date().getFullYear() + 1, 2, 31).toISOString().split('T')[0] + const summerEndDate = new Date(new Date().getFullYear() + 1, 9, 31).toISOString().split('T')[0] + const allYearStartDate = new Date(new Date().getFullYear(), 3, 1).toISOString().split('T')[0] + const summerStartDate = new Date(new Date().getFullYear(), 10, 1).toISOString().split('T')[0] + + describe('When isSummer is false, one return requirement and a licenceRef provided', () => { + let licence + let region + let returnVersion + let returnRequirement + let returnRequirementPoint + let returnRequirementPurpose + + before(async () => { + region = RegionHelper.select() + licence = await LicenceHelper.add({ regionId: region.id }) + returnVersion = await ReturnVersionHelper.add({ licenceId: licence.id }) + returnRequirement = await ReturnRequirementHelper.add({ returnVersionId: returnVersion.id }) + returnRequirementPoint = await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement.id }) + returnRequirementPurpose = await ReturnRequirementPurposeHelper.add({ returnRequirementId: returnRequirement.id }) + }) + + it('should return one return log payload', async () => { + const result = await FetchReturnLogsService.go(false, licence.licenceRef) + + expect(result.length).to.equal(1) + expect(result[0].dueDate).to.equal(allYearDueDate) + expect(result[0].endDate).to.equal(allYearEndDate) + expect(result[0].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement.legacyId}:${allYearStartDate}:${allYearEndDate}`) + expect(result[0].licenceRef).to.equal(licence.licenceRef) + expect(result[0].metadata).to.equal({ + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: false, + isSummer: false, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement.legacyId, + periodStartDay: returnRequirement.abstractionPeriodStartDay, + periodStartMonth: returnRequirement.abstractionPeriodStartMonth, + periodEndDay: returnRequirement.abstractionPeriodEndDay, + periodEndMonth: returnRequirement.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint], + purposes: [returnRequirementPurpose], + version: 1 + }) + expect(result[0].returnsFrequency).to.equal('day') + expect(result[0].startDate).to.equal(allYearStartDate) + expect(result[0].status).to.equal('due') + expect(result[0].source).to.equal('WRLS') + }) + }) + + describe('when isSummer is true, one return requirement and a licenceRef provided', () => { + let licence + let region + let returnVersion + let returnRequirement + let returnRequirementPoint + let returnRequirementPurpose + + before(async () => { + region = RegionHelper.select() + licence = await LicenceHelper.add({ regionId: region.id }) + returnVersion = await ReturnVersionHelper.add({ licenceId: licence.id }) + returnRequirement = await ReturnRequirementHelper.add({ summer: true, returnVersionId: returnVersion.id }) + returnRequirementPoint = await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement.id }) + returnRequirementPurpose = await ReturnRequirementPurposeHelper.add({ returnRequirementId: returnRequirement.id }) + }) + + it('should return one return log payload', async () => { + const result = await FetchReturnLogsService.go(true, licence.licenceRef) + + expect(result.length).to.equal(1) + expect(result[0].dueDate).to.equal(summerDueDate) + expect(result[0].endDate).to.equal(summerEndDate) + expect(result[0].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement.legacyId}:${summerStartDate}:${summerEndDate}`) + expect(result[0].licenceRef).to.equal(licence.licenceRef) + expect(result[0].metadata).to.equal({ + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: false, + isSummer: true, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement.legacyId, + periodStartDay: returnRequirement.abstractionPeriodStartDay, + periodStartMonth: returnRequirement.abstractionPeriodStartMonth, + periodEndDay: returnRequirement.abstractionPeriodEndDay, + periodEndMonth: returnRequirement.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint], + purposes: [returnRequirementPurpose], + version: 1 + }) + expect(result[0].returnsFrequency).to.equal('day') + expect(result[0].startDate).to.equal(summerStartDate) + expect(result[0].status).to.equal('due') + expect(result[0].source).to.equal('WRLS') + }) + }) + + describe('when isSummer is false, two return requirements and a licenceRef provided', () => { + let licence + let region + let returnVersion + let returnRequirement + let returnRequirement2 + let returnRequirementPoint + let returnRequirementPurpose + let returnRequirementPoint2 + let returnRequirementPurpose2 + + before(async () => { + region = RegionHelper.select() + licence = await LicenceHelper.add({ regionId: region.id }) + returnVersion = await ReturnVersionHelper.add({ licenceId: licence.id }) + returnRequirement = await ReturnRequirementHelper.add({ returnVersionId: returnVersion.id }) + returnRequirement2 = await ReturnRequirementHelper.add({ returnVersionId: returnVersion.id }) + returnRequirementPoint = await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement.id }) + returnRequirementPurpose = await ReturnRequirementPurposeHelper.add({ returnRequirementId: returnRequirement.id }) + returnRequirementPoint2 = await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement2.id }) + returnRequirementPurpose2 = await ReturnRequirementPurposeHelper.add({ + returnRequirementId: returnRequirement2.id + }) + }) + + it('should return two return log payloads', async () => { + const result = await FetchReturnLogsService.go(false, licence.licenceRef) + + expect(result.length).to.equal(2) + expect(result[0].dueDate).to.equal(allYearDueDate) + expect(result[0].endDate).to.equal(allYearEndDate) + expect(result[0].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement.legacyId}:${allYearStartDate}:${allYearEndDate}`) + expect(result[0].licenceRef).to.equal(licence.licenceRef) + expect(result[0].metadata).to.equal({ + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: false, + isSummer: false, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement.legacyId, + periodStartDay: returnRequirement.abstractionPeriodStartDay, + periodStartMonth: returnRequirement.abstractionPeriodStartMonth, + periodEndDay: returnRequirement.abstractionPeriodEndDay, + periodEndMonth: returnRequirement.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint], + purposes: [returnRequirementPurpose], + version: 1 + }) + expect(result[0].returnsFrequency).to.equal('day') + expect(result[0].startDate).to.equal(allYearStartDate) + expect(result[0].status).to.equal('due') + expect(result[0].source).to.equal('WRLS') + expect(result[1].dueDate).to.equal(allYearDueDate) + expect(result[1].endDate).to.equal(allYearEndDate) + expect(result[1].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement2.legacyId}:${allYearStartDate}:${allYearEndDate}`) + expect(result[1].licenceRef).to.equal(licence.licenceRef) + expect(result[1].metadata).to.equal({ + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: false, + isSummer: false, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement2.legacyId, + periodStartDay: returnRequirement2.abstractionPeriodStartDay, + periodStartMonth: returnRequirement2.abstractionPeriodStartMonth, + periodEndDay: returnRequirement2.abstractionPeriodEndDay, + periodEndMonth: returnRequirement2.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint2], + purposes: [returnRequirementPurpose2], + version: 1 + }) + expect(result[1].returnsFrequency).to.equal('day') + expect(result[1].startDate).to.equal(allYearStartDate) + expect(result[1].status).to.equal('due') + expect(result[1].source).to.equal('WRLS') + }) + }) + + describe('when isSummer is true, two return requirements and a licenceRef provided', () => { + let licence + let region + let returnVersion + let returnRequirement + let returnRequirement2 + let returnRequirementPoint + let returnRequirementPurpose + let returnRequirementPoint2 + let returnRequirementPurpose2 + + before(async () => { + region = RegionHelper.select() + licence = await LicenceHelper.add({ regionId: region.id }) + returnVersion = await ReturnVersionHelper.add({ licenceId: licence.id }) + returnRequirement = await ReturnRequirementHelper.add({ summer: true, returnVersionId: returnVersion.id }) + returnRequirement2 = await ReturnRequirementHelper.add({ summer: true, returnVersionId: returnVersion.id }) + returnRequirementPoint = await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement.id }) + returnRequirementPurpose = await ReturnRequirementPurposeHelper.add({ returnRequirementId: returnRequirement.id }) + returnRequirementPoint2 = await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement2.id }) + returnRequirementPurpose2 = await ReturnRequirementPurposeHelper.add({ + returnRequirementId: returnRequirement2.id + }) + await ReturnLogHelper.add({ + dueDate: summerDueDate, + endDate: summerEndDate, + licenceRef: licence.licenceRef, + metadata: { + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: false, + isSummer: false, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement.legacyId, + periodStartDay: returnRequirement.abstractionPeriodStartDay, + periodStartMonth: returnRequirement.abstractionPeriodStartMonth, + periodEndDay: returnRequirement.abstractionPeriodEndDay, + periodEndMonth: returnRequirement.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint], + purposes: [returnRequirementPurpose], + version: 1 + }, + returnReference: returnRequirement.legacyId, + startDate: summerStartDate + }) + }) + + it('should return two return log payloads', async () => { + const result = await FetchReturnLogsService.go(true, licence.licenceRef) + + expect(result.length).to.equal(2) + expect(result[0].dueDate).to.equal(summerDueDate) + expect(result[0].endDate).to.equal(summerEndDate) + expect(result[0].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement.legacyId}:${summerStartDate}:${summerEndDate}`) + expect(result[0].licenceRef).to.equal(licence.licenceRef) + expect(result[0].metadata).to.equal({ + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: false, + isSummer: true, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement.legacyId, + periodStartDay: returnRequirement.abstractionPeriodStartDay, + periodStartMonth: returnRequirement.abstractionPeriodStartMonth, + periodEndDay: returnRequirement.abstractionPeriodEndDay, + periodEndMonth: returnRequirement.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint], + purposes: [returnRequirementPurpose], + version: 1 + }) + expect(result[0].returnsFrequency).to.equal('day') + expect(result[0].startDate).to.equal(summerStartDate) + expect(result[0].status).to.equal('due') + expect(result[0].source).to.equal('WRLS') + expect(result[1].dueDate).to.equal(summerDueDate) + expect(result[1].endDate).to.equal(summerEndDate) + expect(result[1].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement2.legacyId}:${summerStartDate}:${summerEndDate}`) + expect(result[1].licenceRef).to.equal(licence.licenceRef) + expect(result[1].metadata).to.equal({ + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: false, + isSummer: true, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement2.legacyId, + periodStartDay: returnRequirement2.abstractionPeriodStartDay, + periodStartMonth: returnRequirement2.abstractionPeriodStartMonth, + periodEndDay: returnRequirement2.abstractionPeriodEndDay, + periodEndMonth: returnRequirement2.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint2], + purposes: [returnRequirementPurpose2], + version: 1 + }) + expect(result[1].returnsFrequency).to.equal('day') + expect(result[1].startDate).to.equal(summerStartDate) + expect(result[1].status).to.equal('due') + expect(result[1].source).to.equal('WRLS') + }) + }) + + describe('when isSummer is false, there is an expired date, one return requirement and a licenceRef provided', () => { + const expiredDate = new Date(new Date().getFullYear() + 1, 1, 31).toISOString().split('T')[0] + + let licence + let region + let returnVersion + let returnRequirement + let returnRequirementPoint + let returnRequirementPurpose + + before(async () => { + region = RegionHelper.select() + licence = await LicenceHelper.add({ expiredDate, regionId: region.id }) + returnVersion = await ReturnVersionHelper.add({ licenceId: licence.id }) + returnRequirement = await ReturnRequirementHelper.add({ returnVersionId: returnVersion.id }) + returnRequirementPoint = await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement.id }) + returnRequirementPurpose = await ReturnRequirementPurposeHelper.add({ returnRequirementId: returnRequirement.id }) + }) + + it('should return one return log payload', async () => { + const result = await FetchReturnLogsService.go(false, licence.licenceRef) + + expect(result.length).to.equal(1) + expect(result[0].dueDate).to.equal(allYearDueDate) + expect(result[0].endDate).to.equal(expiredDate) + expect(result[0].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement.legacyId}:${allYearStartDate}:${expiredDate}`) + expect(result[0].licenceRef).to.equal(licence.licenceRef) + expect(result[0].metadata).to.equal({ + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: true, + isSummer: false, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement.legacyId, + periodStartDay: returnRequirement.abstractionPeriodStartDay, + periodStartMonth: returnRequirement.abstractionPeriodStartMonth, + periodEndDay: returnRequirement.abstractionPeriodEndDay, + periodEndMonth: returnRequirement.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint], + purposes: [returnRequirementPurpose], + version: 1 + }) + expect(result[0].returnsFrequency).to.equal('day') + expect(result[0].startDate).to.equal(allYearStartDate) + expect(result[0].status).to.equal('due') + expect(result[0].source).to.equal('WRLS') + }) + }) + + describe('when isSummer is false, there is an expired date after the end of the cycle, one return requirement and a licenceRef provided', () => { + const expiredDate = new Date(new Date().getFullYear() + 1, 3, 31).toISOString().split('T')[0] + + let licence + let region + let returnVersion + let returnRequirement + let returnRequirementPoint + let returnRequirementPurpose + + before(async () => { + region = RegionHelper.select() + licence = await LicenceHelper.add({ expiredDate, regionId: region.id }) + returnVersion = await ReturnVersionHelper.add({ licenceId: licence.id }) + returnRequirement = await ReturnRequirementHelper.add({ returnVersionId: returnVersion.id }) + returnRequirementPoint = await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement.id }) + returnRequirementPurpose = await ReturnRequirementPurposeHelper.add({ returnRequirementId: returnRequirement.id }) + }) + + it('should return one return log payload', async () => { + const result = await FetchReturnLogsService.go(false, licence.licenceRef) + + expect(result.length).to.equal(1) + expect(result[0].dueDate).to.equal(allYearDueDate) + expect(result[0].endDate).to.equal(allYearEndDate) + expect(result[0].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement.legacyId}:${allYearStartDate}:${allYearEndDate}`) + expect(result[0].licenceRef).to.equal(licence.licenceRef) + expect(result[0].metadata).to.equal({ + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: false, + isSummer: false, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement.legacyId, + periodStartDay: returnRequirement.abstractionPeriodStartDay, + periodStartMonth: returnRequirement.abstractionPeriodStartMonth, + periodEndDay: returnRequirement.abstractionPeriodEndDay, + periodEndMonth: returnRequirement.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint], + purposes: [returnRequirementPurpose], + version: 1 + }) + expect(result[0].returnsFrequency).to.equal('day') + expect(result[0].startDate).to.equal(allYearStartDate) + expect(result[0].status).to.equal('due') + expect(result[0].source).to.equal('WRLS') + }) + }) + + describe('when isSummer is true, there is a lapsed date, one return requirement and a licenceRef provided', () => { + const lapsedDate = new Date(new Date().getFullYear() + 1, 8, 31).toISOString().split('T')[0] + + let licence + let region + let returnVersion + let returnRequirement + let returnRequirementPoint + let returnRequirementPurpose + + before(async () => { + region = RegionHelper.select() + licence = await LicenceHelper.add({ lapsedDate, regionId: region.id }) + returnVersion = await ReturnVersionHelper.add({ licenceId: licence.id }) + returnRequirement = await ReturnRequirementHelper.add({ summer: true, returnVersionId: returnVersion.id }) + returnRequirementPoint = await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement.id }) + returnRequirementPurpose = await ReturnRequirementPurposeHelper.add({ returnRequirementId: returnRequirement.id }) + }) + + it('should return one return log payload', async () => { + const result = await FetchReturnLogsService.go(true, licence.licenceRef) + + expect(result.length).to.equal(1) + expect(result[0].dueDate).to.equal(summerDueDate) + expect(result[0].endDate).to.equal(lapsedDate) + expect(result[0].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement.legacyId}:${summerStartDate}:${lapsedDate}`) + expect(result[0].licenceRef).to.equal(licence.licenceRef) + expect(result[0].metadata).to.equal({ + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: true, + isSummer: true, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement.legacyId, + periodStartDay: returnRequirement.abstractionPeriodStartDay, + periodStartMonth: returnRequirement.abstractionPeriodStartMonth, + periodEndDay: returnRequirement.abstractionPeriodEndDay, + periodEndMonth: returnRequirement.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint], + purposes: [returnRequirementPurpose], + version: 1 + }) + expect(result[0].returnsFrequency).to.equal('day') + expect(result[0].startDate).to.equal(summerStartDate) + expect(result[0].status).to.equal('due') + expect(result[0].source).to.equal('WRLS') + }) + }) + + describe('when isSummer is true, there is a revoked date that is after the cycle, one return requirement and a licenceRef provided', () => { + const revokedDate = new Date(new Date().getFullYear() + 1, 10, 31).toISOString().split('T')[0] + + let licence + let region + let returnVersion + let returnRequirement + let returnRequirementPoint + let returnRequirementPurpose + + before(async () => { + region = RegionHelper.select() + licence = await LicenceHelper.add({ revokedDate, regionId: region.id }) + returnVersion = await ReturnVersionHelper.add({ licenceId: licence.id }) + returnRequirement = await ReturnRequirementHelper.add({ summer: true, returnVersionId: returnVersion.id }) + returnRequirementPoint = await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement.id }) + returnRequirementPurpose = await ReturnRequirementPurposeHelper.add({ returnRequirementId: returnRequirement.id }) + }) + + it('should return one return log payload', async () => { + const result = await FetchReturnLogsService.go(true, licence.licenceRef) + + expect(result.length).to.equal(1) + expect(result[0].dueDate).to.equal(summerDueDate) + expect(result[0].endDate).to.equal(summerEndDate) + expect(result[0].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement.legacyId}:${summerStartDate}:${summerEndDate}`) + expect(result[0].licenceRef).to.equal(licence.licenceRef) + expect(result[0].metadata).to.equal({ + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: false, + isSummer: true, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: region.naldRegionId, + areaCode: licence.regions.historicalAreaCode, + formatId: returnRequirement.legacyId, + periodStartDay: returnRequirement.abstractionPeriodStartDay, + periodStartMonth: returnRequirement.abstractionPeriodStartMonth, + periodEndDay: returnRequirement.abstractionPeriodEndDay, + periodEndMonth: returnRequirement.abstractionPeriodEndMonth + }, + points: [returnRequirementPoint], + purposes: [returnRequirementPurpose], + version: 1 + }) + expect(result[0].returnsFrequency).to.equal('day') + expect(result[0].startDate).to.equal(summerStartDate) + expect(result[0].status).to.equal('due') + expect(result[0].source).to.equal('WRLS') + }) + }) + + describe('when isSummer is false, and no licenceReference is provided it should return all the return logs that are eligible', () => { + it('should return three return log payloads', async () => { + const result = await FetchReturnLogsService.go(false) + + expect(result.length).to.equal(5) + }) + }) + + describe('when isSummer is true, and no licenceReference is provided it should return all the return logs that are eligible', () => { + it('should return three return log payloads', async () => { + const result = await FetchReturnLogsService.go(true) + + expect(result.length).to.equal(5) + }) + }) +}) diff --git a/test/services/jobs/return-logs/process-return-logs.service.test.js b/test/services/jobs/return-logs/process-return-logs.service.test.js new file mode 100644 index 0000000000..cf23261184 --- /dev/null +++ b/test/services/jobs/return-logs/process-return-logs.service.test.js @@ -0,0 +1,77 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') +const Sinon = require('sinon') + +const { describe, it, before } = exports.lab = Lab.script() +const { expect } = Code + +// Test helpers +const ReturnLogModel = require('../../../../app/models/return-log.model.js') +const LicenceHelper = require('../../../support/helpers/licence.helper.js') +const RegionHelper = require('../../../support/helpers/region.helper.js') +const ReturnRequirementHelper = require('../../../support/helpers/return-requirement.helper.js') +const ReturnRequirementPointHelper = require('../../../support/helpers/return-requirement-point.helper.js') +const ReturnRequirementPurposeHelper = require('../../../support/helpers/return-requirement-purpose.helper.js') +const ReturnVersionHelper = require('../../../support/helpers/return-version.helper.js') + +// Thing under test +const ProcessReturnLogsService = require('../../../../app/services/jobs/return-logs/process-return-logs.service.js') + +describe('Process return logs service', () => { + const allYearDueDate = new Date(new Date().getFullYear() + 1, 3, 28).toISOString().split('T')[0] + const allYearEndDate = new Date(new Date().getFullYear() + 1, 2, 31).toISOString().split('T')[0] + const allYearStartDate = new Date(new Date().getFullYear(), 3, 1).toISOString().split('T')[0] + + describe('cycle is "summer" and a licence reference is provided', () => { + let licence + let region + let returnVersion + let returnRequirement + let notifierStub + + before(async () => { + region = RegionHelper.select() + licence = await LicenceHelper.add({ regionId: region.id }) + returnVersion = await ReturnVersionHelper.add({ licenceId: licence.id }) + returnRequirement = await ReturnRequirementHelper.add({ returnVersionId: returnVersion.id }) + await ReturnRequirementPointHelper.add({ returnRequirementId: returnRequirement.id }) + await ReturnRequirementPurposeHelper.add({ returnRequirementId: returnRequirement.id }) + + // BaseRequest depends on the GlobalNotifier to have been set. + // This happens in app/plugins/global-notifier.plugin.js when the app starts up and the plugin is registered. + // As we're not creating an instance of Hapi server in this test we recreate the condition by setting + // it directly with our own stub + notifierStub = { omg: Sinon.stub(), omfg: Sinon.stub() } + global.GlobalNotifier = notifierStub + }) + + it('can successfully save an return log in the database', async () => { + await ProcessReturnLogsService.go('all-year', licence.licenceRef) + + const result = await ReturnLogModel.query().where('licenceRef', licence.licenceRef) + + expect(result.length).to.equal(1) + expect(result[0].dueDate).to.equal(new Date(allYearDueDate)) + expect(result[0].endDate).to.equal(new Date(allYearEndDate)) + expect(result[0].id).to.equal(`v1:${region.naldRegionId}:${licence.licenceRef}:${returnRequirement.legacyId}:${allYearStartDate}:${allYearEndDate}`) + expect(result[0].licenceRef).to.equal(licence.licenceRef) + expect(result[0].returnsFrequency).to.equal('day') + expect(result[0].startDate).to.equal(new Date(allYearStartDate)) + expect(result[0].status).to.equal('due') + expect(result[0].source).to.equal('WRLS') + }) + }) + + describe('cycle is "all-year" and a licence reference is provided but there is no matching return requirements', () => { + it('will not save anything in the database', async () => { + await ProcessReturnLogsService.go('all-year', 'testReference') + + const result = await ReturnLogModel.query().where('licenceRef', 'testReference') + + expect(result.length).to.equal(0) + }) + }) +}) diff --git a/test/services/return-requirements/fetch-return-version.service.test.js b/test/services/return-requirements/fetch-return-version.service.test.js index d5b34e8a19..fb433d2b0e 100644 --- a/test/services/return-requirements/fetch-return-version.service.test.js +++ b/test/services/return-requirements/fetch-return-version.service.test.js @@ -71,7 +71,7 @@ describe('Return Requirements - Fetch Return Version service', () => { legacyId: returnRequirementsOne.legacyId, returnRequirementPoints: [ { - description: null, + description: 'Point description', id: returnRequirementsOne.returnRequirementPoints[0].id, ngr1: returnRequirementsOne.returnRequirementPoints[0].ngr1, ngr2: null, @@ -107,7 +107,7 @@ describe('Return Requirements - Fetch Return Version service', () => { legacyId: returnRequirementsTwo.legacyId, returnRequirementPoints: [ { - description: null, + description: 'Point description', id: returnRequirementsTwo.returnRequirementPoints[0].id, ngr1: returnRequirementsTwo.returnRequirementPoints[0].ngr1, ngr2: null, diff --git a/test/support/helpers/return-log.helper.js b/test/support/helpers/return-log.helper.js index d413b8c3ad..82572d0228 100644 --- a/test/support/helpers/return-log.helper.js +++ b/test/support/helpers/return-log.helper.js @@ -61,7 +61,26 @@ function defaults (data = {}) { dueDate: new Date('2023-04-28'), endDate: new Date('2023-03-31'), licenceRef, - metadata: {}, + metadata: { + description: 'BOREHOLE AT AVALON', + isCurrent: true, + isFinal: false, + isSummer: false, + isTwoPartTariff: false, + isUpload: false, + nald: { + regionCode: 9, + areaCode: 'ARCA', + formatId: returnReference, + periodStartDay: 1, + periodStartMonth: 4, + periodEndDay: 28, + periodEndMonth: 4 + }, + points: [], + purposes: [], + version: 1 + }, receivedDate, returnReference, returnsFrequency: 'month', diff --git a/test/support/helpers/return-requirement-point.helper.js b/test/support/helpers/return-requirement-point.helper.js index d19cc08903..1ae6397078 100644 --- a/test/support/helpers/return-requirement-point.helper.js +++ b/test/support/helpers/return-requirement-point.helper.js @@ -45,6 +45,7 @@ function defaults (data = {}) { const ngr1 = data.ngr1 ? data.ngr1 : generateNationalGridReference() const defaults = { + description: 'Point description', externalId: `9:${randomInteger(100, 99999)}:${naldPointId}`, naldPointId, ngr1, diff --git a/test/support/helpers/return-requirement-purpose.helper.js b/test/support/helpers/return-requirement-purpose.helper.js index 4f0b7ea98b..fecd8438ac 100644 --- a/test/support/helpers/return-requirement-purpose.helper.js +++ b/test/support/helpers/return-requirement-purpose.helper.js @@ -55,6 +55,7 @@ function defaults (data = {}) { externalId, purposeId: purpose.id, primaryPurposeId: primaryPurpose.id, + alias: 'Purpose alias', secondaryPurposeId: secondaryPurpose.id, returnRequirementId: generateUUID() }