diff --git a/app/controllers/data.controller.js b/app/controllers/data.controller.js index b2d35c8593..8fd54304ab 100644 --- a/app/controllers/data.controller.js +++ b/app/controllers/data.controller.js @@ -6,14 +6,36 @@ */ const SeedService = require('../services/data/seed/seed.service.js') +const SubmitDeduplicateService = require('../services/data/deduplicate/submit-deduplicate.service.js') const TearDownService = require('../services/data/tear-down/tear-down.service.js') +async function deduplicate (_request, h) { + return h.view('data/deduplicate.njk', { + pageTitle: 'De-duplicate a licence', + activeNavBar: 'search' + }) +} + async function seed (_request, h) { await SeedService.go() return h.response().code(204) } +async function submitDeduplicate (request, h) { + const pageData = await SubmitDeduplicateService.go(request.payload) + + if (pageData.error) { + return h.view('data/deduplicate.njk', { + pageTitle: 'De-duplicate a licence', + activeNavBar: 'search', + ...pageData + }) + } + + return h.redirect(`/licences?query=${pageData.licenceRef}`) +} + async function tearDown (_request, h) { await TearDownService.go() @@ -21,6 +43,8 @@ async function tearDown (_request, h) { } module.exports = { + deduplicate, seed, + submitDeduplicate, tearDown } diff --git a/app/routes/data.routes.js b/app/routes/data.routes.js index f21ab7bd64..e316985297 100644 --- a/app/routes/data.routes.js +++ b/app/routes/data.routes.js @@ -3,6 +3,32 @@ const DataController = require('../controllers/data.controller.js') const routes = [ + { + method: 'GET', + path: '/data/deduplicate', + handler: DataController.deduplicate, + options: { + auth: { + access: { + scope: ['billing'] + } + }, + description: 'De-duplicate a licence' + } + }, + { + method: 'POST', + path: '/data/deduplicate', + handler: DataController.submitDeduplicate, + options: { + auth: { + access: { + scope: ['billing'] + } + }, + description: 'Submit licence to be de-duped' + } + }, { method: 'POST', path: '/data/seed', diff --git a/app/services/data/deduplicate/de-duplicate-licence.service.js b/app/services/data/deduplicate/de-duplicate-licence.service.js new file mode 100644 index 0000000000..30ddcdde25 --- /dev/null +++ b/app/services/data/deduplicate/de-duplicate-licence.service.js @@ -0,0 +1,175 @@ +'use strict' + +/** + * De-duplicates a licence by removing the record with whitespace and its related records + * @module DeDuplicateService + */ + +const { db } = require('../../../../db/db.js') +const LicenceModel = require('../../../models/licence.model.js') + +/** + * De-duplicates a licence by removing the record with whitespace and its related records + * + * If first matches all licences against the licence reference provided. For example, if '01/120' is the reference we + * will match + * + * - '01/120' + * - 'WA/01/120' + * - '02/01/120/R01' + * - ' 01/120' + * - ' 01/120 ' + * - '01/120 ' + * - '\n01/120' + * - '\n01/120\n' + * - '01/120\n' + * + * We then use a regex to filter out any result that does not include whitespace. These are the invalid licence records + * that need to be removed. We iterate through them (though we expect there to only ever be 1 result) and first delete + * all related records before then deleting the main licence record. + * + * We've opted to use {@link https://knexjs.org/guide/raw.html | Knex.raw()} rather than + * {@link https://vincit.github.io/objection.js/ | Objection.js} because not all the tables referenced have models. So, + * rather than switch between different ways of querying we stick to one for the removal. + * + * @param {string} licenceRef - Reference for the licence with a duplicate record + * + * @returns {Promise} the service does not resolve to a value + */ +async function go (licenceRef) { + const invalidLicences = await _determineInvalidLicences(licenceRef) + + // NOTE: In theory there could be more than one 'bad' licence for the matching licence reference! But mainly we do + // this as a loop because it is an easy way to deal with the fact we have an array. + for (const invalidLicence of invalidLicences) { + const { id: licenceId, licenceRef: invalidLicenceRef } = invalidLicence + + // NOTE: Order is important. Some tables have to be cleared before others. So resist trying to make the calls + // alphabetical! + await _documentRoles(invalidLicenceRef) + await _documents(invalidLicenceRef) + await _documentHeaders(invalidLicenceRef) + await _permitLicences(invalidLicenceRef) + await _returns(invalidLicenceRef) + await _licenceVersionPurposeConditions(licenceId) + await _licenceVersionPurposes(licenceId) + await _licenceVersions(licenceId) + await _returnRequirementPurposes(licenceId) + await _returnRequirements(licenceId) + await _returnVersions(licenceId) + await _licences(licenceId) + } +} + +async function _determineInvalidLicences (licenceRef) { + const licences = await LicenceModel.query() + .select([ + 'id', + 'licenceRef' + ]) + .whereILike('licenceRef', `%${licenceRef}%`) + + // NOTE: Match any string which has whitespace at the start or the end of the string + // ^ asserts position at start of the string + // \s matches any whitespace character + const whitespaceAtStartRegex = /^\s/ + // \s matches any whitespace character + // $ asserts position at end of the string + const whitespaceAtEndRegex = /\s$/ + + const invalidLicences = licences.filter((licence) => { + return whitespaceAtStartRegex.test(licence.licenceRef) || whitespaceAtEndRegex.test(licence.licenceRef) + }) + + return invalidLicences +} + +async function _documentHeaders (licenceRef) { + return db.raw(` + DELETE FROM crm.document_header WHERE system_external_id = ?; + `, licenceRef) +} + +async function _documentRoles (licenceRef) { + return db.raw(` + DELETE FROM "crm_v2"."document_roles" WHERE document_id IN ( + SELECT document_id FROM "crm_v2"."documents" WHERE document_ref = ? + ); + `, licenceRef) +} + +async function _documents (licenceRef) { + return db.raw(` + DELETE FROM "crm_v2"."documents" WHERE document_ref = ?; + `, licenceRef) +} + +async function _licences (licenceId) { + return db.raw(` + DELETE FROM water.licences WHERE licence_id = ?; + `, licenceId) +} + +async function _licenceVersionPurposeConditions (licenceId) { + return db.raw(` + DELETE FROM water.licence_version_purpose_conditions WHERE licence_version_purpose_id IN ( + SELECT licence_version_purpose_id FROM water.licence_version_purposes WHERE licence_version_id IN ( + SELECT licence_version_id FROM water.licence_versions WHERE licence_id = ? + ) + ); + `, licenceId) +} + +async function _licenceVersionPurposes (licenceId) { + return db.raw(` + DELETE FROM water.licence_version_purposes WHERE licence_version_id IN ( + SELECT licence_version_id FROM water.licence_versions WHERE licence_id = ? + ); + `, licenceId) +} + +async function _licenceVersions (licenceId) { + return db.raw(` + DELETE FROM water.licence_versions WHERE licence_id = ?; + `, licenceId) +} + +async function _permitLicences (licenceRef) { + return db.raw(` + DELETE FROM permit.licence WHERE licence_ref = ?; + `, licenceRef) +} + +async function _returnRequirementPurposes (licenceId) { + return db.raw(` + DELETE FROM water.return_requirement_purposes WHERE return_requirement_id IN ( + SELECT return_requirement_id FROM water.return_requirements WHERE return_version_id IN ( + SELECT return_version_id FROM water.return_versions WHERE licence_id = ? + ) + ); + `, licenceId) +} + +async function _returnRequirements (licenceId) { + return db.raw(` + DELETE FROM water.return_requirements WHERE return_version_id IN ( + SELECT return_version_id FROM water.return_versions WHERE licence_id = ? + ); + `, licenceId) +} + +async function _returns (licenceRef) { + return db.raw(` + DELETE FROM "returns"."returns" WHERE licence_ref = ?; + `, licenceRef) +} + +async function _returnVersions (licenceId) { + return db.raw(` + DELETE FROM water.return_versions WHERE licence_id = ?; + `, licenceId) +} + +module.exports = { + go +} diff --git a/app/services/data/deduplicate/submit-deduplicate.service.js b/app/services/data/deduplicate/submit-deduplicate.service.js new file mode 100644 index 0000000000..4f5394de4d --- /dev/null +++ b/app/services/data/deduplicate/submit-deduplicate.service.js @@ -0,0 +1,47 @@ +'use strict' + +/** + * Handles the user submission for the `/data/deduplicate` page + * @module SubmitDeduplicateService + */ + +const DeDuplicateService = require('./de-duplicate-licence.service.js') + +/** + * Handles the user submission for the `/data/deduplicate` page + * + * It will first validate that the user has entered a reference. If they haven't we return an error that can be used by + * the Nunjucks view to let the user know. + * + * If they have we first parse it, removing any whitespace and converting any lowercase characters to uppercase. This + * parsed reference is then passed to the `DeDuplicateService` to do the actual removal of the duplicate licence. + * + * Once complete we return the parsed reference back to the controller. As a handy way of confirming if it worked the + * controller will redirect the user to the search page and have it search for the licence reference. If the tool has + * done its job the page should no longer error. + * + * @returns {Promise} an object containing a parsed version of the licence reference submitted else an error + * message if nothing was entered + */ +async function go (payload) { + const licenceRef = payload['licence-ref'] + + if (!licenceRef || licenceRef.trim() === '') { + return { + error: { + text: 'Enter a licence reference to de-dupe' + } + } + } + + const parsedLicenceRef = licenceRef.trim().toUpperCase() + await DeDuplicateService.go(parsedLicenceRef) + + return { + licenceRef: parsedLicenceRef + } +} + +module.exports = { + go +} diff --git a/app/views/data/deduplicate.njk b/app/views/data/deduplicate.njk new file mode 100644 index 0000000000..1cf0f40f06 --- /dev/null +++ b/app/views/data/deduplicate.njk @@ -0,0 +1,39 @@ +{% extends 'layout.njk' %} +{% from "govuk/components/button/macro.njk" import govukButton %} +{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% from "govuk/components/input/macro.njk" import govukInput %} + +{% block content %} + {% if error %} + {{ govukErrorSummary({ + titleText: "There is a problem", + errorList: [ + { + text: error.text, + href: "#licence-ref-error" + } + ] + }) }} + {% endif %} + +
+
+ {{ govukInput({ + classes: 'govuk-!-width-one-third', + errorMessage: error, + label: { + text: pageTitle, + classes: 'govuk-label--l', + isPageHeading: true + }, + hint: { + text: 'Enter the licence reference for de-duping' + }, + id: 'licence-ref', + name: 'licence-ref' + }) }} + + {{ govukButton({ text: "Remove duplicate" }) }} +
+
+{% endblock %} diff --git a/db/migrations/legacy/20221108007023-water-return-versions.js b/db/migrations/legacy/20221108007023-water-return-versions.js new file mode 100644 index 0000000000..0e29f5f5ac --- /dev/null +++ b/db/migrations/legacy/20221108007023-water-return-versions.js @@ -0,0 +1,35 @@ +'use strict' + +const tableName = 'return_versions' + +exports.up = function (knex) { + return knex + .schema + .withSchema('water') + .createTable(tableName, (table) => { + // Primary Key + table.uuid('return_version_id').primary().defaultTo(knex.raw('gen_random_uuid()')) + + // Data + table.uuid('licence_id').notNullable() + table.integer('version_number').notNullable() + table.date('start_date').notNullable() + table.date('end_date') + table.string('status water').notNullable() + table.string('external_id') + + // Legacy timestamps + table.timestamp('date_created', { useTz: false }).notNullable() + table.timestamp('date_updated', { useTz: false }) + + // Constraints + table.unique(['external_id'], { useConstraint: true }) + }) +} + +exports.down = function (knex) { + return knex + .schema + .withSchema('water') + .dropTableIfExists(tableName) +} diff --git a/db/migrations/legacy/20221108007024-water-return-requirements.js b/db/migrations/legacy/20221108007024-water-return-requirements.js new file mode 100644 index 0000000000..abfe78396b --- /dev/null +++ b/db/migrations/legacy/20221108007024-water-return-requirements.js @@ -0,0 +1,41 @@ +'use strict' + +const tableName = 'return_requirements' + +exports.up = function (knex) { + return knex + .schema + .withSchema('water') + .createTable(tableName, (table) => { + // Primary Key + table.uuid('return_requirement_id').primary().defaultTo(knex.raw('gen_random_uuid()')) + + // Data + table.uuid('return_version_id').notNullable() + table.string('returns_frequency').notNullable() + table.boolean('is_summer').notNullable() + table.boolean('is_upload').notNullable() + table.smallint('abstraction_period_start_day') + table.smallint('abstraction_period_start_month') + table.smallint('abstraction_period_end_day') + table.smallint('abstraction_period_end_month') + table.string('site_description') + table.string('description') + table.integer('legacy_id') + table.string('external_id') + + // Legacy timestamps + table.timestamp('date_created', { useTz: false }).notNullable() + table.timestamp('date_updated', { useTz: false }) + + // Constraints + table.unique(['external_id'], { useConstraint: true }) + }) +} + +exports.down = function (knex) { + return knex + .schema + .withSchema('water') + .dropTableIfExists(tableName) +} diff --git a/db/migrations/legacy/20221108007025-water-return-requirement-purposes.js b/db/migrations/legacy/20221108007025-water-return-requirement-purposes.js new file mode 100644 index 0000000000..3b1c1772a5 --- /dev/null +++ b/db/migrations/legacy/20221108007025-water-return-requirement-purposes.js @@ -0,0 +1,35 @@ +'use strict' + +const tableName = 'return_requirement_purposes' + +exports.up = function (knex) { + return knex + .schema + .withSchema('water') + .createTable(tableName, (table) => { + // Primary Key + table.uuid('return_requirement_purpose_id').primary().defaultTo(knex.raw('gen_random_uuid()')) + + // Data + table.uuid('return_requirement_id').notNullable() + table.uuid('purpose_primary_id').notNullable() + table.uuid('purpose_secondary_id').notNullable() + table.uuid('purpose_use_id').notNullable() + table.string('purpose_alias') + table.string('external_id') + + // Legacy timestamps + table.timestamp('date_created', { useTz: false }).notNullable() + table.timestamp('date_updated', { useTz: false }) + + // Constraints + table.unique(['external_id'], { useConstraint: true }) + }) +} + +exports.down = function (knex) { + return knex + .schema + .withSchema('water') + .dropTableIfExists(tableName) +} diff --git a/test/controllers/data.controller.test.js b/test/controllers/data.controller.test.js index 110756b62f..a72a684b38 100644 --- a/test/controllers/data.controller.test.js +++ b/test/controllers/data.controller.test.js @@ -10,6 +10,7 @@ const { expect } = Code // Things we need to stub const SeedService = require('../../app/services/data/seed/seed.service.js') +const SubmitDeduplicateService = require('../../app/services/data/deduplicate/submit-deduplicate.service.js') const TearDownService = require('../../app/services/data/tear-down/tear-down.service.js') // For running our service @@ -34,67 +35,144 @@ describe('Data controller', () => { Sinon.restore() }) - describe('POST /data/seed', () => { - const options = { - method: 'POST', - url: '/data/seed' + describe('/data/deduplicate', () => { + const auth = { + strategy: 'session', + credentials: { scope: ['billing'] } } - describe('when the request succeeds', () => { - beforeEach(async () => { - Sinon.stub(SeedService, 'go').resolves() + let options + + describe('GET', () => { + beforeEach(() => { + options = { + method: 'GET', + url: '/data/deduplicate', + auth + } }) - it('displays the correct message', async () => { - const response = await server.inject(options) + describe('when the request succeeds', () => { + it('returns the page successfully', async () => { + const response = await server.inject(options) - expect(response.statusCode).to.equal(204) + expect(response.statusCode).to.equal(200) + expect(response.payload).to.contain('De-duplicate a licence') + }) }) }) - describe('when the request fails', () => { - describe('because the SeedService errors', () => { + describe('POST', () => { + const licenceRef = '01/120' + + beforeEach(() => { + options = { + method: 'POST', + url: '/data/deduplicate', + auth + } + }) + + describe('when a request is valid', () => { + beforeEach(async () => { + options.payload = { 'licence-ref': licenceRef } + + Sinon.stub(SubmitDeduplicateService, 'go').resolves({ licenceRef }) + }) + + it('redirects to the search page', async () => { + const response = await server.inject(options) + + expect(response.statusCode).to.equal(302) + expect(response.headers.location).to.equal(`/licences?query=${licenceRef}`) + }) + }) + + describe('when a request is invalid', () => { beforeEach(async () => { - Sinon.stub(SeedService, 'go').rejects() + options.payload = {} + + Sinon.stub(SubmitDeduplicateService, 'go').resolves({ + error: { text: 'Enter a licence reference to de-dupe' } + }) }) - it('returns a 500 status', async () => { + it('re-renders the page with an error message', async () => { const response = await server.inject(options) - expect(response.statusCode).to.equal(500) + expect(response.statusCode).to.equal(200) + expect(response.payload).to.contain('There is a problem') + expect(response.payload).to.contain('Enter a licence reference to de-dupe') }) }) }) }) - describe('POST /data/tear-down', () => { - const options = { - method: 'POST', - url: '/data/tear-down' - } + describe('/data/seed', () => { + describe('POST', () => { + const options = { + method: 'POST', + url: '/data/seed' + } + + describe('when the request succeeds', () => { + beforeEach(async () => { + Sinon.stub(SeedService, 'go').resolves() + }) - describe('when the request succeeds', () => { - beforeEach(async () => { - Sinon.stub(TearDownService, 'go').resolves() + it('displays the correct message', async () => { + const response = await server.inject(options) + + expect(response.statusCode).to.equal(204) + }) }) - it('returns a 204 status', async () => { - const response = await server.inject(options) + describe('when the request fails', () => { + describe('because the SeedService errors', () => { + beforeEach(async () => { + Sinon.stub(SeedService, 'go').rejects() + }) + + it('returns a 500 status', async () => { + const response = await server.inject(options) - expect(response.statusCode).to.equal(204) + expect(response.statusCode).to.equal(500) + }) + }) }) }) + }) - describe('when the request fails', () => { - describe('because the TearDownService errors', () => { + describe('/data/tear-down', () => { + describe('POST', () => { + const options = { + method: 'POST', + url: '/data/tear-down' + } + + describe('when the request succeeds', () => { beforeEach(async () => { - Sinon.stub(TearDownService, 'go').rejects() + Sinon.stub(TearDownService, 'go').resolves() }) - it('returns a 500 status', async () => { + it('returns a 204 status', async () => { const response = await server.inject(options) - expect(response.statusCode).to.equal(500) + expect(response.statusCode).to.equal(204) + }) + }) + + describe('when the request fails', () => { + describe('because the TearDownService errors', () => { + beforeEach(async () => { + Sinon.stub(TearDownService, 'go').rejects() + }) + + it('returns a 500 status', async () => { + const response = await server.inject(options) + + expect(response.statusCode).to.equal(500) + }) }) }) }) diff --git a/test/services/data/deduplicate/de-duplicate-licence.service.test.js b/test/services/data/deduplicate/de-duplicate-licence.service.test.js new file mode 100644 index 0000000000..e20cf4422b --- /dev/null +++ b/test/services/data/deduplicate/de-duplicate-licence.service.test.js @@ -0,0 +1,182 @@ +'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 DatabaseSupport = require('../../../support/database.js') +const LicenceHelper = require('../../../support/helpers/licence.helper.js') +const LicenceModel = require('../../../../app/models/licence.model.js') + +// Thing under test +const DeDuplicateLicenceService = require('../../../../app/services/data/deduplicate/de-duplicate-licence.service.js') + +describe('De-duplicate Licence service', () => { + const licenceRef = '01/120' + + let invalidLicence + let validLicences + + beforeEach(async () => { + await DatabaseSupport.clean() + + validLicences = [] + + // Create our valid licence + let licence = await LicenceHelper.add({ licenceRef }) + validLicences.push(licence.id) + + // Create other valid licences that will match our search. We need to ensure these do not get deleted by the service + licence = await LicenceHelper.add({ licenceRef: 'WA/01/120' }) + validLicences.push(licence.id) + licence = await LicenceHelper.add({ licenceRef: '02/01/120/R01' }) + validLicences.push(licence.id) + }) + + describe('when there is a duplicate licence', () => { + describe('with a space', () => { + describe('at the start of the reference', () => { + beforeEach(async () => { + invalidLicence = await LicenceHelper.add({ licenceRef: ' 01/120' }) + }) + + it('removes just that invalid licence', async () => { + await DeDuplicateLicenceService.go(licenceRef) + + const allLicences = await LicenceModel.query().select('id') + const allLicenceIds = allLicences.map((licence) => { + return licence.id + }) + + expect(allLicenceIds).include(validLicences) + expect(allLicenceIds).not.include(invalidLicence.id) + }) + }) + + describe('at the end of the reference', () => { + beforeEach(async () => { + invalidLicence = await LicenceHelper.add({ licenceRef: '01/120 ' }) + }) + + it('removes just that invalid licence', async () => { + await DeDuplicateLicenceService.go(licenceRef) + + const allLicences = await LicenceModel.query().select('id') + const allLicenceIds = allLicences.map((licence) => { + return licence.id + }) + + expect(allLicenceIds).include(validLicences) + expect(allLicenceIds).not.include(invalidLicence.id) + }) + }) + + describe('at the start and end of the reference', () => { + beforeEach(async () => { + invalidLicence = await LicenceHelper.add({ licenceRef: ' 01/120 ' }) + }) + + it('removes just that invalid licence', async () => { + await DeDuplicateLicenceService.go(licenceRef) + + const allLicences = await LicenceModel.query().select('id') + const allLicenceIds = allLicences.map((licence) => { + return licence.id + }) + + expect(allLicenceIds).include(validLicences) + expect(allLicenceIds).not.include(invalidLicence.id) + }) + }) + }) + + describe('with a newline', () => { + describe('at the start of the reference', () => { + beforeEach(async () => { + invalidLicence = await LicenceHelper.add({ licenceRef: '\n01/120' }) + }) + + it('removes just that invalid licence', async () => { + await DeDuplicateLicenceService.go(licenceRef) + + const allLicences = await LicenceModel.query().select('id') + const allLicenceIds = allLicences.map((licence) => { + return licence.id + }) + + expect(allLicenceIds).include(validLicences) + expect(allLicenceIds).not.include(invalidLicence.id) + }) + }) + + describe('at the end of the reference', () => { + beforeEach(async () => { + invalidLicence = await LicenceHelper.add({ licenceRef: '01/120\n' }) + }) + + it('removes just that invalid licence', async () => { + await DeDuplicateLicenceService.go(licenceRef) + + const allLicences = await LicenceModel.query().select('id') + const allLicenceIds = allLicences.map((licence) => { + return licence.id + }) + + expect(allLicenceIds).include(validLicences) + expect(allLicenceIds).not.include(invalidLicence.id) + }) + }) + + describe('at the start and end of the reference', () => { + beforeEach(async () => { + invalidLicence = await LicenceHelper.add({ licenceRef: '\n01/120\n' }) + }) + + it('removes just that invalid licence', async () => { + await DeDuplicateLicenceService.go(licenceRef) + + const allLicences = await LicenceModel.query().select('id') + const allLicenceIds = allLicences.map((licence) => { + return licence.id + }) + + expect(allLicenceIds).include(validLicences) + expect(allLicenceIds).not.include(invalidLicence.id) + }) + }) + }) + }) + + describe('when there is no duplicate licence', () => { + it('removes no licences', async () => { + await DeDuplicateLicenceService.go(licenceRef) + + const allLicences = await LicenceModel.query().select('id') + const allLicenceIds = allLicences.map((licence) => { + return licence.id + }) + + expect(allLicenceIds).include(validLicences) + }) + }) + + describe('when there is no matching licence', () => { + it('removes no licences', async () => { + // NOTE: We also pondered what happens if someone enters a reference that is invalid for other reasons like + // they put a space in the middle. We definitely have no licences with this kind of reference so it will be + // treated in the same way as a reference that matches no licences. + await DeDuplicateLicenceService.go('01/ 120') + + const allLicences = await LicenceModel.query().select('id') + const allLicenceIds = allLicences.map((licence) => { + return licence.id + }) + + expect(allLicenceIds).include(validLicences) + }) + }) +}) diff --git a/test/services/data/deduplicate/submit-deduplicate.service.test.js b/test/services/data/deduplicate/submit-deduplicate.service.test.js new file mode 100644 index 0000000000..191b6a5542 --- /dev/null +++ b/test/services/data/deduplicate/submit-deduplicate.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, beforeEach, afterEach } = exports.lab = Lab.script() +const { expect } = Code + +// Things we need to stub +const DeDuplicateService = require('../../../../app/services/data/deduplicate/de-duplicate-licence.service.js') + +// Thing under test +const SubmitDeduplicateService = require('../../../../app/services/data/deduplicate/submit-deduplicate.service.js') + +describe('Submit Deduplicate service', () => { + let deDuplicateServiceStub + let payload + + beforeEach(async () => { + deDuplicateServiceStub = Sinon.stub(DeDuplicateService, 'go').resolves() + }) + + afterEach(() => { + Sinon.restore() + }) + + describe('when called with a populated payload', () => { + beforeEach(() => { + payload = { 'licence-ref': ' wa/01/120 ' } + }) + + it('removes whitespace and uppercases the licence reference entered and returns it', async () => { + const result = await SubmitDeduplicateService.go(payload) + + expect(result.licenceRef).to.equal('WA/01/120') + }) + + it('calls the de-dupe service with the parsed licence reference', async () => { + await SubmitDeduplicateService.go(payload) + + const parsedLicenceRef = deDuplicateServiceStub.args[0][0] + + expect(deDuplicateServiceStub.called).to.be.true() + expect(deDuplicateServiceStub.calledWith(parsedLicenceRef)).to.be.true() + }) + }) + + describe('when called with a unpopulated payload', () => { + describe('because nothing was entered by the user', () => { + beforeEach(() => { + payload = {} + }) + + it('returns page data for the view containing an error', async () => { + const result = await SubmitDeduplicateService.go(payload) + + expect(result.error).to.exist() + expect(result.error).to.equal({ text: 'Enter a licence reference to de-dupe' }) + }) + }) + + describe('because only whitespace was entered by the user', () => { + beforeEach(() => { + payload = { 'licence-ref': ' ' } + }) + + it('returns page data for the view containing an error', async () => { + const result = await SubmitDeduplicateService.go(payload) + + expect(result.error).to.exist() + expect(result.error).to.equal({ text: 'Enter a licence reference to de-dupe' }) + }) + }) + }) +})