-
Notifications
You must be signed in to change notification settings - Fork 0
Add FetchDownloadRecipientsService #1678
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jonathangoulding
merged 8 commits into
main
from
feature-add-fetch-download-recipients-service
Feb 4, 2025
Merged
Changes from 3 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1c2c15b
Add FetchDownloadRecipientsService
jonathangoulding 3597677
feature: add all relevant return log data
jonathangoulding 964f5b3
feature: add all relevant return log data
jonathangoulding a50f823
chore: pre pr checks
jonathangoulding 0ef428b
chore: pre pr checks
jonathangoulding e1598aa
Merge branch 'main' into feature-add-fetch-download-recipients-service
jonathangoulding acbce52
Update app/services/notifications/setup/fetch-download-recipients.ser…
jonathangoulding 9dead9f
Merge branch 'main' into feature-add-fetch-download-recipients-service
jonathangoulding File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
136 changes: 136 additions & 0 deletions
136
app/services/notifications/setup/fetch-download-recipients.service.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
'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). | ||
jonathangoulding marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* @param {Date} dueDate | ||
* @param {boolean} summer | ||
* | ||
* @returns {Promise<object[]>} - 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, | ||
jonathangoulding marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
} |
155 changes: 155 additions & 0 deletions
155
test/services/notifications/setup/fetch-download-recipients.service.test.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: '[email protected]', | ||
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: '[email protected]', | ||
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 | ||
} | ||
]) | ||
}) | ||
}) | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.