From b14c7d4b121297ed0a3086b9aa03d5310b5e66a6 Mon Sep 17 00:00:00 2001 From: jonathangoulding Date: Mon, 27 Jan 2025 15:03:03 +0000 Subject: [PATCH] fix: simplify dedupe to bare requirement --- app/presenters/contact.presenter.js | 69 -------- app/presenters/crm-contact.presenter.js | 161 ++++++++++++++++++ .../view-licence-contact-details.presenter.js | 4 +- .../notifications/setup/review.presenter.js | 5 +- .../setup/fetch-recipients.service.js | 2 - test/fixtures/recipients.fixtures.js | 2 + test/presenters/crm-contact.presenter.js | 147 ++++++++++++++++ 7 files changed, 314 insertions(+), 76 deletions(-) delete mode 100644 app/presenters/contact.presenter.js create mode 100644 app/presenters/crm-contact.presenter.js create mode 100644 test/presenters/crm-contact.presenter.js diff --git a/app/presenters/contact.presenter.js b/app/presenters/contact.presenter.js deleted file mode 100644 index 3ed09bbc2c..0000000000 --- a/app/presenters/contact.presenter.js +++ /dev/null @@ -1,69 +0,0 @@ -function _licenceContactAddress(contact) { - const contactAddressFields = [ - 'addressLine1', - 'addressLine2', - 'addressLine3', - 'addressLine4', - 'town', - 'county', - 'postcode', - 'country' - ] - - // NOTE: Maps over the `contactAddressFields` array to create an array of values from the `contact` object. Each - // `contactAddressField` corresponds to a property in the `contact` object, mapping and creating a contactAddress - // array. The `filter(Boolean)` function then removes falsy values from the `contactAddress` array. - const contactAddress = contactAddressFields - .map((contactAddressField) => { - return contact[contactAddressField] - }) - .filter(Boolean) - - return contactAddress -} - -function _licenceContactName(contact) { - if (contact.type === 'Person') { - const { salutation, forename, initials, name } = contact - - // NOTE: Prioritise the initials and use the contact forename if initials is null - const initialsOrForename = initials || forename - - const nameComponents = [salutation, initialsOrForename, name] - - const filteredNameComponents = nameComponents.filter((item) => { - return item - }) - - return filteredNameComponents.join(' ') - } - - return contact.name -} - -/** - * - * @param licenceDocumentHeader - * @param contact - */ -function _licenceContactDetails(contact) { - const licenceContactDetailsData = contact - - const roles = ['Licence holder', 'Returns to', 'Licence contact'] - - const filteredContactDetails = licenceContactDetailsData.filter((licenceContactDetail) => { - return roles.includes(licenceContactDetail.role) - }) - - return filteredContactDetails.map((contact) => { - return { - address: _licenceContactAddress(contact), - role: contact.role, - name: _licenceContactName(contact) - } - }) -} - -module.exports = { - licenceContactDetails: _licenceContactDetails -} diff --git a/app/presenters/crm-contact.presenter.js b/app/presenters/crm-contact.presenter.js new file mode 100644 index 0000000000..867923f81d --- /dev/null +++ b/app/presenters/crm-contact.presenter.js @@ -0,0 +1,161 @@ +'use strict' + +/** + * Formats contact data from crm.metadata.contacts + * @module CRMContactPresenter + */ + +const roles = ['Licence holder', 'Returns to', 'Licence contact'] + +const contactAddressFields = [ + 'addressLine1', + 'addressLine2', + 'addressLine3', + 'addressLine4', + 'town', + 'county', + 'postcode', + 'country' +] + +/** + * Processes an array of contacts and returns a list of objects containing the contact's address, role, and name. + * + * This contacts come directly from the **licenceDocumentHeader.metadata.contacts** object. + * + * The function first filters the contacts to include only those with valid roles. + * Then, for each valid contact, it extracts the address, role, and name. + * + * @param {Array} contacts - An array of contact objects, each containing various fields including role, address, and name. + * @param {string} contacts[].role - The role of the contact (e.g., 'Licence holder', 'Returns to'). + * @param {Object} contacts[].address - The address details of the contact (used by `_contactAddress`). + * @param {string} contacts[].name - The name of the contact (used by `_contactName`). + * + * @returns {Array} - A new array of objects, each containing the contact's address, role, and name. + * + * @example + * const contacts = [ + * { role: 'Licence holder', name: 'John Doe', address: { street: '123 Main St', city: 'Springfield' } }, + * { role: 'Returns to', name: 'Jane Smith', address: { street: '456 Elm St', city: 'Springfield' } } + * ]; + * const result = go(contacts); + * console.log(result); + * // Output: + * // [ + * // { address: { street: '123 Main St', city: 'Springfield' }, role: 'Licence holder', name: 'John Doe' }, + * // { address: { street: '456 Elm St', city: 'Springfield' }, role: 'Returns to', name: 'Jane Smith' } + * // ] + * + * @private + */ +function go(contacts) { + const filteredContacts = _extractOnlyContactsWithRoles(contacts) + + return filteredContacts.map((contact) => { + return { + address: _contactAddress(contact), + role: contact.role, + name: _contactName(contact) + } + }) +} + +function _extractOnlyContactsWithRoles(contacts) { + return contacts.filter((contact) => { + return roles.includes(contact.role) + }) +} + +/** + * Extracts the address fields from a contact object and returns them as an array, + * omitting any falsy values (e.g., empty strings, null, undefined). + * + * This function maps over a predefined set of address fields in the `contact` object, + * retrieves their values, and filters out any falsy values that may exist in the address + * data (e.g., missing or empty fields). + * + * @param {Object} contact - The contact object containing address-related fields. + * @param {string} contact.addressLine1 - The first line of the contact's address. + * @param {string} contact.addressLine2 - The second line of the contact's address. + * @param {string} contact.addressLine3 - The third line of the contact's address (optional). + * @param {string} contact.addressLine4 - The fourth line of the contact's address (optional). + * @param {string} contact.town - The town or city of the contact's address. + * @param {string} contact.county - The county or region of the contact's address. + * @param {string} contact.postcode - The postcode of the contact's address. + * @param {string} contact.country - The country of the contact's address. + * + * @returns {Array} An array of address fields from the contact, with falsy values removed. + * Each element in the array corresponds to a non-falsy value from the contact's address fields. + * + * @example + * const contact = { + * addressLine1: '123 Main St', + * addressLine2: '', + * addressLine3: 'Apt 4B', + * addressLine4: null, + * town: 'Springfield', + * county: 'Greene', + * postcode: '12345', + * country: 'USA' + * }; + * const address = _contactAddress(contact); + * console.log(address); // ['123 Main St', 'Apt 4B', 'Springfield', 'Greene', '12345', 'USA'] + * + * @private + */ +function _contactAddress(contact) { + return contactAddressFields.map((field) => contact[field]).filter(Boolean) +} + +/** + * Constructs a full name for a contact based on their type. + * + * If the contact is a "Person", the function will prioritize using the initials + * (if available). If not, it will use the forename. The salutation, initials or forename, + * and last name will be concatenated together to form the full name. Any falsy values + * (such as empty strings or `null`) are removed from the final result. + * + * If the contact is not a "Person", the function simply returns the `name` property. + * + * @param {Object} contact - The contact object containing information about the person. + * @param {string} contact.type - The type of the contact (e.g., "Person"). + * @param {string} contact.salutation - The salutation (e.g., "Mr.", "Dr."). + * @param {string} contact.forename - The forename (e.g., "John"). + * @param {string} contact.initials - The initials (e.g., "J.D."). + * @param {string} contact.name - The full name of the contact (e.g., "Doe"). + * @returns {string} The full name of the contact, or the contact's `name` if not a "Person". + * + * @example + * const contact = { + * type: 'Person', + * salutation: 'Dr.', + * forename: 'John', + * initials: 'J.D.', + * name: 'Doe' + * }; + * console.log(_contactName(contact)); // 'Dr. J.D. Doe' + * + * @private + */ +function _contactName(contact) { + if (contact.type === 'Person') { + const { salutation, forename, initials, name } = contact + + // NOTE: Prioritise the initials and use the contact forename if initials is null + const initialsOrForename = initials || forename + + const nameComponents = [salutation, initialsOrForename, name] + + const filteredNameComponents = nameComponents.filter((item) => { + return item + }) + + return filteredNameComponents.join(' ') + } + + return contact.name +} + +module.exports = { + go +} diff --git a/app/presenters/licences/view-licence-contact-details.presenter.js b/app/presenters/licences/view-licence-contact-details.presenter.js index 5db7c73cfc..ee8b95f9e6 100644 --- a/app/presenters/licences/view-licence-contact-details.presenter.js +++ b/app/presenters/licences/view-licence-contact-details.presenter.js @@ -5,7 +5,7 @@ * @module ViewLicenceContactDetailsPresenter */ -const { licenceContactDetails } = require('../contact.presenter.js') +const CRMContactPresenter = require('../crm-contact.presenter.js') /** * Formats data for the `/licences/{id}/licence-contact` view licence contact details link page @@ -20,7 +20,7 @@ function go(licence) { return { licenceId, licenceRef, - licenceContactDetails: licenceContactDetails(licenceDocumentHeader.metadata.contacts), + licenceContactDetails: CRMContactPresenter.go(licenceDocumentHeader.metadata.contacts), pageTitle: 'Licence contact details' } } diff --git a/app/presenters/notifications/setup/review.presenter.js b/app/presenters/notifications/setup/review.presenter.js index 21001c192f..b7036211da 100644 --- a/app/presenters/notifications/setup/review.presenter.js +++ b/app/presenters/notifications/setup/review.presenter.js @@ -6,8 +6,7 @@ */ const { defaultPageSize } = require('../../../../config/database.config.js') -const { licenceContactDetails } = require('../..//contact.presenter.js') -const { titleCase } = require('../../base.presenter.js') +const CRMContactPresenter = require('../../crm-contact.presenter.js') /** * Formats data for the `/notifications/setup/review` page @@ -43,7 +42,7 @@ function _contact(recipient) { return [recipient.recipient] } - const [contact] = licenceContactDetails([recipient.contact]) + const [contact] = CRMContactPresenter.go([recipient.contact]) return [contact.name, ...contact.address] } diff --git a/app/services/notifications/setup/fetch-recipients.service.js b/app/services/notifications/setup/fetch-recipients.service.js index ee964433d4..22527f6014 100644 --- a/app/services/notifications/setup/fetch-recipients.service.js +++ b/app/services/notifications/setup/fetch-recipients.service.js @@ -17,7 +17,6 @@ const { db } = require('../../../../db/db.js') */ async function go(dueDate, summer) { const { rows } = await _fetch(dueDate, summer) - // const { rows } = await _fetch('2024-11-28', 'true') return rows } @@ -148,7 +147,6 @@ FROM ( AND rl.metadata->>'isCurrent' = 'true' AND rl.metadata->>'isSummer' = ? ) recipients --- WHERE contact_hash_id = 801469274 GROUP BY message_type, recipient, diff --git a/test/fixtures/recipients.fixtures.js b/test/fixtures/recipients.fixtures.js index ef7f4344b1..459ac83789 100644 --- a/test/fixtures/recipients.fixtures.js +++ b/test/fixtures/recipients.fixtures.js @@ -34,7 +34,9 @@ function duplicateRecipients() { } /** + * Create duplicate by contact hash recipients with different types * + * @returns {object} - Returns duplicate contact hash recipients with different types */ function duplicateContactWithDifferentType() { return { diff --git a/test/presenters/crm-contact.presenter.js b/test/presenters/crm-contact.presenter.js new file mode 100644 index 0000000000..5a59844852 --- /dev/null +++ b/test/presenters/crm-contact.presenter.js @@ -0,0 +1,147 @@ +'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 + +// Thing under test +const CRMContactPresenter = require('../../app/presenters/crm-contact.presenter.js') + +describe('CRM contact presenter', () => { + let contacts + + beforeEach(() => { + contacts = _contacts() + }) + + describe('when provided with valid contacts', () => { + it('correctly presents the data', () => { + const result = CRMContactPresenter.go(contacts) + + expect(result).to.equal([ + { + address: ['ENVIRONMENT AGENCY', 'HORIZON HOUSE', 'DEANERY ROAD', 'BRISTOL', 'BS1 5AH', 'United Kingdom'], + role: 'Licence holder', + name: 'Acme ltd' + }, + { + address: ['Furland', 'Crewkerne', 'Somerset', 'TA18 7TT', 'United Kingdom'], + role: 'Licence contact', + name: 'Furland Farm' + }, + { + address: ['Crinkley Bottom', 'Cricket St Thomas', 'Somerset', 'TA20 1KL', 'United Kingdom'], + role: 'Returns to', + name: 'Mr N Edmonds' + } + ]) + }) + + describe('the "name" property', () => { + describe('and the "role" is "person"', () => { + describe('when the initials are null', () => { + beforeEach(() => { + contacts[2].initials = null + }) + + it("returns the contact's forename and name", () => { + const result = CRMContactPresenter.go(contacts) + + expect(result[2].name).to.equal('Mr Noel Edmonds') + }) + }) + + describe('when the initials are not null', () => { + it("returns the contact's initials and name", () => { + const result = CRMContactPresenter.go(contacts) + + expect(result[2].name).to.equal('Mr N Edmonds') + }) + }) + }) + + describe('and the "role" is NOT a "person"', () => { + it("returns the contact's forename and name", () => { + const result = CRMContactPresenter.go(contacts) + + expect(result[0].name).to.equal('Acme ltd') + }) + }) + }) + }) + + describe('when provided with invalid contacts', () => { + describe('when a "role" is not a valid role', () => { + beforeEach(() => { + contacts = [ + { + ...contacts[0], + role: 'Enforcement office' + } + ] + }) + + it('does not return the contact', () => { + const result = CRMContactPresenter.go(contacts) + + expect(result).to.equal([]) + }) + }) + }) +}) + +function _contacts() { + return [ + { + name: 'Acme ltd', + role: 'Licence holder', + town: 'BRISTOL', + type: 'Organisation', + county: null, + country: 'United Kingdom', + forename: null, + initials: null, + postcode: 'BS1 5AH', + salutation: null, + addressLine1: 'ENVIRONMENT AGENCY', + addressLine2: 'HORIZON HOUSE', + addressLine3: 'DEANERY ROAD', + addressLine4: null + }, + { + name: 'Furland Farm', + role: 'Licence contact', + town: 'Somerset', + type: 'Organisation', + county: null, + country: 'United Kingdom', + forename: null, + initials: null, + postcode: 'TA18 7TT', + salutation: null, + addressLine1: 'Furland', + addressLine2: 'Crewkerne', + addressLine3: null, + addressLine4: null + }, + { + name: 'Edmonds', + role: 'Returns to', + town: 'Somerset', + type: 'Person', + county: null, + country: 'United Kingdom', + forename: 'Noel', + initials: 'N', + postcode: 'TA20 1KL', + salutation: 'Mr', + addressLine1: 'Crinkley Bottom', + addressLine2: 'Cricket St Thomas', + addressLine3: null, + addressLine4: null + } + ] +}