-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: GEO-888 Schedule to email about expiring announcements (#702)
- Loading branch information
Showing
23 changed files
with
487 additions
and
117 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains 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
This file contains 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
This file contains 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 |
---|---|---|
|
@@ -25,19 +25,25 @@ export class ChesService { | |
private axios: Axios; | ||
private readonly apiUrl: string; | ||
|
||
constructor({ tokenUrl, clientId, clientSecret, apiUrl, clientConnectionInstance=null }) { | ||
constructor({ | ||
tokenUrl, | ||
clientId, | ||
clientSecret, | ||
apiUrl, | ||
clientConnectionInstance = null, | ||
}) { | ||
if (!tokenUrl || !clientId || !clientSecret || !apiUrl) { | ||
logger.error('Invalid configuration.', { function: 'constructor' }); | ||
throw new Error('ChesService is not configured. Check configuration.'); | ||
} | ||
if(!clientConnectionInstance){ | ||
if (!clientConnectionInstance) { | ||
this.connection = new ClientConnection({ | ||
tokenUrl, | ||
clientId, | ||
clientSecret | ||
clientSecret, | ||
}); | ||
this.axios = this.connection.getAxios(); | ||
}else{ | ||
} else { | ||
this.axios = clientConnectionInstance.getAxios(); | ||
} | ||
|
||
|
@@ -48,29 +54,27 @@ export class ChesService { | |
try { | ||
const { data, status } = await this.axios.get(`${this.apiUrl}/health`, { | ||
headers: { | ||
'Content-Type': 'application/json' | ||
} | ||
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
return { data, status }; | ||
} catch (e) { | ||
logger.error(SERVICE, e); | ||
} | ||
} | ||
|
||
|
||
|
||
async send(email: Email) { | ||
try { | ||
const { data, status } = await this.axios.post( | ||
`${this.apiUrl}/email`, | ||
email, | ||
{ | ||
headers: { | ||
'Content-Type': 'application/json' | ||
'Content-Type': 'application/json', | ||
}, | ||
maxContentLength: Infinity, | ||
maxBodyLength: Infinity | ||
} | ||
maxBodyLength: Infinity, | ||
}, | ||
); | ||
return { data, status }; | ||
} catch (e) { | ||
|
@@ -82,9 +86,9 @@ export class ChesService { | |
/** | ||
* Send an email with retries | ||
* @param email | ||
* @param retries , optional , default value 3 | ||
* @param retries , optional , default value 5 | ||
*/ | ||
async sendEmailWithRetry(email: Email, retries?: number):Promise<string> { | ||
async sendEmailWithRetry(email: Email, retries?: number): Promise<string> { | ||
const retryCount = retries || 5; | ||
try { | ||
await retry( | ||
|
@@ -95,7 +99,7 @@ export class ChesService { | |
if (status === 201) { | ||
logger.info( | ||
SERVICE, | ||
`Email sent successfully , transactionId : ${data.txId}` | ||
`Email sent successfully , transactionId : ${data.txId}`, | ||
); | ||
return data.txId; | ||
} else { | ||
|
@@ -106,8 +110,8 @@ export class ChesService { | |
} | ||
}, | ||
{ | ||
retries: retryCount | ||
} | ||
retries: retryCount, | ||
}, | ||
); | ||
} catch (e) { | ||
logger.error(SERVICE, e); | ||
|
@@ -116,19 +120,20 @@ export class ChesService { | |
} | ||
|
||
/** | ||
* Generate an email object with HTML content | ||
* Generate an email object with HTML content. Must provide emailHTMLContent, or, title and body. | ||
* @param subjectLine SUBJECT of email | ||
* @param to array of email addresses | ||
* @param title TITLE of email | ||
* @param body BODY of email | ||
* @param emailHTMLContent HTML content of email | ||
* @param emailHTMLContent (Optional) HTML content of email. | ||
* If provided, title and body is ignored. Otherwise will use title and body to create HTML content. | ||
*/ | ||
generateHtmlEmail( | ||
subjectLine: string, | ||
to: string[], | ||
title: string, | ||
body: string, | ||
emailHTMLContent?: string | ||
emailHTMLContent?: string, | ||
): Email { | ||
const emailContents = | ||
emailHTMLContent || | ||
|
@@ -149,7 +154,7 @@ export class ChesService { | |
from: '[email protected]', | ||
priority: 'normal', | ||
subject: subjectLine, | ||
to: to | ||
to: to, | ||
}; | ||
} | ||
} |
81 changes: 81 additions & 0 deletions
81
backend/src/schedulers/email-expiring-announcements-scheduler.spec.ts
This file contains 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,81 @@ | ||
import waitFor from 'wait-for-expect'; | ||
import emailExpiringAnnouncementsJob from './email-expiring-announcements-scheduler'; | ||
|
||
jest.mock('../v1/services/utils-service', () => ({ | ||
utils: { | ||
delay: jest.fn(), | ||
}, | ||
})); | ||
|
||
const mock_sendAnnouncementExpiringEmails = jest.fn(); | ||
jest.mock('../v1/services/scheduler-service', () => ({ | ||
schedulerService: { | ||
sendAnnouncementExpiringEmails: () => mock_sendAnnouncementExpiringEmails(), | ||
}, | ||
})); | ||
|
||
const mock_asyncRetry = jest.fn((fn) => fn()); | ||
jest.mock('async-retry', () => ({ | ||
__esModule: true, | ||
default: async (fn) => mock_asyncRetry(fn), | ||
})); | ||
|
||
const mock_generateHtmlEmail = jest.fn(); | ||
const mock_sendEmailWithRetry = jest.fn(); | ||
|
||
jest.mock('../external/services/ches', () => ({ | ||
__esModule: true, | ||
default: { | ||
generateHtmlEmail: (...args) => mock_generateHtmlEmail(...args), | ||
sendEmailWithRetry: (...args) => mock_sendEmailWithRetry(...args), | ||
}, | ||
})); | ||
|
||
jest.mock('cron', () => ({ | ||
CronJob: class MockCron { | ||
constructor( | ||
public expression: string, | ||
public cb, | ||
) {} | ||
async start() { | ||
return this.cb(); | ||
} | ||
}, | ||
})); | ||
|
||
jest.mock('../config', () => ({ | ||
config: { | ||
get: (key: string) => { | ||
const settings = { | ||
'server:emailExpiringAnnouncementsCronTime': '121212121', | ||
'server:schedulerTimeZone': 'America/Vancouver', | ||
'ches:enabled': true, | ||
}; | ||
|
||
return settings[key]; | ||
}, | ||
}, | ||
})); | ||
|
||
const mock_tryLock = jest.fn(); | ||
const mock_unlock = jest.fn(); | ||
jest.mock('advisory-lock', () => ({ | ||
...jest.requireActual('advisory-lock'), | ||
default: () => { | ||
return () => ({ | ||
tryLock: () => mock_tryLock(), | ||
}); | ||
}, | ||
})); | ||
|
||
describe('email-expiring-announcements-scheduler', () => { | ||
it("should run the 'send emails' function", async () => { | ||
mock_tryLock.mockReturnValue(mock_unlock); | ||
emailExpiringAnnouncementsJob.start(); | ||
await waitFor(async () => { | ||
expect(mock_tryLock).toHaveBeenCalledTimes(1); | ||
expect(mock_sendAnnouncementExpiringEmails).toHaveBeenCalled(); | ||
expect(mock_unlock).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); |
25 changes: 25 additions & 0 deletions
25
backend/src/schedulers/email-expiring-announcements-scheduler.ts
This file contains 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,25 @@ | ||
import { config } from '../config'; | ||
import { schedulerService } from '../v1/services/scheduler-service'; | ||
import { logger as log } from '../logger'; | ||
import advisoryLock from 'advisory-lock'; | ||
import { createJob } from './create-job'; | ||
|
||
const SCHEDULER_NAME = 'EmailExpiringAnnouncements'; | ||
const mutex = advisoryLock(config.get('server:databaseUrl'))( | ||
`${SCHEDULER_NAME}-lock`, | ||
); | ||
const crontime = config.get('server:emailExpiringAnnouncementsCronTime'); | ||
|
||
export default createJob( | ||
crontime, | ||
async () => { | ||
log.info(`Starting scheduled job '${SCHEDULER_NAME}'.`); | ||
await schedulerService.sendAnnouncementExpiringEmails(); | ||
log.info(`Completed scheduled job '${SCHEDULER_NAME}'.`); | ||
}, | ||
mutex, | ||
{ | ||
title: `Error in ${SCHEDULER_NAME}`, | ||
message: `Error in scheduled job: ${SCHEDULER_NAME}`, | ||
}, | ||
); |
This file contains 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
This file contains 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
This file contains 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
This file contains 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.