diff --git a/app/services/notifications/setup/fetch-download-recipients.service.js b/app/services/notifications/setup/fetch-download-recipients.service.js new file mode 100644 index 0000000000..9e9ce9d9ae --- /dev/null +++ b/app/services/notifications/setup/fetch-download-recipients.service.js @@ -0,0 +1,135 @@ +'use strict' + +/** + * Formats the contact data from which recipients will be determined for the `/notifications/setup/download` link + * @module FetchDownloadRecipientsService + */ + +const { db } = require('../../../../db/db.js') + +/** + * Formats the contact data from which recipients will be determined for the `/notifications/setup/download` link + * + * > IMPORTANT! The source for notification contacts is `crm.document_headers` (view `licence_document_headers`), not + * > the tables in `crm_v2`. + * + * Our overall goal is that a 'recipient' receives only one notification, irrespective of how many licences they are + * linked to, or what roles they have. + * + * We start by determining which licences we need to send notifications for, by looking for 'due' return logs with a + * matching 'due date' and cycle (summer or winter and all year). + * + * For each licence linked to one of these return logs, we extract the contact information. This is complicated by a + * number of factors. + * + * - if a licence is _registered_ (more details below), we only care about the email addresses registered against it + * - all licences should have a 'licence holder' contact, but they may also have a 'returns' contact + * - there is a one-to-one relationship between `licences` and `licence_document_headers`, but the same contact (licence + * holder or returns) can appear in different licences, and we are expected to group them into a 'single' contact + * + * WRLS has the concept of a registered and unregistered licences: + * + * - **Unregistered licences** have not been linked to an external email, so do not have a 'primary user'. All licences + * have a contact with the role 'Licence holder', so this will be extracted as a 'contact'. They may also have a + * contact with the role 'Returns to' (but only one), which is extracted as well. + * - **Registered licences** have been linked to an external email. That initial email will be linked as the 'primary + * user'. These licences may also have designated other accounts as 'returns agents', which will be extracted as well. + * + * If a licence is registered, we only extract the email contacts. Unregistered licences its the 'Licence holder' and + * 'Returns to' contacts from `licence_document_headers.metadata->contacts`. + * + * We have another service 'FetchContactsService' which removes duplicates rows by squashing them together. We do not + * want to remove duplicates for the downloadable recipients. Each row in the CSV file should represent the data + * received from this query (For either registered to unregistered licence). We expect to see duplicate licences with + * different contacts types (but still preferring the registered over unregistered licence). + * + * @param {Date} dueDate + * @param {boolean} summer + * + * @returns {Promise} - matching recipients + */ +async function go(dueDate, summer) { + const { rows } = await _fetch(dueDate, summer) + + return rows +} + +async function _fetch(dueDate, summer) { + const query = _query() + + return db.raw(query, [dueDate, summer, dueDate, summer]) +} + +function _query() { + return ` +SELECT + contacts.licence_ref, + contacts.contact_type, + contacts.return_reference, + contacts.start_date, + contacts.end_date, + contacts.due_date, + contacts.email, + contacts.contact +FROM ( + SELECT DISTINCT + ldh.licence_ref, + (contacts->>'role') AS contact_type, + (NULL) AS email, + contacts as contact, + rl.return_reference, + rl.start_date, + rl.end_date, + rl.due_date + FROM public.licence_document_headers ldh + INNER JOIN LATERAL jsonb_array_elements(ldh.metadata -> 'contacts') AS contacts ON TRUE + INNER JOIN public.return_logs rl + ON rl.licence_ref = ldh.licence_ref + WHERE + rl.status = 'due' + AND rl.due_date = ? + AND rl.metadata->>'isCurrent' = 'true' + AND rl.metadata->>'isSummer' = ? + AND contacts->>'role' IN ('Licence holder', 'Returns to') + AND NOT EXISTS ( + SELECT + 1 + FROM public.licence_entity_roles ler + WHERE + ler.company_entity_id = ldh.company_entity_id + AND ler."role" IN ('primary_user', 'user_returns') + ) + UNION ALL + SELECT + ldh.licence_ref, + (CASE + WHEN ler."role" = 'primary_user' THEN 'Primary user' + ELSE 'Returns agent' + END) AS contact_type, + le."name" AS email, + (NULL) AS contact, + rl.return_reference, + rl.start_date, + rl.end_date, + rl.due_date + FROM public.licence_document_headers ldh + INNER JOIN public.licence_entity_roles ler + ON ler.company_entity_id = ldh.company_entity_id + AND ler."role" IN ('primary_user', 'user_returns') + INNER JOIN public.licence_entities le + ON le.id = ler.licence_entity_id + INNER JOIN public.return_logs rl + ON rl.licence_ref = ldh.licence_ref + WHERE + rl.status = 'due' + AND rl.due_date = ? + AND rl.metadata->>'isCurrent' = 'true' + AND rl.metadata->>'isSummer' = ? +) contacts +ORDER BY +contacts.licence_ref` +} + +module.exports = { + go +} diff --git a/test/services/notifications/setup/fetch-download-recipients.service.test.js b/test/services/notifications/setup/fetch-download-recipients.service.test.js new file mode 100644 index 0000000000..d02280705d --- /dev/null +++ b/test/services/notifications/setup/fetch-download-recipients.service.test.js @@ -0,0 +1,155 @@ +'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 LicenceDocumentHeaderSeeder = require('../../../support/seeders/licence-document-header.seeder.js') + +// Thing under test +const FetchDownloadRecipientsService = require('../../../../app/services/notifications/setup/fetch-download-recipients.service.js') + +describe('Notifications Setup - Fetch Download Recipients service', () => { + // These dates match the return logs helper + const startDate = new Date('2022-04-01') + const endDate = new Date('2023-03-31') + + let dueDate + let isSummer + let testRecipients + + before(async () => { + dueDate = '2024-04-28' // This needs to differ from any other returns log tests + isSummer = 'false' + + testRecipients = await LicenceDocumentHeaderSeeder.seed(true, dueDate) + }) + + describe('when there are recipients', () => { + it('correctly returns "Primary user" and "Returns agent" contacts', async () => { + const result = await FetchDownloadRecipientsService.go(dueDate, isSummer) + + const primaryUser = result.find((item) => item.contact_type === 'Primary user') + const returnsAgent = result.find((item) => item.contact_type === 'Returns agent') + + expect(primaryUser).to.equal({ + contact: null, + contact_type: 'Primary user', + due_date: new Date(dueDate), + email: 'primary.user@important.com', + end_date: endDate, + licence_ref: testRecipients.primaryUser.licenceRef, + return_reference: testRecipients.primaryUser.returnLog.returnReference, + start_date: startDate + }) + + expect(returnsAgent).to.equal({ + contact: null, + contact_type: 'Returns agent', + due_date: new Date(dueDate), + email: 'returns.agent@important.com', + end_date: endDate, + licence_ref: testRecipients.primaryUser.licenceRef, + return_reference: testRecipients.primaryUser.returnLog.returnReference, + start_date: startDate + }) + }) + + it('correctly returns "Licence holder" contact', async () => { + const result = await FetchDownloadRecipientsService.go(dueDate, isSummer) + + const found = result.filter((item) => item.licence_ref === testRecipients.licenceHolder.licenceRef) + + expect(found).to.equal([ + { + contact: { + addressLine1: '4', + addressLine2: 'Privet Drive', + addressLine3: null, + addressLine4: null, + country: null, + county: 'Surrey', + forename: 'Harry', + initials: 'J', + name: 'Licence holder only', + postcode: 'WD25 7LR', + role: 'Licence holder', + salutation: null, + town: 'Little Whinging', + type: 'Person' + }, + contact_type: 'Licence holder', + due_date: new Date(dueDate), + email: null, + end_date: endDate, + licence_ref: testRecipients.licenceHolder.licenceRef, + return_reference: testRecipients.licenceHolder.returnLog.returnReference, + start_date: startDate + } + ]) + }) + + it('correctly returns duplicate "Licence holder" and "Returns to" contacts', async () => { + const result = await FetchDownloadRecipientsService.go(dueDate, isSummer) + + const found = result.filter((item) => item.licence_ref === testRecipients.licenceHolderAndReturnTo.licenceRef) + + expect(found).to.equal([ + { + contact: { + addressLine1: '4', + addressLine2: 'Privet Drive', + addressLine3: null, + addressLine4: null, + country: null, + county: 'Surrey', + forename: 'Harry', + initials: 'J', + name: 'Licence holder and returns to', + postcode: 'WD25 7LR', + role: 'Licence holder', + salutation: null, + town: 'Little Whinging', + type: 'Person' + }, + contact_type: 'Licence holder', + due_date: new Date(dueDate), + email: null, + end_date: endDate, + licence_ref: testRecipients.licenceHolderAndReturnTo.licenceRef, + return_reference: testRecipients.licenceHolderAndReturnTo.returnLog.returnReference, + start_date: startDate + }, + { + contact: { + addressLine1: '4', + addressLine2: 'Privet Drive', + addressLine3: null, + addressLine4: null, + country: null, + county: 'Surrey', + forename: 'Harry', + initials: 'J', + name: 'Licence holder and returns to', + postcode: 'WD25 7LR', + role: 'Returns to', + salutation: null, + town: 'Little Whinging', + type: 'Person' + }, + contact_type: 'Returns to', + due_date: new Date(dueDate), + email: null, + end_date: endDate, + licence_ref: testRecipients.licenceHolderAndReturnTo.licenceRef, + return_reference: testRecipients.licenceHolderAndReturnTo.returnLog.returnReference, + start_date: startDate + } + ]) + }) + }) +}) diff --git a/test/support/helpers/return-log.helper.js b/test/support/helpers/return-log.helper.js index 3908fc6ac0..ed515ddbc3 100644 --- a/test/support/helpers/return-log.helper.js +++ b/test/support/helpers/return-log.helper.js @@ -57,11 +57,12 @@ function defaults(data = {}) { const receivedDate = data.receivedDate ? data.receivedDate : null const startDate = data.startDate ? new Date(data.startDate) : new Date('2022-04-01') const endDate = data.endDate ? new Date(data.endDate) : new Date('2023-03-31') + const dueDate = data.dueDate ? new Date(data.dueDate) : new Date('2023-04-28') const defaults = { id: generateReturnLogId(startDate, endDate, 1, licenceRef, returnReference), createdAt: timestamp, - dueDate: new Date('2023-04-28'), + dueDate, endDate, licenceRef, metadata: { diff --git a/test/support/seeders/licence-document-header.seeder.js b/test/support/seeders/licence-document-header.seeder.js index 80eb688c16..684af026f0 100644 --- a/test/support/seeders/licence-document-header.seeder.js +++ b/test/support/seeders/licence-document-header.seeder.js @@ -12,20 +12,21 @@ const ReturnLogHelper = require('../helpers/return-log.helper.js') /** * Adds licence document header and return log records to the database which are linked by licence ref * - * @param {boolean} returnLogs - defaulted to true, this needs to be false if you do not want the `licenceDocumentHeader` + * @param {boolean} enableReturnLog - defaulted to true, this needs to be false if you do not want the `licenceDocumentHeader` * to be included in the recipients list + * @param {string} returnLogDueDate - defaulted to the same due date set by the returnsLogHelper * * @returns {object[]} - an array of the added licenceDocumentHeaders */ -async function seed(returnLogs = true) { +async function seed(enableReturnLog = true, returnLogDueDate = '2023-04-28') { return { - licenceHolder: await _addLicenceHolder(returnLogs), - licenceHolderAndReturnTo: await _addLicenceHolderAndReturnToSameRef(returnLogs), - primaryUser: await _addLicenceEntityRoles(returnLogs) + licenceHolder: await _addLicenceHolder(enableReturnLog, returnLogDueDate), + licenceHolderAndReturnTo: await _addLicenceHolderAndReturnToSameRef(enableReturnLog, returnLogDueDate), + primaryUser: await _addLicenceEntityRoles(enableReturnLog, returnLogDueDate) } } -async function _addLicenceEntityRoles(returnLogs) { +async function _addLicenceEntityRoles(enableReturnLog, returnLogDueDate) { const primaryUser = { name: 'Primary User test', email: 'primary.user@important.com', @@ -68,16 +69,19 @@ async function _addLicenceEntityRoles(returnLogs) { role: userReturns.role }) - if (returnLogs) { - await ReturnLogHelper.add({ - licenceRef: licenceDocumentHeader.licenceRef + let returnLog + + if (enableReturnLog) { + returnLog = await ReturnLogHelper.add({ + licenceRef: licenceDocumentHeader.licenceRef, + dueDate: returnLogDueDate }) } - return licenceDocumentHeader + return { ...licenceDocumentHeader, returnLog } } -async function _addLicenceHolder(returnLogs) { +async function _addLicenceHolder(enableReturnLog, returnLogDueDate) { const name = 'Licence holder only' const licenceDocumentHeader = await LicenceDocumentHeaderHelper.add({ metadata: { @@ -86,16 +90,19 @@ async function _addLicenceHolder(returnLogs) { } }) - if (returnLogs) { - await ReturnLogHelper.add({ - licenceRef: licenceDocumentHeader.licenceRef + let returnLog + + if (enableReturnLog) { + returnLog = await ReturnLogHelper.add({ + licenceRef: licenceDocumentHeader.licenceRef, + dueDate: returnLogDueDate }) } - return licenceDocumentHeader + return { ...licenceDocumentHeader, returnLog } } -async function _addLicenceHolderAndReturnToSameRef(returnLogs) { +async function _addLicenceHolderAndReturnToSameRef(enableReturnLog, returnLogDueDate) { const name = 'Licence holder and returns to' const licenceDocumentHeader = await LicenceDocumentHeaderHelper.add({ metadata: { @@ -104,13 +111,16 @@ async function _addLicenceHolderAndReturnToSameRef(returnLogs) { } }) - if (returnLogs) { - await ReturnLogHelper.add({ - licenceRef: licenceDocumentHeader.licenceRef + let returnLog + + if (enableReturnLog) { + returnLog = await ReturnLogHelper.add({ + licenceRef: licenceDocumentHeader.licenceRef, + dueDate: returnLogDueDate }) } - return licenceDocumentHeader + return { ...licenceDocumentHeader, returnLog } } function _contact(name, role) {