From fbf8833803e03801477a2dc9ac447aa3ff3eb27e Mon Sep 17 00:00:00 2001 From: Robert Parkinson Date: Mon, 5 Feb 2024 10:31:57 +0000 Subject: [PATCH] Display purposes in view licence page summary tab (#682) * Display purposes in view licence page summary tab https://eaflood.atlassian.net/browse/WATER-4326 Migrating the licence summary page from the water-abstraction-ui to the water-abstraction-system. This ticket is for adding the purposes to the licence summary tab --- app/models/licence-version-purpose.model.js | 31 ++++++++ app/models/licence-version.model.js | 12 +++ .../licences/view-licence.presenter.js | 29 ++++++- .../licences/fetch-licence.service.js | 1 + app/views/licences/tabs/summary.njk | 29 +++++-- ...8_create-licence-version-purposes-table.js | 44 +++++++++++ ...49_create-licence-version-purposes-view.js | 36 +++++++++ .../licence-version-purposes.model.test.js | 68 +++++++++++++++++ test/models/licence-version.model.test.js | 37 +++++++++ .../licences/view-licence.presenter.test.js | 72 ++++++++++++++++++ .../licences/fetch-licence.service.test.js | 11 ++- .../licences/view-licence.service.test.js | 1 + .../helpers/licence-version-purpose.helper.js | 75 +++++++++++++++++++ 13 files changed, 437 insertions(+), 9 deletions(-) create mode 100644 app/models/licence-version-purpose.model.js create mode 100644 db/migrations/legacy/20240129145658_create-licence-version-purposes-table.js create mode 100644 db/migrations/public/20240129151549_create-licence-version-purposes-view.js create mode 100644 test/models/licence-version-purposes.model.test.js create mode 100644 test/support/helpers/licence-version-purpose.helper.js diff --git a/app/models/licence-version-purpose.model.js b/app/models/licence-version-purpose.model.js new file mode 100644 index 0000000000..153fd967ee --- /dev/null +++ b/app/models/licence-version-purpose.model.js @@ -0,0 +1,31 @@ +'use strict' + +/** + * Model for LicenceVersionPurposes (water.licence_version_purposes) + * @module LicenceVersionPurposes + */ + +const { Model } = require('objection') + +const BaseModel = require('./base.model.js') + +class LicenceVersionPurposes extends BaseModel { + static get tableName () { + return 'licenceVersionPurposes' + } + + static get relationMappings () { + return { + licenceVersion: { + relation: Model.BelongsToOneRelation, + modelClass: 'licence-version.model', + join: { + from: 'licenceVersionPurposes.licenceVersionId', + to: 'licenceVersions.id' + } + } + } + } +} + +module.exports = LicenceVersionPurposes diff --git a/app/models/licence-version.model.js b/app/models/licence-version.model.js index ad212a3857..66498e9148 100644 --- a/app/models/licence-version.model.js +++ b/app/models/licence-version.model.js @@ -23,6 +23,18 @@ class LicenceVersionModel extends BaseModel { from: 'licenceVersions.licenceId', to: 'licences.id' } + }, + purposes: { + relation: Model.ManyToManyRelation, + modelClass: 'purpose.model', + join: { + from: 'licenceVersions.id', + through: { + from: 'licenceVersionPurposes.licenceVersionId', + to: 'licenceVersionPurposes.purposeId' + }, + to: 'purposes.id' + } } } } diff --git a/app/presenters/licences/view-licence.presenter.js b/app/presenters/licences/view-licence.presenter.js index cd759f9527..0273dd8649 100644 --- a/app/presenters/licences/view-licence.presenter.js +++ b/app/presenters/licences/view-licence.presenter.js @@ -15,7 +15,17 @@ const { formatLongDate } = require('../base.presenter.js') * @returns {Object} The data formatted for the view template */ function go (licence) { - const { ends, expiredDate, id, licenceDocumentHeader, licenceHolder, licenceRef, region, startDate } = licence + const { + ends, + expiredDate, + id, + licenceDocumentHeader, + licenceHolder, + licenceRef, + licenceVersions, + region, + startDate + } = licence return { id, @@ -24,6 +34,7 @@ function go (licence) { licenceHolder: _generateLicenceHolder(licenceHolder), licenceRef, pageTitle: `Licence ${licenceRef}`, + purposes: _generatePurposes(licenceVersions), region: region.displayName, startDate: formatLongDate(startDate), warning: _generateWarningMessage(ends) @@ -46,6 +57,22 @@ function _generateLicenceHolder (licenceHolder) { return licenceHolder } +function _generatePurposes (licenceVersions) { + if (!licenceVersions || licenceVersions.length === 0 || licenceVersions[0]?.purposes.length === 0) { + return null + } + const allPurposeDescriptions = licenceVersions[0].purposes.map((item) => { + return item.description + }) + + const uniquePurposes = [...new Set(allPurposeDescriptions)] + + return { + caption: uniquePurposes.length === 1 ? 'Purpose' : 'Purposes', + data: uniquePurposes + } +} + function _generateWarningMessage (ends) { if (!ends) { return null diff --git a/app/services/licences/fetch-licence.service.js b/app/services/licences/fetch-licence.service.js index 81bb6c7291..d93bd9a0cf 100644 --- a/app/services/licences/fetch-licence.service.js +++ b/app/services/licences/fetch-licence.service.js @@ -55,6 +55,7 @@ async function _fetchLicence (id) { 'id' ]) }) + .withGraphFetched('licenceVersions.[purposes]') .modify('licenceHolder') return result diff --git a/app/views/licences/tabs/summary.njk b/app/views/licences/tabs/summary.njk index 4469ca9c2b..808fe99ae9 100644 --- a/app/views/licences/tabs/summary.njk +++ b/app/views/licences/tabs/summary.njk @@ -4,7 +4,7 @@

Summary

-
+
Licence Holder
{{ licenceHolder }} @@ -23,13 +23,28 @@ {% endif %}
-
- Billing region -
-
- {{ region }} -
+
Billing region
+
{{ region }}
+ + {% if purposes %} +
+
{{ purposes.caption }}
+
+ {% if purposes.data.length > 5 %} + You have {{ purposes.data.length }} purposes + {% elseif purposes.data.length > 1 %} +
    + {% for purpose in purposes.data %} +
  • {{ purpose }}
  • + {% endfor %} +
+ {% else %} + {{ purposes.data[0] }} + {% endif %} +
+
+ {% endif %}
diff --git a/db/migrations/legacy/20240129145658_create-licence-version-purposes-table.js b/db/migrations/legacy/20240129145658_create-licence-version-purposes-table.js new file mode 100644 index 0000000000..b40ce42105 --- /dev/null +++ b/db/migrations/legacy/20240129145658_create-licence-version-purposes-table.js @@ -0,0 +1,44 @@ +'use strict' + +const tableName = 'licence_version_purposes' + +exports.up = function (knex) { + return knex + .schema + .withSchema('water') + .createTable(tableName, (table) => { + // Primary Key + table.uuid('licence_version_purpose_id').primary().defaultTo(knex.raw('gen_random_uuid()')) + + // Data + table.uuid('licence_version_id').notNullable() + table.uuid('purpose_primary_id').notNullable() + table.uuid('purpose_secondary_id').notNullable() + table.uuid('purpose_use_id').notNullable() + table.integer('abstraction_period_start_day').notNullable() + table.integer('abstraction_period_start_month').notNullable() + table.integer('abstraction_period_end_day').notNullable() + table.integer('abstraction_period_end_month').notNullable() + table.date('time_limited_start_date') + table.date('time_limited_end_date') + table.text('notes') + table.decimal('annual_quantity') + table.string('external_id') + table.boolean('is_test') + + // Legacy timestamps + // NOTE: They are not automatically set + table.dateTime('date_created').notNullable() + table.dateTime('date_updated').notNullable() + + // Constraints + table.unique(['external_id'], { useConstraint: true }) + }) +} + +exports.down = function (knex) { + return knex + .schema + .withSchema('water') + .dropTableIfExists(tableName) +} diff --git a/db/migrations/public/20240129151549_create-licence-version-purposes-view.js b/db/migrations/public/20240129151549_create-licence-version-purposes-view.js new file mode 100644 index 0000000000..6cf68bbe8c --- /dev/null +++ b/db/migrations/public/20240129151549_create-licence-version-purposes-view.js @@ -0,0 +1,36 @@ +'use strict' + +const viewName = 'licence_version_purposes' + +exports.up = function (knex) { + return knex + .schema + .createView(viewName, (view) => { + // NOTE: We have commented out unused columns from the source table + view.as(knex('licence_version_purposes').withSchema('water').select([ + 'licence_version_purpose_id AS id', + 'licence_version_id', + 'purpose_primary_id', + 'purpose_secondary_id', + 'purpose_use_id AS purpose_id', + 'abstraction_period_start_day', + 'abstraction_period_start_month', + 'abstraction_period_end_day', + 'abstraction_period_end_month', + 'time_limited_start_date', + 'time_limited_end_date', + 'notes', + 'annual_quantity', + 'external_id', + // 'is_test ', + 'date_created AS created_at', + 'date_updated AS updated_at' + ])) + }) +} + +exports.down = function (knex) { + return knex + .schema + .dropViewIfExists(viewName) +} diff --git a/test/models/licence-version-purposes.model.test.js b/test/models/licence-version-purposes.model.test.js new file mode 100644 index 0000000000..13db82863b --- /dev/null +++ b/test/models/licence-version-purposes.model.test.js @@ -0,0 +1,68 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') + +const { describe, it, beforeEach } = exports.lab = Lab.script() +const { expect } = Code + +// Test helpers +const DatabaseHelper = require('../support/helpers/database.helper.js') +const LicenceVersionModel = require('../../app/models/licence-version.model.js') +const LicenceVersionHelper = require('../support/helpers/licence-version.helper.js') +const LicenceVersionPurposesHelper = require('../support/helpers/licence-version-purpose.helper.js') + +// Thing under test +const LicenceVersionPurposeModel = require('../../app/models/licence-version-purpose.model.js') + +describe('Licence Version Purposes model', () => { + let testRecord + + beforeEach(async () => { + await DatabaseHelper.clean() + + testRecord = await LicenceVersionPurposesHelper.add() + }) + + describe('Basic query', () => { + it('can successfully run a basic query', async () => { + const result = await LicenceVersionPurposeModel.query().findById(testRecord.id) + + expect(result).to.be.an.instanceOf(LicenceVersionPurposeModel) + expect(result.id).to.equal(testRecord.id) + }) + }) + + describe('Relationships', () => { + describe('when linking to licence version purposes', () => { + let testLicenceVersion + + beforeEach(async () => { + testLicenceVersion = await LicenceVersionHelper.add() + + const { id } = testLicenceVersion + testRecord = await LicenceVersionPurposesHelper.add({ licenceVersionId: id }) + }) + + it('can successfully run a related query', async () => { + const query = await LicenceVersionPurposeModel.query() + .innerJoinRelated('licenceVersion') + + expect(query).to.exist() + }) + + it('can eager load the licence version', async () => { + const result = await LicenceVersionPurposeModel.query() + .findById(testRecord.id) + .withGraphFetched('licenceVersion') + + expect(result).to.be.instanceOf(LicenceVersionPurposeModel) + expect(result.id).to.equal(testRecord.id) + + expect(result.licenceVersion).to.be.an.instanceOf(LicenceVersionModel) + expect(result.licenceVersion.id).to.equal(testLicenceVersion.id) + }) + }) + }) +}) diff --git a/test/models/licence-version.model.test.js b/test/models/licence-version.model.test.js index d07ac4b1b7..d3ccba1d29 100644 --- a/test/models/licence-version.model.test.js +++ b/test/models/licence-version.model.test.js @@ -12,6 +12,9 @@ const DatabaseHelper = require('../support/helpers/database.helper.js') const LicenceHelper = require('../support/helpers/licence.helper.js') const LicenceModel = require('../../app/models/licence.model.js') const LicenceVersionHelper = require('../support/helpers/licence-version.helper.js') +const LicenceVersionPurposesHelper = require('../support/helpers/licence-version-purpose.helper.js') +const PurposeHelper = require('../support/helpers/purpose.helper.js') +const PurposeModel = require('../../app/models/purpose.model.js') // Thing under test const LicenceVersionModel = require('../../app/models/licence-version.model.js') @@ -64,5 +67,39 @@ describe('Licence Version model', () => { expect(result.licence).to.equal(testLicence) }) }) + + describe('when linking through licence version purposes to purposes', () => { + let purpose + + beforeEach(async () => { + testRecord = await LicenceVersionHelper.add() + purpose = await PurposeHelper.add() + + const { id } = testRecord + await LicenceVersionPurposesHelper.add({ + licenceVersionId: id, + purposeId: purpose.id + }) + }) + + it('can successfully run a related query', async () => { + const query = await LicenceVersionModel.query() + .innerJoinRelated('purposes') + + expect(query).to.exist() + }) + + it('can eager load the purposes', async () => { + const result = await LicenceVersionModel.query() + .findById(testRecord.id) + .withGraphFetched('purposes') + + expect(result).to.be.instanceOf(LicenceVersionModel) + expect(result.id).to.equal(testRecord.id) + + expect(result.purposes[0]).to.be.an.instanceOf(PurposeModel) + expect(result.purposes).to.equal([purpose]) + }) + }) }) }) diff --git a/test/presenters/licences/view-licence.presenter.test.js b/test/presenters/licences/view-licence.presenter.test.js index 56fe0e0739..fe3198cf63 100644 --- a/test/presenters/licences/view-licence.presenter.test.js +++ b/test/presenters/licences/view-licence.presenter.test.js @@ -37,6 +37,7 @@ describe('View Licence presenter', () => { licenceHolder: 'Unregistered licence', licenceRef: '01/123', pageTitle: 'Licence 01/123', + purposes: null, region: 'Narnia', startDate: '1 April 2019', warning: null @@ -162,5 +163,76 @@ describe('View Licence presenter', () => { }) }) }) + + describe("the 'purposes' property", () => { + describe('when there are no licenceVersions', () => { + it('returns null', () => { + const result = ViewLicencePresenter.go(licence) + + expect(result.purposes).to.equal(null) + }) + }) + + describe('when the licenceVersions has one entry', () => { + beforeEach(() => { + licence.licenceVersions = [{ + purposes: [{ + description: 'Spray Irrigation - Storage' + }] + }] + }) + + it('returns an object with a caption and an array with one entry', () => { + const result = ViewLicencePresenter.go(licence) + + expect(result.purposes).to.equal({ + caption: 'Purpose', + data: ['Spray Irrigation - Storage'] + }) + }) + }) + + describe('when the licenceVersions has more than one entry of the same type', () => { + beforeEach(() => { + licence.licenceVersions = [{ + purposes: [{ + description: 'Spray Irrigation - Storage' + }, { + description: 'Spray Irrigation - Storage' + }] + }] + }) + + it('returns an object with a caption and an array with one entry', () => { + const result = ViewLicencePresenter.go(licence) + + expect(result.purposes).to.equal({ + caption: 'Purpose', + data: ['Spray Irrigation - Storage'] + }) + }) + }) + + describe('when the licenceVersions has more than one entry of different types', () => { + beforeEach(() => { + licence.licenceVersions = [{ + purposes: [{ + description: 'Spray Irrigation - Storage' + }, { + description: 'Make-Up Or Top Up Water' + }] + }] + }) + + it('returns an object with a caption and an array with two entries', () => { + const result = ViewLicencePresenter.go(licence) + + expect(result.purposes).to.equal({ + caption: 'Purposes', + data: ['Spray Irrigation - Storage', 'Make-Up Or Top Up Water'] + }) + }) + }) + }) }) }) diff --git a/test/services/licences/fetch-licence.service.test.js b/test/services/licences/fetch-licence.service.test.js index 01e5c7c643..ac3a8fac20 100644 --- a/test/services/licences/fetch-licence.service.test.js +++ b/test/services/licences/fetch-licence.service.test.js @@ -12,6 +12,8 @@ const DatabaseHelper = require('../../support/helpers/database.helper.js') const LicenceHelper = require('../../support/helpers/licence.helper.js') const LicenceHolderSeeder = require('../../support/seeders/licence-holder.seeder.js') const LicenceVersionHelper = require('../../support/helpers/licence-version.helper.js') +const LicenceVersionPurposeHelper = require('../../support/helpers/licence-version-purpose.helper.js') +const PurposeHelper = require('../../support/helpers/purpose.helper.js') const RegionHelper = require('../../support/helpers/region.helper.js') // Thing under test @@ -39,10 +41,16 @@ describe('Fetch licence service', () => { await LicenceVersionHelper.add({ licenceId: licence.id, startDate: new Date('2021-10-11'), status: 'superseded' }) - await LicenceVersionHelper.add({ + const licenceVersion = await LicenceVersionHelper.add({ licenceId: licence.id, startDate: new Date('2022-05-01') }) + const purpose = await PurposeHelper.add() + await LicenceVersionPurposeHelper.add({ + licenceVersionId: licenceVersion.id, + purposeId: purpose.id + }) + // Create a licence holder for the licence with the default name 'Licence Holder Ltd' await LicenceHolderSeeder.seed(licence.licenceRef) }) @@ -57,6 +65,7 @@ describe('Fetch licence service', () => { expect(result.revokedDate).to.equal(null) expect(result.licenceRef).to.equal(licence.licenceRef) expect(result.licenceHolder).to.equal('Licence Holder Ltd') + expect(result.licenceVersions[1].purposes[0].description).to.equal('Spray Irrigation - Storage') }) }) }) diff --git a/test/services/licences/view-licence.service.test.js b/test/services/licences/view-licence.service.test.js index 978b766f03..67b98903dc 100644 --- a/test/services/licences/view-licence.service.test.js +++ b/test/services/licences/view-licence.service.test.js @@ -42,6 +42,7 @@ describe('View Licence service', () => { licenceHolder: 'Unregistered licence', licenceRef: '01/130/R01', pageTitle: 'Licence 01/130/R01', + purposes: null, region: 'South West', startDate: '7 March 2013', warning: null diff --git a/test/support/helpers/licence-version-purpose.helper.js b/test/support/helpers/licence-version-purpose.helper.js new file mode 100644 index 0000000000..d0993d1d4e --- /dev/null +++ b/test/support/helpers/licence-version-purpose.helper.js @@ -0,0 +1,75 @@ +'use strict' + +/** + * @module LicenceVersionPurposesHelper + */ + +const { generateUUID, timestampForPostgres } = require('../../../app/lib/general.lib.js') +const LicenceVersionPurposesModel = require('../../../app/models/licence-version-purpose.model.js') +const { randomInteger } = require('./general.helper.js') + +/** + * Add a new licence version purpose + * + * If no `data` is provided, default values will be used. These are + * + * - `licenceVersionPurposeId` - [random UUID] + * - `licenceVersionId` - [random UUID] + * - `purposePrimaryId` - [random UUID] + * - `purposeSecondaryId` - [random UUID] + * - `purposeUseId` - [random UUID] + * - `abstractionPeriodStartDay` - [1] + * - `abstractionPeriodStartMonth` - [1] + * - `abstractionPeriodEndDay` - [31] + * - `abstractionPeriodEndMonth` - [3] + * - `dateCreated` - new Date() + * - `dateUpdated` - new Date() + * + * @param {Object} [data] Any data you want to use instead of the defaults used here or in the database + * + * @returns {module:LicenceVersionPurposesModel} The instance of the newly created record + */ +async function add (data = {}) { + const insertData = defaults(data) + + return LicenceVersionPurposesModel.query() + .insert({ ...insertData }) + .returning('*') +} + +/** + * Returns the defaults used + * + * It will override or append to them any data provided. Mainly used by the `add()` method, we make it available + * for use in tests to avoid having to duplicate values. + * + * @param {Object} [data] Any data you want to use instead of the defaults used here or in the database + */ +function defaults (data = {}) { + const timestamp = timestampForPostgres() + + const defaults = { + abstractionPeriodStartDay: 1, + abstractionPeriodStartMonth: 1, + abstractionPeriodEndDay: 31, + abstractionPeriodEndMonth: 3, + annualQuantity: 1000, + createdAt: timestamp, + updatedAt: timestamp, + externalId: `9:${randomInteger(10000, 99999)}:1:0`, + licenceVersionId: generateUUID(), + purposePrimaryId: generateUUID(), + purposeSecondaryId: generateUUID(), + purposeId: generateUUID() + } + + return { + ...defaults, + ...data + } +} + +module.exports = { + add, + defaults +}