diff --git a/app/models/return-log.model.js b/app/models/return-log.model.js new file mode 100644 index 0000000000..ba724845fe --- /dev/null +++ b/app/models/return-log.model.js @@ -0,0 +1,38 @@ +'use strict' + +/** + * Model for return_logs + * @module ReturnLogModel + */ + +const { Model } = require('objection') + +const BaseModel = require('./base.model.js') + +class ReturnLogModel extends BaseModel { + static get tableName () { + return 'returnLogs' + } + + // Defining which fields contain json allows us to insert an object without needing to stringify it first + static get jsonAttributes () { + return [ + 'metadata' + ] + } + + static get relationMappings () { + return { + returnSubmissions: { + relation: Model.HasManyRelation, + modelClass: 'return-submission.model', + join: { + from: 'returnLogs.id', + to: 'returnSubmissions.returnLogId' + } + } + } + } +} + +module.exports = ReturnLogModel diff --git a/app/models/return-submission-line.model.js b/app/models/return-submission-line.model.js new file mode 100644 index 0000000000..5f8bb41116 --- /dev/null +++ b/app/models/return-submission-line.model.js @@ -0,0 +1,31 @@ +'use strict' + +/** + * Model for return_submission_lines + * @module ReturnSubmissionLineModel + */ + +const { Model } = require('objection') + +const BaseModel = require('./base.model.js') + +class ReturnSubmissionLineModel extends BaseModel { + static get tableName () { + return 'returnSubmissionLines' + } + + static get relationMappings () { + return { + returnSubmission: { + relation: Model.BelongsToOneRelation, + modelClass: 'return-submission.model', + join: { + from: 'returnSubmissionLines.returnSubmissionId', + to: 'returnSubmissions.id' + } + } + } + } +} + +module.exports = ReturnSubmissionLineModel diff --git a/app/models/return-submission.model.js b/app/models/return-submission.model.js new file mode 100644 index 0000000000..d09bb3a205 --- /dev/null +++ b/app/models/return-submission.model.js @@ -0,0 +1,46 @@ +'use strict' + +/** + * Model for return_submissions + * @module ReturnSubmissionModel + */ + +const { Model } = require('objection') + +const BaseModel = require('./base.model.js') + +class ReturnSubmissionModel extends BaseModel { + static get tableName () { + return 'returnSubmissions' + } + + // Defining which fields contain json allows us to insert an object without needing to stringify it first + static get jsonAttributes () { + return [ + 'metadata' + ] + } + + static get relationMappings () { + return { + returnLog: { + relation: Model.BelongsToOneRelation, + modelClass: 'return-log.model', + join: { + from: 'returnSubmissions.returnLogId', + to: 'returnLogs.id' + } + }, + returnSubmissionLines: { + relation: Model.HasManyRelation, + modelClass: 'return-submission-line.model', + join: { + from: 'returnSubmissions.id', + to: 'returnSubmissionLines.returnSubmissionId' + } + } + } + } +} + +module.exports = ReturnSubmissionModel diff --git a/db/migrations/legacy/20231122181918_add-defaults-to-returns-table.js b/db/migrations/legacy/20231122181918_add-defaults-to-returns-table.js new file mode 100644 index 0000000000..3d73910bfd --- /dev/null +++ b/db/migrations/legacy/20231122181918_add-defaults-to-returns-table.js @@ -0,0 +1,23 @@ +'use strict' + +const tableName = 'returns' + +exports.up = function (knex) { + return knex + .schema + .withSchema('returns') + .alterTable(tableName, (table) => { + table.string('regime').notNullable().defaultTo('water').alter() + table.string('licence_type').notNullable().defaultTo('abstraction').alter() + }) +} + +exports.down = async function (knex) { + return knex + .schema + .withSchema('returns') + .alterTable(tableName, (table) => { + table.string('regime').notNullable().alter() + table.string('licence_type').notNullable().alter() + }) +} diff --git a/db/migrations/legacy/20231122183613_add-defaults-to-lines-table.js b/db/migrations/legacy/20231122183613_add-defaults-to-lines-table.js new file mode 100644 index 0000000000..020e336dc4 --- /dev/null +++ b/db/migrations/legacy/20231122183613_add-defaults-to-lines-table.js @@ -0,0 +1,23 @@ +'use strict' + +const tableName = 'lines' + +exports.up = function (knex) { + return knex + .schema + .withSchema('returns') + .alterTable(tableName, (table) => { + table.string('substance').notNullable().defaultTo('water').alter() + table.string('unit').notNullable().defaultTo('m³').alter() + }) +} + +exports.down = async function (knex) { + return knex + .schema + .withSchema('returns') + .alterTable(tableName, (table) => { + table.string('substance').notNullable().alter() + table.string('unit').notNullable().alter() + }) +} diff --git a/db/migrations/public/20231120122556_create-form-logs-view.js b/db/migrations/public/20231120122556_create-return-logs-view.js similarity index 93% rename from db/migrations/public/20231120122556_create-form-logs-view.js rename to db/migrations/public/20231120122556_create-return-logs-view.js index 4d4dc2123a..5cd038552e 100644 --- a/db/migrations/public/20231120122556_create-form-logs-view.js +++ b/db/migrations/public/20231120122556_create-return-logs-view.js @@ -1,6 +1,6 @@ 'use strict' -const viewName = 'form_logs' +const viewName = 'return_logs' exports.up = function (knex) { return knex diff --git a/db/migrations/public/20231120140419_create-return-submissions-view.js b/db/migrations/public/20231120140419_create-return-submissions-view.js index ac3f5e19ca..1a40066949 100644 --- a/db/migrations/public/20231120140419_create-return-submissions-view.js +++ b/db/migrations/public/20231120140419_create-return-submissions-view.js @@ -8,7 +8,7 @@ exports.up = function (knex) { .createView(viewName, (view) => { view.as(knex('versions').withSchema('returns').select([ 'versions.version_id AS id', - 'versions.return_id as form_log_id', + 'versions.return_id as return_log_id', 'versions.user_id', 'versions.user_type', 'versions.version_number as version', diff --git a/test/models/return-log.model.test.js b/test/models/return-log.model.test.js new file mode 100644 index 0000000000..b9bab60eaf --- /dev/null +++ b/test/models/return-log.model.test.js @@ -0,0 +1,74 @@ +'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 ReturnLogHelper = require('../support/helpers/return-log.helper.js') +const ReturnSubmissionHelper = require('../support/helpers/return-submission.helper.js') +const ReturnSubmissionModel = require('../../app/models/return-submission.model.js') + +// Thing under test +const ReturnLogModel = require('../../app/models/return-log.model.js') + +describe('Return Log model', () => { + let testRecord + + beforeEach(async () => { + await DatabaseHelper.clean() + + testRecord = await ReturnLogHelper.add() + }) + + describe('Basic query', () => { + it('can successfully run a basic query', async () => { + const result = await ReturnLogModel.query().findById(testRecord.id) + + expect(result).to.be.an.instanceOf(ReturnLogModel) + expect(result.id).to.equal(testRecord.id) + }) + }) + + describe('Relationships', () => { + describe('when linking to return submissions', () => { + let returnSubmissions + + beforeEach(async () => { + const { id: returnLogId } = testRecord + + returnSubmissions = [] + for (let i = 0; i < 2; i++) { + const version = i + const returnSubmission = await ReturnSubmissionHelper.add({ returnLogId, version }) + returnSubmissions.push(returnSubmission) + } + }) + + it('can successfully run a related query', async () => { + const query = await ReturnLogModel.query() + .innerJoinRelated('returnSubmissions') + + expect(query).to.exist() + }) + + it('can eager load the return submissions', async () => { + const result = await ReturnLogModel.query() + .findById(testRecord.id) + .withGraphFetched('returnSubmissions') + + expect(result).to.be.instanceOf(ReturnLogModel) + expect(result.id).to.equal(testRecord.id) + + expect(result.returnSubmissions).to.be.an.array() + expect(result.returnSubmissions[0]).to.be.an.instanceOf(ReturnSubmissionModel) + expect(result.returnSubmissions).to.include(returnSubmissions[0]) + expect(result.returnSubmissions).to.include(returnSubmissions[1]) + }) + }) + }) +}) diff --git a/test/models/return-submission-line.model.test.js b/test/models/return-submission-line.model.test.js new file mode 100644 index 0000000000..24158ab399 --- /dev/null +++ b/test/models/return-submission-line.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 ReturnSubmissionLineHelper = require('../support/helpers/return-submission-line.helper.js') +const ReturnSubmissionHelper = require('../support/helpers/return-submission.helper.js') +const ReturnSubmissionModel = require('../../app/models/return-submission.model.js') + +// Thing under test +const ReturnSubmissionLineModel = require('../../app/models/return-submission-line.model.js') + +describe('Return Submission Line model', () => { + let testRecord + + beforeEach(async () => { + await DatabaseHelper.clean() + }) + + describe('Basic query', () => { + beforeEach(async () => { + testRecord = await ReturnSubmissionLineHelper.add() + }) + + it('can successfully run a basic query', async () => { + const result = await ReturnSubmissionLineModel.query().findById(testRecord.id) + + expect(result).to.be.an.instanceOf(ReturnSubmissionLineModel) + expect(result.id).to.equal(testRecord.id) + }) + }) + + describe('Relationships', () => { + describe('when linking to return submission', () => { + let testReturnSubmission + + beforeEach(async () => { + testReturnSubmission = await ReturnSubmissionHelper.add() + testRecord = await ReturnSubmissionLineHelper.add({ returnSubmissionId: testReturnSubmission.id }) + }) + + it('can successfully run a related query', async () => { + const query = await ReturnSubmissionLineModel.query() + .innerJoinRelated('returnSubmission') + + expect(query).to.exist() + }) + + it('can eager load the return submission', async () => { + const result = await ReturnSubmissionLineModel.query() + .findById(testRecord.id) + .withGraphFetched('returnSubmission') + + expect(result).to.be.instanceOf(ReturnSubmissionLineModel) + expect(result.id).to.equal(testRecord.id) + + expect(result.returnSubmission).to.be.an.instanceOf(ReturnSubmissionModel) + expect(result.returnSubmission).to.equal(testReturnSubmission) + }) + }) + }) +}) diff --git a/test/models/return-submission.model.test.js b/test/models/return-submission.model.test.js new file mode 100644 index 0000000000..7fd45e1aae --- /dev/null +++ b/test/models/return-submission.model.test.js @@ -0,0 +1,112 @@ +'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 ReturnSubmissionLineHelper = require('../support/helpers/return-submission-line.helper.js') +const ReturnSubmissionLineModel = require('../../app/models/return-submission-line.model.js') +const ReturnLogHelper = require('../support/helpers/return-log.helper.js') +const ReturnLogModel = require('../../app/models/return-log.model.js') +const ReturnSubmissionHelper = require('../support/helpers/return-submission.helper.js') + +// Thing under test +const ReturnSubmissionModel = require('../../app/models/return-submission.model.js') + +describe('Return Submission model', () => { + let testRecord + + beforeEach(async () => { + await DatabaseHelper.clean() + }) + + describe('Basic query', () => { + beforeEach(async () => { + testRecord = await ReturnSubmissionHelper.add() + }) + + it('can successfully run a basic query', async () => { + const result = await ReturnSubmissionModel.query().findById(testRecord.id) + + expect(result).to.be.an.instanceOf(ReturnSubmissionModel) + expect(result.id).to.equal(testRecord.id) + }) + }) + + describe('Relationships', () => { + describe('when linking to return log', () => { + let testReturnLog + + beforeEach(async () => { + testReturnLog = await ReturnLogHelper.add() + testRecord = await ReturnSubmissionHelper.add({ returnLogId: testReturnLog.id }) + }) + + it('can successfully run a related query', async () => { + const query = await ReturnSubmissionModel.query() + .innerJoinRelated('returnLog') + + expect(query).to.exist() + }) + + it('can eager load the return log', async () => { + const result = await ReturnSubmissionModel.query() + .findById(testRecord.id) + .withGraphFetched('returnLog') + + expect(result).to.be.instanceOf(ReturnSubmissionModel) + expect(result.id).to.equal(testRecord.id) + + expect(result.returnLog).to.be.an.instanceOf(ReturnLogModel) + expect(result.returnLog).to.equal(testReturnLog) + }) + }) + + describe('when linking to return submission lines', () => { + let testLines + + beforeEach(async () => { + testRecord = await ReturnSubmissionHelper.add() + const { id: returnSubmissionId } = testRecord + + testLines = [] + for (let i = 0; i < 2; i++) { + // NOTE: A constraint in the lines table means you cannot have 2 records with the same returnSubmissionId, + // startDate and endDate + const returnSubmissionLine = await ReturnSubmissionLineHelper.add({ + returnSubmissionId, + startDate: new Date(2022, 11, 1 + i), + endDate: new Date(2022, 11, 2 + i) + }) + testLines.push(returnSubmissionLine) + } + }) + + it('can successfully run a related query', async () => { + const query = await ReturnSubmissionModel.query() + .innerJoinRelated('returnSubmissionLines') + + expect(query).to.exist() + }) + + it('can eager load the return submission lines', async () => { + const result = await ReturnSubmissionModel.query() + .findById(testRecord.id) + .withGraphFetched('returnSubmissionLines') + + expect(result).to.be.instanceOf(ReturnSubmissionModel) + expect(result.id).to.equal(testRecord.id) + + expect(result.returnSubmissionLines).to.be.an.array() + expect(result.returnSubmissionLines[0]).to.be.an.instanceOf(ReturnSubmissionLineModel) + expect(result.returnSubmissionLines).to.include(testLines[0]) + expect(result.returnSubmissionLines).to.include(testLines[1]) + }) + }) + }) +}) diff --git a/test/support/helpers/return-log.helper.js b/test/support/helpers/return-log.helper.js new file mode 100644 index 0000000000..43a644bf6b --- /dev/null +++ b/test/support/helpers/return-log.helper.js @@ -0,0 +1,92 @@ +'use strict' + +/** + * @module ReturnLogHelper + */ + +const { generateLicenceRef } = require('./water/licence.helper.js') +const { randomInteger } = require('./general.helper.js') +const ReturnLogModel = require('../../../app/models/return-log.model.js') + +/** + * Add a new return log + * + * If no `data` is provided, default values will be used. These are + * + * - `id` - v1:1:[the generated licenceRef]:[the generated returnRequirement]:2022-04-01:2023-03-31 + * - `licenceRef` - [randomly generated - 1/23/45/76/3672] + * - `startDate` - 2022-04-01 + * - `endDate` - 2023-03-31 + * - `returnsFrequency` - month + * - `status` - completed + * - `metadata` - {} + * - `receivedDate` - 2023-04-12 + * - `returnRequirement` - [randomly generated - 10000321] + * - `dueDate` - 2023-04-28 + * + * @param {Object} [data] Any data you want to use instead of the defaults used here or in the database + * + * @returns {module:ReturnLogModel} The instance of the newly created record + */ +function add (data = {}) { + const insertData = defaults(data) + + return ReturnLogModel.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 licenceRef = data.licenceRef ? data.licenceRef : generateLicenceRef() + const returnRequirement = data.returnRequirement ? data.returnRequirement : randomInteger(10000000, 19999999) + + const defaults = { + id: generateReturnLogId('2022-04-01', '2023-03-31', 1, licenceRef, returnRequirement), + licenceRef, + startDate: new Date('2022-04-01'), + endDate: new Date('2023-03-31'), + returnsFrequency: 'month', + status: 'completed', + metadata: {}, + receivedDate: new Date('2023-04-12'), + returnRequirement, + dueDate: new Date('2023-04-28') + } + + return { + ...defaults, + ...data + } +} + +function generateReturnLogId ( + startDate = '2022-04-01', + endDate = '2023-03-31', + version = 1, + licenceRef, + returnRequirement +) { + if (!licenceRef) { + licenceRef = generateLicenceRef() + } + + if (!returnRequirement) { + returnRequirement = randomInteger(10000000, 19999999) + } + + return `v${version}:1:${licenceRef}:${returnRequirement}:${startDate}:${endDate}` +} + +module.exports = { + add, + defaults, + generateReturnLogId +} diff --git a/test/support/helpers/return-submission-line.helper.js b/test/support/helpers/return-submission-line.helper.js new file mode 100644 index 0000000000..0b1ce07a69 --- /dev/null +++ b/test/support/helpers/return-submission-line.helper.js @@ -0,0 +1,75 @@ +'use strict' + +/** + * @module ReturnSubmissionLineHelper + */ + +const { generateUUID } = require('../../../app/lib/general.lib.js') +const ReturnSubmissionLineModel = require('../../../app/models/return-submission-line.model.js') + +/** + * Add a new return submission line + * + * If no `data` is provided, default values will be used. These are + * + * - `id` - [random UUID] + * - `returnSubmissionId` - [random UUID] + * - `quantity` - 4380 + * - `startDate` - 2021-12-26 + * - `endDate` - 2022-01-01 + * - `timePeriod` - week + * - `createdAt` - 2022-11-16 09:42:11.000 + * - `updatedAt` - null + * - `readingType` - measured + * - `userUnit` - m³ + * + * @param {Object} [data] Any data you want to use instead of the defaults used here or in the database + * + * @returns {module:ReturnSubmissionLineModel} The instance of the newly created record + */ +function add (data = {}) { + const insertData = defaults(data) + + return ReturnSubmissionLineModel.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 defaults = { + id: generateUUID(), + returnSubmissionId: generateUUID(), + quantity: 4380, + startDate: new Date('2021-12-26'), + endDate: new Date('2022-01-01'), + timePeriod: 'week', + // INFO: The lines table does not have a default for the date_created column. But it is set as + // 'not nullable'! So, we need to ensure we set it when creating a new record, something we'll never actually need + // to do because it's a static table. Also, we can't use Date.now() because Javascript returns the time since the + // epoch in milliseconds, whereas a PostgreSQL timestamp field can only hold the seconds since the epoch. Pass it + // an ISO string though ('2023-01-05T08:37:05.575Z') and PostgreSQL can do the conversion + // https://stackoverflow.com/a/61912776/6117745 + createdAt: new Date('2022-11-16 09:42:11.000').toISOString(), + updatedAt: null, + readingType: 'measured', + userUnit: 'm³' + } + + return { + ...defaults, + ...data + } +} + +module.exports = { + add, + defaults +} diff --git a/test/support/helpers/return-submission.helper.js b/test/support/helpers/return-submission.helper.js new file mode 100644 index 0000000000..88a339a8bd --- /dev/null +++ b/test/support/helpers/return-submission.helper.js @@ -0,0 +1,76 @@ +'use strict' + +/** + * @module ReturnSubmissionHelper + */ + +const { generateUUID } = require('../../../app/lib/general.lib.js') +const { generateReturnLogId } = require('./return-log.helper.js') +const ReturnSubmissionModel = require('../../../app/models/return-submission.model.js') + +/** + * Add a new return submission + * + * If no `data` is provided, default values will be used. These are + * + * - `id` - [random UUID] + * - `returnLogId` - [randomly generated - v1:1:03/28/78/0033:10025289:2022-04-01:2023-03-31] + * - `userId` - admin-internal@wrls.gov.uk + * - `userType` - internal + * - `version` - 1 + * - `metadata` - {} + * - `createdAt` - 2022-11-16 09:42:11.000 + * - `updatedAt` - null + * - `nilReturn` - false + * - `current` - true + * + * @param {Object} [data] Any data you want to use instead of the defaults used here or in the database + * + * @returns {module:ReturnSubmissionModel} The instance of the newly created record + */ +function add (data = {}) { + const insertData = defaults(data) + + return ReturnSubmissionModel.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 defaults = { + id: generateUUID(), + returnLogId: generateReturnLogId(), + userId: 'admin-internal@wrls.gov.uk', + userType: 'internal', + version: 1, + metadata: {}, + // INFO: The lines table does not have a default for the date_created column. But it is set as + // 'not nullable'! So, we need to ensure we set it when creating a new record, something we'll never actually need + // to do because it's a static table. Also, we can't use Date.now() because Javascript returns the time since the + // epoch in milliseconds, whereas a PostgreSQL timestamp field can only hold the seconds since the epoch. Pass it + // an ISO string though ('2023-01-05T08:37:05.575Z') and PostgreSQL can do the conversion + // https://stackoverflow.com/a/61912776/6117745 + createdAt: new Date('2022-11-16 09:42:11.000').toISOString(), + updatedAt: null, + nilReturn: false, + current: true + } + + return { + ...defaults, + ...data + } +} + +module.exports = { + add, + defaults +}