diff --git a/package-lock.json b/package-lock.json index a4208a8..0d11f9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6439,12 +6439,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -8854,9 +8854,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -12939,9 +12939,9 @@ } }, "node_modules/mysql2": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz", - "integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.1.tgz", + "integrity": "sha512-6zo1T3GILsXMCex3YEu7hCz2OXLUarxFsxvFcUHWMpkPtmZLeTTWgRdc1gWyNJiYt6AxITmIf9bZDRy/jAfWew==", "dependencies": { "denque": "^2.1.0", "generate-function": "^2.3.1", @@ -17761,9 +17761,9 @@ } }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "engines": { "node": ">=8.3.0" diff --git a/src/functions/reportGen.ts b/src/functions/reportGen.ts index 8bc5cf8..a4bd3bf 100644 --- a/src/functions/reportGen.ts +++ b/src/functions/reportGen.ts @@ -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"; @@ -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 */ @@ -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[] = []; - 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 }; diff --git a/src/services/ActivitiesService.ts b/src/services/ActivitiesService.ts index d498b26..3ecacbb 100644 --- a/src/services/ActivitiesService.ts +++ b/src/services/ActivitiesService.ts @@ -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; @@ -19,6 +19,7 @@ class ActivitiesService { * @param params - getActivities query parameters */ public getActivities(params: any): Promise { + console.log(`getActivities called with params: ${JSON.stringify(params)}`); const config: IInvokeConfig = this.config.getInvokeConfig(); const invokeParams: InvocationRequest = { FunctionName: config.functions.getActivities.name, @@ -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([]); } diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index c8be884..2bf5026 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -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 @@ -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 { + 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); + 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); - }); } } diff --git a/src/services/ReportGenerationService.ts b/src/services/ReportGenerationService.ts index bca4c83..2a5abf0 100644 --- a/src/services/ReportGenerationService.ts +++ b/src/services/ReportGenerationService.ts @@ -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; @@ -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 { - 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 { + 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); + } } } diff --git a/src/services/SendATFReport.ts b/src/services/SendATFReport.ts index bb97d07..e0d59d5 100644 --- a/src/services/SendATFReport.ts +++ b/src/services/SendATFReport.ts @@ -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"; @@ -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 { + 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); } /** diff --git a/src/services/TestResultsService.ts b/src/services/TestResultsService.ts index a2af848..e1994ee 100644 --- a/src/services/TestResultsService.ts +++ b/src/services/TestResultsService.ts @@ -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; @@ -19,6 +19,7 @@ class TestResultsService { * @param params - getTestResultsByTesterStaffId query parameters */ public getTestResults(params: any): Promise { + console.debug(`inside get test results: ${JSON.stringify(params)}`); const config: IInvokeConfig = this.config.getInvokeConfig(); const invokeParams: InvocationRequest = { FunctionName: config.functions.testResults.name, @@ -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)) { @@ -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 diff --git a/tests/unit/notificationService.unitTest.ts b/tests/unit/notificationService.unitTest.ts index 514fb37..5ca6856 100644 --- a/tests/unit/notificationService.unitTest.ts +++ b/tests/unit/notificationService.unitTest.ts @@ -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"; @@ -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"); diff --git a/tests/unit/reportGenFunction.unitTest.ts b/tests/unit/reportGenFunction.unitTest.ts index 2461326..6eac9b2 100644 --- a/tests/unit/reportGenFunction.unitTest.ts +++ b/tests/unit/reportGenFunction.unitTest.ts @@ -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)); diff --git a/tests/unit/sendATFReport.unitTest.ts b/tests/unit/sendATFReport.unitTest.ts index 6576954..54d8cab 100644 --- a/tests/unit/sendATFReport.unitTest.ts +++ b/tests/unit/sendATFReport.unitTest.ts @@ -106,16 +106,14 @@ describe("sendATFReport", () => { sendATFReport.testStationsService.getTestStationEmail = jest.fn().mockResolvedValue([ { testStationPNumber: "09-4129632", - testStationEmails: ["teststationname@dvsa.gov.uk"], + testStationEmails: ["teststationname@dvsa.gov.uk", "anotherteststationname@dvsa.gov.uk"], testStationId: "9", }, ]); - expect.assertions(2); + expect.assertions(1); return sendATFReport.sendATFReport(generationServiceResponse, visit).then((response: any) => { - const notifyCallArgsTestStation = notifyMock.mock.calls[0]; - const notifyCallArgsTester = notifyMock.mock.calls[1]; - expect(notifyCallArgsTestStation[1]).toEqual(["teststationname@dvsa.gov.uk"]); - expect(notifyCallArgsTester[1]).toEqual(["test@dvsa.gov.uk"]); + const notifyCallArgs = notifyMock.mock.calls[0]; + expect(notifyCallArgs[1]).toEqual(["test@dvsa.gov.uk", "teststationname@dvsa.gov.uk", "anotherteststationname@dvsa.gov.uk"]); }); }); });