Skip to content

Commit 1c2c15b

Browse files
Add FetchDownloadRecipientsService
https://eaflood.atlassian.net/browse/WATER-4776 As part of the work to implement notifications in the system repo we have a requirement to download the recipients data as a CSV. This change adds the required data for the CSV. This recipient list does not require deduplication as per previous work. And we can expect multiple rows per licence. However, we will still a registered licence contact over an unregistered licence. The controller / presenter / service will be added in a later change.
1 parent 2195037 commit 1c2c15b

File tree

4 files changed

+292
-20
lines changed

4 files changed

+292
-20
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
'use strict'
2+
3+
/**
4+
* Formats the contact data from which recipients will be determined for the `/notifications/setup/download` link
5+
* @module FetchDownloadRecipientsService
6+
*/
7+
8+
const { db } = require('../../../../db/db.js')
9+
10+
/**
11+
* Formats the contact data from which recipients will be determined for the `/notifications/setup/download` link
12+
*
13+
* > IMPORTANT! The source for notification contacts is `crm.document_headers` (view `licence_document_headers`), not
14+
* > the tables in `crm_v2`.
15+
*
16+
* Our overall goal is that a 'recipient' receives only one notification, irrespective of how many licences they are
17+
* linked to, or what roles they have.
18+
*
19+
* We start by determining which licences we need to send notifications for, by looking for 'due' return logs with a
20+
* matching 'due date' and cycle (summer or winter and all year).
21+
*
22+
* For each licence linked to one of these return logs, we extract the contact information. This is complicated by a
23+
* number of factors.
24+
*
25+
* - if a licence is _registered_ (more details below), we only care about the email addresses registered against it
26+
* - all licences should have a 'licence holder' contact, but they may also have a 'returns' contact
27+
* - there is a one-to-one relationship between `licences` and `licence_document_headers`, but the same contact (licence
28+
* holder or returns) can appear in different licences, and we are expected to group them into a 'single' contact
29+
*
30+
* WRLS has the concept of a registered and unregistered licences:
31+
*
32+
* - **Unregistered licences** have not been linked to an external email, so do not have a 'primary user'. All licences
33+
* have a contact with the role 'Licence holder', so this will be extracted as a 'contact'. They may also have a
34+
* contact with the role 'Returns to' (but only one), which is extracted as well.
35+
* - **Registered licences** have been linked to an external email. That initial email will be linked as the 'primary
36+
* user'. These licences may also have designated other accounts as 'returns agents', which will be extracted as well.
37+
*
38+
* If a licence is registered, we only extract the email contacts. Unregistered licences its the 'Licence holder' and
39+
* 'Returns to' contacts from `licence_document_headers.metadata->contacts`.
40+
*
41+
* FetchContactsService removes duplicates, and we have other functions that squash duplicate licences together.
42+
* We do not want to remove duplicates for the downloadable recipients. Each row in the CSV file should represent the
43+
* data received from this query. We expect to see duplicate licences with different contacts types.
44+
*
45+
* @param {Date} dueDate
46+
* @param {boolean} summer
47+
*
48+
* @returns {Promise<object[]>} - matching recipients
49+
*/
50+
async function go(dueDate, summer) {
51+
const { rows } = await _fetch(dueDate, summer)
52+
53+
return rows
54+
}
55+
56+
async function _fetch(dueDate, summer) {
57+
const query = _query()
58+
59+
return db.raw(query, [dueDate, summer, dueDate, summer])
60+
}
61+
62+
function _query() {
63+
return `
64+
SELECT
65+
contacts.licence_ref,
66+
contacts.contact_type,
67+
contacts.return_reference,
68+
contacts.email,
69+
contacts.contact
70+
FROM (
71+
SELECT DISTINCT
72+
ldh.licence_ref,
73+
(contacts->>'role') AS contact_type,
74+
(NULL) AS email,
75+
contacts as contact,
76+
rl.return_reference
77+
FROM public.licence_document_headers ldh
78+
INNER JOIN LATERAL jsonb_array_elements(ldh.metadata -> 'contacts') AS contacts ON TRUE
79+
INNER JOIN public.return_logs rl
80+
ON rl.licence_ref = ldh.licence_ref
81+
WHERE
82+
rl.status = 'due'
83+
AND rl.due_date = ?
84+
AND rl.metadata->>'isCurrent' = 'true'
85+
AND rl.metadata->>'isSummer' = ?
86+
AND contacts->>'role' IN ('Licence holder', 'Returns to')
87+
AND NOT EXISTS (
88+
SELECT
89+
1
90+
FROM public.licence_entity_roles ler
91+
WHERE
92+
ler.company_entity_id = ldh.company_entity_id
93+
AND ler."role" IN ('primary_user', 'user_returns')
94+
)
95+
UNION ALL
96+
SELECT
97+
ldh.licence_ref,
98+
(CASE
99+
WHEN ler."role" = 'primary_user' THEN 'Primary user'
100+
ELSE 'Returns agent'
101+
END) AS contact_type,
102+
le."name" AS email,
103+
(NULL) AS contact,
104+
rl.return_reference
105+
FROM public.licence_document_headers ldh
106+
INNER JOIN public.licence_entity_roles ler
107+
ON ler.company_entity_id = ldh.company_entity_id
108+
AND ler."role" IN ('primary_user', 'user_returns')
109+
INNER JOIN public.licence_entities le
110+
ON le.id = ler.licence_entity_id
111+
INNER JOIN public.return_logs rl
112+
ON rl.licence_ref = ldh.licence_ref
113+
WHERE
114+
rl.status = 'due'
115+
AND rl.due_date = ?
116+
AND rl.metadata->>'isCurrent' = 'true'
117+
AND rl.metadata->>'isSummer' = ?
118+
) contacts
119+
ORDER BY
120+
contacts.licence_ref`
121+
}
122+
123+
module.exports = {
124+
go
125+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
'use strict'
2+
3+
// Test framework dependencies
4+
const Lab = require('@hapi/lab')
5+
const Code = require('@hapi/code')
6+
7+
const { describe, it, before } = (exports.lab = Lab.script())
8+
const { expect } = Code
9+
10+
// Test helpers
11+
const LicenceDocumentHeaderSeeder = require('../../../support/seeders/licence-document-header.seeder.js')
12+
13+
// Thing under test
14+
const FetchDownloadRecipientsService = require('../../../../app/services/notifications/setup/fetch-download-recipients.service.js')
15+
16+
describe('Notifications Setup - Fetch Download Recipients service', () => {
17+
let dueDate
18+
let isSummer
19+
let testRecipients
20+
21+
before(async () => {
22+
dueDate = '2024-04-28' // This needs to differ from any other returns log tests
23+
isSummer = 'false'
24+
25+
testRecipients = await LicenceDocumentHeaderSeeder.seed(true, dueDate)
26+
})
27+
28+
describe('when there are recipients', () => {
29+
it('correctly returns "Primary user" and "Returns agent" contacts', async () => {
30+
const result = await FetchDownloadRecipientsService.go(dueDate, isSummer)
31+
32+
const primaryUser = result.find((item) => item.contact_type === 'Primary user')
33+
const returnsAgent = result.find((item) => item.contact_type === 'Returns agent')
34+
35+
expect(primaryUser).to.equal({
36+
contact: null,
37+
contact_type: 'Primary user',
38+
39+
licence_ref: testRecipients.primaryUser.licenceRef,
40+
return_reference: testRecipients.primaryUser.returnLog.returnReference
41+
})
42+
43+
expect(returnsAgent).to.equal({
44+
contact: null,
45+
contact_type: 'Returns agent',
46+
47+
licence_ref: testRecipients.primaryUser.licenceRef,
48+
return_reference: testRecipients.primaryUser.returnLog.returnReference
49+
})
50+
})
51+
52+
it('correctly returns "Licence holder" contact', async () => {
53+
const result = await FetchDownloadRecipientsService.go(dueDate, isSummer)
54+
55+
const found = result.filter((item) => item.licence_ref === testRecipients.licenceHolder.licenceRef)
56+
57+
expect(found).to.equal([
58+
{
59+
contact: {
60+
addressLine1: '4',
61+
addressLine2: 'Privet Drive',
62+
addressLine3: null,
63+
addressLine4: null,
64+
country: null,
65+
county: 'Surrey',
66+
forename: 'Harry',
67+
initials: 'J',
68+
name: 'Licence holder only',
69+
postcode: 'WD25 7LR',
70+
role: 'Licence holder',
71+
salutation: null,
72+
town: 'Little Whinging',
73+
type: 'Person'
74+
},
75+
contact_type: 'Licence holder',
76+
email: null,
77+
licence_ref: testRecipients.licenceHolder.licenceRef,
78+
return_reference: testRecipients.licenceHolder.returnLog.returnReference
79+
}
80+
])
81+
})
82+
83+
it('correctly returns duplicate "Licence holder" and "Returns to" contacts', async () => {
84+
const result = await FetchDownloadRecipientsService.go(dueDate, isSummer)
85+
86+
const found = result.filter((item) => item.licence_ref === testRecipients.licenceHolderAndReturnTo.licenceRef)
87+
88+
expect(found).to.equal([
89+
{
90+
contact: {
91+
addressLine1: '4',
92+
addressLine2: 'Privet Drive',
93+
addressLine3: null,
94+
addressLine4: null,
95+
country: null,
96+
county: 'Surrey',
97+
forename: 'Harry',
98+
initials: 'J',
99+
name: 'Licence holder and returns to',
100+
postcode: 'WD25 7LR',
101+
role: 'Licence holder',
102+
salutation: null,
103+
town: 'Little Whinging',
104+
type: 'Person'
105+
},
106+
contact_type: 'Licence holder',
107+
email: null,
108+
licence_ref: testRecipients.licenceHolderAndReturnTo.licenceRef,
109+
return_reference: testRecipients.licenceHolderAndReturnTo.returnLog.returnReference
110+
},
111+
{
112+
contact: {
113+
addressLine1: '4',
114+
addressLine2: 'Privet Drive',
115+
addressLine3: null,
116+
addressLine4: null,
117+
country: null,
118+
county: 'Surrey',
119+
forename: 'Harry',
120+
initials: 'J',
121+
name: 'Licence holder and returns to',
122+
postcode: 'WD25 7LR',
123+
role: 'Returns to',
124+
salutation: null,
125+
town: 'Little Whinging',
126+
type: 'Person'
127+
},
128+
contact_type: 'Returns to',
129+
email: null,
130+
licence_ref: testRecipients.licenceHolderAndReturnTo.licenceRef,
131+
return_reference: testRecipients.licenceHolderAndReturnTo.returnLog.returnReference
132+
}
133+
])
134+
})
135+
})
136+
})

test/support/helpers/return-log.helper.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ function defaults(data = {}) {
5757
const receivedDate = data.receivedDate ? data.receivedDate : null
5858
const startDate = data.startDate ? new Date(data.startDate) : new Date('2022-04-01')
5959
const endDate = data.endDate ? new Date(data.endDate) : new Date('2023-03-31')
60+
const dueDate = data.dueDate ? new Date(data.dueDate) : new Date('2023-04-28')
6061

6162
const defaults = {
6263
id: generateReturnLogId(startDate, endDate, 1, licenceRef, returnReference),
6364
createdAt: timestamp,
64-
dueDate: new Date('2023-04-28'),
65+
dueDate,
6566
endDate,
6667
licenceRef,
6768
metadata: {

test/support/seeders/licence-document-header.seeder.js

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,19 @@ const ReturnLogHelper = require('../helpers/return-log.helper.js')
1414
*
1515
* @param {boolean} returnLogs - defaulted to true, this needs to be false if you do not want the `licenceDocumentHeader`
1616
* to be included in the recipients list
17+
* @param {string} dueDate
1718
*
1819
* @returns {object[]} - an array of the added licenceDocumentHeaders
1920
*/
20-
async function seed(returnLogs = true) {
21+
async function seed(returnLogs = true, dueDate = '2023-04-28') {
2122
return {
22-
licenceHolder: await _addLicenceHolder(returnLogs),
23-
licenceHolderAndReturnTo: await _addLicenceHolderAndReturnToSameRef(returnLogs),
24-
primaryUser: await _addLicenceEntityRoles(returnLogs)
23+
licenceHolder: await _addLicenceHolder(returnLogs, dueDate),
24+
licenceHolderAndReturnTo: await _addLicenceHolderAndReturnToSameRef(returnLogs, dueDate),
25+
primaryUser: await _addLicenceEntityRoles(returnLogs, dueDate)
2526
}
2627
}
2728

28-
async function _addLicenceEntityRoles(returnLogs) {
29+
async function _addLicenceEntityRoles(enableReturnLog, dueDate) {
2930
const primaryUser = {
3031
name: 'Primary User test',
3132
@@ -68,16 +69,19 @@ async function _addLicenceEntityRoles(returnLogs) {
6869
role: userReturns.role
6970
})
7071

71-
if (returnLogs) {
72-
await ReturnLogHelper.add({
73-
licenceRef: licenceDocumentHeader.licenceRef
72+
let returnLog
73+
74+
if (enableReturnLog) {
75+
returnLog = await ReturnLogHelper.add({
76+
licenceRef: licenceDocumentHeader.licenceRef,
77+
dueDate
7478
})
7579
}
7680

77-
return licenceDocumentHeader
81+
return { ...licenceDocumentHeader, returnLog }
7882
}
7983

80-
async function _addLicenceHolder(returnLogs) {
84+
async function _addLicenceHolder(enableReturnLog, dueDate) {
8185
const name = 'Licence holder only'
8286
const licenceDocumentHeader = await LicenceDocumentHeaderHelper.add({
8387
metadata: {
@@ -86,16 +90,19 @@ async function _addLicenceHolder(returnLogs) {
8690
}
8791
})
8892

89-
if (returnLogs) {
90-
await ReturnLogHelper.add({
91-
licenceRef: licenceDocumentHeader.licenceRef
93+
let returnLog
94+
95+
if (enableReturnLog) {
96+
returnLog = await ReturnLogHelper.add({
97+
licenceRef: licenceDocumentHeader.licenceRef,
98+
dueDate
9299
})
93100
}
94101

95-
return licenceDocumentHeader
102+
return { ...licenceDocumentHeader, returnLog }
96103
}
97104

98-
async function _addLicenceHolderAndReturnToSameRef(returnLogs) {
105+
async function _addLicenceHolderAndReturnToSameRef(enableReturnLog, dueDate) {
99106
const name = 'Licence holder and returns to'
100107
const licenceDocumentHeader = await LicenceDocumentHeaderHelper.add({
101108
metadata: {
@@ -104,13 +111,16 @@ async function _addLicenceHolderAndReturnToSameRef(returnLogs) {
104111
}
105112
})
106113

107-
if (returnLogs) {
108-
await ReturnLogHelper.add({
109-
licenceRef: licenceDocumentHeader.licenceRef
114+
let returnLog
115+
116+
if (enableReturnLog) {
117+
returnLog = await ReturnLogHelper.add({
118+
licenceRef: licenceDocumentHeader.licenceRef,
119+
dueDate
110120
})
111121
}
112122

113-
return licenceDocumentHeader
123+
return { ...licenceDocumentHeader, returnLog }
114124
}
115125

116126
function _contact(name, role) {

0 commit comments

Comments
 (0)