Skip to content
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

feat(cb2-12590): refactor to use async await pattern when sending emails #119

Merged
merged 11 commits into from
Jul 3, 2024
26 changes: 13 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 17 additions & 24 deletions src/functions/reportGen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { LambdaClient } from "@aws-sdk/client-lambda";
import { PutObjectRequest } from "@aws-sdk/client-s3";
import { unmarshall } from '@aws-sdk/util-dynamodb';
import { ServiceException } from "@smithy/smithy-client";
import { unmarshall } from "@aws-sdk/util-dynamodb";
import { Callback, Context, Handler } from "aws-lambda";
import { ERRORS } from "../assets/enum";
import { ActivitiesService } from "../services/ActivitiesService";
Expand All @@ -11,8 +10,8 @@ import { SendATFReport } from "../services/SendATFReport";
import { TestResultsService } from "../services/TestResultsService";

/**
* λ function to process a DynamoDB stream of test results into a queue for certificate generation.
* @param event - DynamoDB Stream event
* λ function to process a SQS of test results into a queue for certificate generation.
* @param event - SQS event
* @param context - λ Context
* @param callback - callback function
*/
Expand All @@ -23,33 +22,27 @@ const reportGen: Handler = async (event: any, context?: Context, callback?: Call
}
const lambdaService = new LambdaService(new LambdaClient({}));
const reportService: ReportGenerationService = new ReportGenerationService(new TestResultsService(lambdaService), new ActivitiesService(lambdaService));
const atfReportPromises: Promise<PutObjectRequest>[] = [];

const sendATFReport: SendATFReport = new SendATFReport();

event.Records.forEach((record: any) => {
const recordBody = JSON.parse(record?.body)
const visit: any = unmarshall(recordBody?.dynamodb.NewImage);
console.debug("Services injected, looping over sqs events");
try {
for (const record of event.Records) {
const recordBody = JSON.parse(record?.body);
const visit: any = unmarshall(recordBody?.dynamodb.NewImage);

if (visit) {
const atfReportPromise = reportService
.generateATFReport(visit)
.then((generationServiceResponse) => {
return sendATFReport.sendATFReport(generationServiceResponse, visit);
})
.catch((error: any) => {
console.log(error);
throw error;
});
console.debug(`visit is: ${JSON.stringify(visit.id)}`);

atfReportPromises.push(atfReportPromise);
if (visit) {
const generationServiceResponse = await reportService.generateATFReport(visit);
console.debug(`Report generated: ${JSON.stringify(generationServiceResponse)}`);
await sendATFReport.sendATFReport(generationServiceResponse, visit);
console.debug("All emails sent, terminating lambda");
}
}
});

return Promise.all(atfReportPromises).catch((error: ServiceException) => {
} catch(error) {
console.error(error);
throw error;
});
}
};

export { reportGen };
11 changes: 6 additions & 5 deletions src/services/ActivitiesService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { IInvokeConfig } from "../models";
import { InvocationRequest, InvocationResponse } from "@aws-sdk/client-lambda";
import { LambdaService } from "./LambdaService";
import { Configuration } from "../utils/Configuration";
import moment from "moment";
import { toUint8Array } from "@smithy/util-utf8";
import moment from "moment";
import { IInvokeConfig } from "../models";
import { Configuration } from "../utils/Configuration";
import { LambdaService } from "./LambdaService";

class ActivitiesService {
private readonly lambdaClient: LambdaService;
Expand All @@ -19,6 +19,7 @@ class ActivitiesService {
* @param params - getActivities query parameters
*/
public getActivities(params: any): Promise<any> {
console.log(`getActivities called with params: ${JSON.stringify(params)}`);
const config: IInvokeConfig = this.config.getInvokeConfig();
const invokeParams: InvocationRequest = {
FunctionName: config.functions.getActivities.name,
Expand All @@ -33,8 +34,8 @@ class ActivitiesService {
),
};

// TODO fail fast if activityType is not 'visit' as per CVSB-19853 - this code will be removed as part of the 'wait time epic'
if (params.activityType !== "visit") {
console.log("not a visit, resolving a promise with an empty array");
return Promise.resolve([]);
}

Expand Down
28 changes: 9 additions & 19 deletions src/services/NotificationService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { ServiceException } from "@smithy/smithy-client";
import { HTTPError } from "../models/HTTPError";
// @ts-ignore
import { NotifyClient } from "notifications-node-client";
import { Configuration } from "../utils/Configuration";
import { EMAIL_TYPE } from "../assets/enum";

/**
* Service class for Certificate Notifications
Expand All @@ -21,30 +18,23 @@ class NotificationService {
* Sending email with the certificate according to the given params
* @param params - personalization details,email and certificate
* @param emails - emails to send to
* @param emailType - email receiver type
* @param activityId - activityId for logging purposes
*/
public async sendNotification(params: any, emails: string[], emailType: string, activityId: string): Promise<any[]> {
public async sendNotification(params: any, emails: string[], activityId: string) {
const templateId: string = await this.config.getTemplateIdFromEV();
const emailDetails = {
personalisation: params,
};
const sendEmailPromise = [];

for (const email of emails) {
const sendEmail = this.notifyClient.sendEmail(templateId, email, emailDetails).then((response: any) => response.data);
sendEmailPromise.push(sendEmail);
try {
await this.notifyClient.sendEmail(templateId, email, emailDetails);
naathanbrown marked this conversation as resolved.
Show resolved Hide resolved
console.log(`report successfully sent email for PNumber ${params.testStationPNumber} with activity ${activityId}.`);
} catch (error) {
console.log(`failed to send for ${email}`);
console.error(error);
}
}

if (emailType === EMAIL_TYPE.ATF) {
console.log(`report successfully sent to ATF for PNumber ${params.testStationPNumber} with activity ${activityId}.`);
} else if (emailType === EMAIL_TYPE.VSA) {
console.log(`report successfully sent to VSA for PNumber ${params.testStationPNumber} with activity ${activityId}.`);
}

return Promise.all(sendEmailPromise).catch((error: ServiceException) => {
console.error(error);
throw new HTTPError(error.$response?.statusCode, error.message);
});
}
}

Expand Down
63 changes: 31 additions & 32 deletions src/services/ReportGenerationService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { IActivity } from "../models";
import { TestResultsService } from "./TestResultsService";
import { ERRORS, STATUSES } from "../assets/enum";
import { IActivity } from "../models";
import { HTTPError } from "../models/HTTPError";
import { ActivitiesService } from "./ActivitiesService";
import { TestResultsService } from "./TestResultsService";

class ReportGenerationService {
private readonly testResultsService: TestResultsService;
Expand All @@ -17,37 +17,36 @@ class ReportGenerationService {
* Generates the ATF report for a given activity
* @param activity - activity for which to generate the report
*/
public generateATFReport(activity: IActivity): Promise<any> {
return this.testResultsService
.getTestResults({
testerStaffId: activity.testerStaffId,
fromDateTime: activity.startTime,
toDateTime: activity.endTime,
testStationPNumber: activity.testStationPNumber,
testStatus: STATUSES.SUBMITTED,
})
.then((testResults: any) => {
// Fetch 'wait' activities for this visit activity
return this.activitiesService
.getActivities({
testerStaffId: activity.testerStaffId,
fromStartTime: activity.startTime,
toStartTime: activity.endTime,
testStationPNumber: activity.testStationPNumber,
activityType: "wait",
})
.then((waitActivities: any[]) => {
console.log(`wait Activities Size: ${waitActivities.length}`);
const totalActivitiesLen = testResults.length + waitActivities.length;
console.log(`Total Activities Len: ${totalActivitiesLen}`);
public async generateATFReport(activity: IActivity): Promise<any> {
console.debug("Inside generateATFReport");
try {
const testResults = await this.testResultsService
.getTestResults({
testerStaffId: activity.testerStaffId,
fromDateTime: activity.startTime,
toDateTime: activity.endTime,
testStationPNumber: activity.testStationPNumber,
testStatus: STATUSES.SUBMITTED,
});

const waitActivities = await this.activitiesService
.getActivities({
testerStaffId: activity.testerStaffId,
fromStartTime: activity.startTime,
toStartTime: activity.endTime,
testStationPNumber: activity.testStationPNumber,
activityType: "wait",
});

console.log(`wait Activities Size: ${waitActivities.length}`);
const totalActivitiesLen = testResults.length + waitActivities.length;
console.log(`Total Activities Len: ${totalActivitiesLen}`);

return { testResults, waitActivities };
})
.catch((error: any) => {
console.log(error);
throw new HTTPError(500, ERRORS.ATF_CANT_BE_CREATED);
});
});
return { testResults, waitActivities };
} catch (error) {
console.log(error);
throw new HTTPError(500, ERRORS.ATF_CANT_BE_CREATED);
}
}
}

Expand Down
18 changes: 11 additions & 7 deletions src/services/SendATFReport.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { LambdaClient } from "@aws-sdk/client-lambda";
// @ts-ignore
import { NotifyClient } from "notifications-node-client";
import { LambdaClient } from "@aws-sdk/client-lambda";
import { ACTIVITY_TYPE, EMAIL_TYPE } from "../assets/enum";
import { Configuration } from "../utils/Configuration";
import { ACTIVITY_TYPE } from "../assets/enum";
import { IActivitiesList, IActivity, ITestResults } from "../models";
import { LambdaService } from "./LambdaService";
import { Configuration } from "../utils/Configuration";
import { NotificationData } from "../utils/generateNotificationData";
import { LambdaService } from "./LambdaService";
import { NotificationService } from "./NotificationService";
import { TestStationsService } from "./TestStationsService";

Expand All @@ -25,25 +25,29 @@ class SendATFReport {
* @param generationServiceResponse - The response from the ATF generation service
* @param visit - Data about the current visit
*/
public async sendATFReport(generationServiceResponse: any, visit: any): Promise<any> {
public async sendATFReport(generationServiceResponse: any, visit: any) {
// Add testResults and waitActivities in a common list and sort it by startTime
const activitiesList = this.computeActivitiesList(generationServiceResponse.testResults, generationServiceResponse.waitActivities);

const response = await this.testStationsService.getTestStationEmail(visit.testStationPNumber);
console.debug("get test stations responded");
const sendNotificationData = this.notificationData.generateActivityDetails(visit, activitiesList);
console.debug(`send notification data: ${JSON.stringify(sendNotificationData)}`);
if (!this.notifyService) {
if (!this.apiKey) {
this.apiKey = (await Configuration.getInstance().getGovNotifyConfig()).api_key;
}
this.notifyService = new NotificationService(new NotifyClient(this.apiKey));
}

const emails = [visit.testerEmail];
// VTM allows blank email addresses on a test-station record so check before sending
if (response[0].testStationEmails && response[0].testStationEmails.length > 0) {
await this.notifyService.sendNotification(sendNotificationData, response[0].testStationEmails, EMAIL_TYPE.ATF, visit.id);
emails.push(...response[0].testStationEmails);
} else {
console.log(`No email address exists for test station PNumber ${visit.testStationPNumber}`);
}
return this.notifyService.sendNotification(sendNotificationData, [visit.testerEmail], EMAIL_TYPE.VSA, visit.id);
await this.notifyService.sendNotification(sendNotificationData, emails, visit.id);
}

/**
Expand Down
11 changes: 7 additions & 4 deletions src/services/TestResultsService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { IInvokeConfig } from "../models";
import { InvocationRequest, InvocationResponse } from "@aws-sdk/client-lambda";
import { LambdaService } from "./LambdaService";
import { Configuration } from "../utils/Configuration";
import moment from "moment";
import { toUint8Array } from "@smithy/util-utf8";
import moment from "moment";
import { IInvokeConfig } from "../models";
import { Configuration } from "../utils/Configuration";
import { LambdaService } from "./LambdaService";

class TestResultsService {
private readonly lambdaClient: LambdaService;
Expand All @@ -19,6 +19,7 @@ class TestResultsService {
* @param params - getTestResultsByTesterStaffId query parameters
*/
public getTestResults(params: any): Promise<any> {
console.debug(`inside get test results: ${JSON.stringify(params)}`);
const config: IInvokeConfig = this.config.getInvokeConfig();
const invokeParams: InvocationRequest = {
FunctionName: config.functions.testResults.name,
Expand All @@ -37,6 +38,7 @@ class TestResultsService {
const payload: any = this.lambdaClient.validateInvocationResponse(response); // Response validation
const testResults: any[] = JSON.parse(payload.body); // Response conversion

console.debug(`test result response is: ${JSON.stringify(testResults)}`);
// Sort results by testTypeEndTimeStamp
testResults.sort((first: any, second: any): number => {
if (moment(first.testTypes[0].testTypeEndTimeStamp).isBefore(second.testTypes[0].testTypeEndTimeStamp)) {
Expand All @@ -60,6 +62,7 @@ class TestResultsService {
* @param testResults
*/
public expandTestResults(testResults: any): any[] {
console.debug("Splitting test results into multiple records");
return testResults
.map((testResult: any) => {
// Separate each test type in a record to form multiple test results
Expand Down
3 changes: 1 addition & 2 deletions tests/unit/notificationService.unitTest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { EMAIL_TYPE } from "../../src/assets/enum";
import { NotificationService } from "../../src/services/NotificationService";
import { SendATFReport } from "../../src/services/SendATFReport";
import { TestResultsService } from "../../src/services/TestResultsService";
Expand Down Expand Up @@ -34,7 +33,7 @@ describe("notification service", () => {
});
const notifyService: NotificationService = new NotificationService(new notifyClientMock());

await notifyService.sendNotification(sendNotificationData, ["test@test.com"], EMAIL_TYPE.VSA, "3124124-12341243");
await notifyService.sendNotification(sendNotificationData, ["test@test.com"], "3124124-12341243");
const args = sendEmailMock.mock.calls[0];
const personalisation = args[2].personalisation;
expect(args[0]).toEqual("306d864b-a56d-49eb-b3cc-6d23cf8bcc26");
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/reportGenFunction.unitTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SendATFReport } from "../../src/services/SendATFReport";
jest.mock("../../src/services/ReportGenerationService");
jest.mock("../../src/services/SendATFReport");

const mockPayload = '{"eventID":"f9e63bf29bd6adf174e308201a97259f","eventName":"MODIFY","eventVersion":"1.1","eventSource":"aws:dynamodb","awsRegion":"eu-west-1","dynamodb":{"ApproximateCreationDateTime":1711549645,"Keys":{"id":{"S":"6e4bd304-446e-4678-8289-dasdasjkl"}},"NewImage":{"testerStaffId":{"S":"132"},"testStationPNumber":{"S":"87-1369564"},"testerEmail":{"S":"tester@dvsa.gov.uk1111"},"testStationType":{"S":"gvts"},"testStationEmail":{"S":"teststationname@dvsa.gov.uk"},"startTime":{"S":"2022-01-01T10:00:40.561Z"},"endTime":{"S":"2022-01-01T10:00:40.561Z"},"id":{"S":"6e4bd304-446e-4678-8289-dasdasjkl"},"testStationName":{"S":"Rowe, Wunsch and Wisoky"},"activityType":{"S":"visit"},"activityDay":{"S":"2022-01-01"},"testerName":{"S":"namey mcname"}},"OldImage":{"testerStaffId":{"S":"132"},"testStationPNumber":{"S":"87-1369564"},"testerEmail":{"S":"tester@dvsa.gov.uk1111"},"testStationType":{"S":"gvts"},"testStationEmail":{"S":"teststationname@dvsa.gov.uk"},"startTime":{"S":"2022-01-01T10:00:40.561Z"},"endTime":{"S":"2022-01-01T10:00:40.561Z"},"id":{"S":"6e4bd304-446e-4678-8289-dasdasjkl"},"testStationName":{"S":"Rowe, Wunsch and Wisoky"},"activityType":{"S":"visit"},"activityDay":{"S":"2022-01-01"},"testerName":{"S":"231232132"}},"SequenceNumber":"1234","SizeBytes":704,"StreamViewType":"NEW_AND_OLD_IMAGES"},"eventSourceARN":"arn:aws::eu--1::/cvs---//:32:37.491"}'
const mockPayload = '{"eventID":"f9e63bf29bd6adf174e308201a97259f","eventName":"MODIFY","eventVersion":"1.1","eventSource":"aws:dynamodb","awsRegion":"eu-west-1","dynamodb":{"ApproximateCreationDateTime":1711549645,"Keys":{"id":{"S":"6e4bd304-446e-4678-8289-dasdasjkl"}},"NewImage":{"testerStaffId":{"S":"132"},"testStationPNumber":{"S":"87-1369564"},"testerEmail":{"S":"tester@dvsa.gov.uk1111"},"testStationType":{"S":"gvts"},"testStationEmail":{"S":"teststationname@dvsa.gov.uk"},"startTime":{"S":"2022-01-01T10:00:40.561Z"},"endTime":{"S":"2022-01-01T10:00:40.561Z"},"id":{"S":"6e4bd304-446e-4678-8289-dasdasjkl"},"testStationName":{"S":"Rowe, Wunsch and Wisoky"},"activityType":{"S":"visit"},"activityDay":{"S":"2022-01-01"},"testerName":{"S":"namey mcname"}},"OldImage":{"testerStaffId":{"S":"132"},"testStationPNumber":{"S":"87-1369564"},"testerEmail":{"S":"tester@dvsa.gov.uk1111"},"testStationType":{"S":"gvts"},"testStationEmail":{"S":"teststationname@dvsa.gov.uk"},"startTime":{"S":"2022-01-01T10:00:40.561Z"},"endTime":{"S":"2022-01-01T10:00:40.561Z"},"id":{"S":"6e4bd304-446e-4678-8289-dasdasjkl"},"testStationName":{"S":"Rowe, Wunsch and Wisoky"},"activityType":{"S":"visit"},"activityDay":{"S":"2022-01-01"},"testerName":{"S":"231232132"}},"SequenceNumber":"1234","SizeBytes":704,"StreamViewType":"NEW_AND_OLD_IMAGES"},"eventSourceARN":"arn:aws::eu--1::/cvs---//:32:37.491"}';

describe("Retro Gen Function", () => {
beforeAll(() => jest.setTimeout(60000));
Expand Down
Loading
Loading