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-12690): added activities and test results types #123

Merged
merged 12 commits into from
Jul 12, 2024
Merged
718 changes: 250 additions & 468 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@
"author": "",
"license": "ISC",
"dependencies": {
"@dvsa/cvs-type-definitions": "7.1.0",
"@aws-sdk/client-lambda": "^3.549.0",
"@aws-sdk/client-s3": "^3.550.0",
"@aws-sdk/client-secrets-manager": "^3.549.0",
"@aws-sdk/util-dynamodb": "^3.592.0",
"@dvsa/cvs-microservice-common": "^0.9.8",
"@dvsa/cvs-microservice-common": "^1.1.0",
Daniel-Searle marked this conversation as resolved.
Show resolved Hide resolved
"@smithy/util-utf8": "^2.3.0",
"aws-lambda": "^1.0.7",
"aws-xray-sdk": "^3.6.0",
Expand Down
3 changes: 2 additions & 1 deletion src/functions/reportGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { LambdaService } from "../services/LambdaService";
import { ReportGenerationService } from "../services/ReportGenerationService";
import { SendATFReport } from "../services/SendATFReport";
import { TestResultsService } from "../services/TestResultsService";
import { ActivitySchema } from "@dvsa/cvs-type-definitions/types/v1/activity";

/**
* λ function to process a SQS of test results into a queue for certificate generation.
Expand All @@ -28,7 +29,7 @@ const reportGen: Handler = async (event: any, context?: Context, callback?: Call
try {
for (const record of event.Records) {
const recordBody = JSON.parse(record?.body);
const visit: any = unmarshall(recordBody?.dynamodb.NewImage);
const visit: ActivitySchema = unmarshall(recordBody?.dynamodb.NewImage) as ActivitySchema;

console.debug(`visit is: ${JSON.stringify(visit.id)}`);

Expand Down
39 changes: 1 addition & 38 deletions src/models/index.d.ts
Daniel-Searle marked this conversation as resolved.
Show resolved Hide resolved
Daniel-Searle marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -33,43 +33,6 @@ interface IMOTConfig {
templateId: string;
}

interface IActivity {
id: string;
activityType: "visit" | "wait";
testStationName: string;
testStationPNumber: string;
testStationEmail: string;
testStationType: StationType;
testerName: string;
testerStaffId: string;
startTime: string;
endTime: string;
waitReason: [string];
notes: string;
}

interface ITestType {
testTypeStartTimestamp: string;
testTypeName: string;
testResult: string;
certificateNumber: string;
testExpiryDate: number;
testTypeEndTimeStamp: string;
}

interface ITestResults {
testerStaffId: string;
vrm: string;
testStationPNumber: string;
preparerId: string;
numberOfSeats: number;
testStartTimestamp: string;
testEndTimestamp: string;
testTypes: ITestType;
vin: string;
vehicleType: string;
}

interface IInvokeConfig {
params: { apiVersion: string; endpoint?: string };
functions: { testResults: { name: string }; testStations: { name: string; mock: string }; getActivities: { name: string } };
Expand Down Expand Up @@ -102,4 +65,4 @@ interface IConfig {
invoke: IIndexInvokeConfig;
}

export { IS3Config, IActivity, IInvokeConfig, IMOTConfig, ITestResults, IActivitiesList, ISecretConfig, IConfig };
export { IS3Config, IInvokeConfig, IMOTConfig, IActivitiesList, ISecretConfig, IConfig };
7 changes: 4 additions & 3 deletions src/services/ActivitiesService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { InvocationRequest, InvocationResponse } from "@aws-sdk/client-lambda";
import { toUint8Array } from "@smithy/util-utf8";
import moment from "moment";
import { IInvokeConfig } from "../models";
import { IActivitiesList, IInvokeConfig } from "../models";
import { Configuration } from "../utils/Configuration";
import { LambdaService } from "./LambdaService";
import { ActivitySchema } from "@dvsa/cvs-type-definitions/types/v1/activity";

class ActivitiesService {
private readonly lambdaClient: LambdaService;
Expand All @@ -18,7 +19,7 @@ class ActivitiesService {
* Retrieves Activities based on the provided parameters
* @param params - getActivities query parameters
*/
public getActivities(params: any): Promise<any> {
public getActivities(params: any): Promise<ActivitySchema[]> {
console.log(`getActivities called with params: ${JSON.stringify(params)}`);
const config: IInvokeConfig = this.config.getInvokeConfig();
const invokeParams: InvocationRequest = {
Expand All @@ -41,7 +42,7 @@ class ActivitiesService {

return this.lambdaClient.invoke(invokeParams).then((response: InvocationResponse) => {
const payload: any = this.lambdaClient.validateInvocationResponse(response); // Response validation
const activityResults: any[] = JSON.parse(payload.body); // Response conversion
const activityResults: ActivitySchema[] = JSON.parse(payload.body); // Response conversion
console.log(`Activities: ${activityResults.length}`);

// Sort results by startTime
Expand Down
9 changes: 5 additions & 4 deletions src/services/ReportGenerationService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ERRORS, STATUSES } from "../assets/enum";
import { IActivity } from "../models";
import { HTTPError } from "../models/HTTPError";
import { ActivitiesService } from "./ActivitiesService";
import { TestResultsService } from "./TestResultsService";
import { ActivitySchema } from "@dvsa/cvs-type-definitions/types/v1/activity";
import { TestResultSchema } from "@dvsa/cvs-type-definitions/types/v1/test-result";

class ReportGenerationService {
private readonly testResultsService: TestResultsService;
Expand All @@ -17,10 +18,10 @@ class ReportGenerationService {
* Generates the ATF report for a given activity
* @param activity - activity for which to generate the report
*/
public async generateATFReport(activity: IActivity): Promise<any> {
public async generateATFReport(activity: ActivitySchema): Promise<{ testResults: TestResultSchema[]; waitActivities: ActivitySchema[]; }> {
console.debug("Inside generateATFReport");
try {
const testResults = await this.testResultsService
const testResults: TestResultSchema[] = await this.testResultsService
.getTestResults({
testerStaffId: activity.testerStaffId,
fromDateTime: activity.startTime,
Expand All @@ -29,7 +30,7 @@ class ReportGenerationService {
testStatus: STATUSES.SUBMITTED,
});

const waitActivities = await this.activitiesService
const waitActivities: ActivitySchema[] = await this.activitiesService
.getActivities({
testerStaffId: activity.testerStaffId,
fromStartTime: activity.startTime,
Expand Down
22 changes: 13 additions & 9 deletions src/services/SendATFReport.ts
Daniel-Searle marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { LambdaClient } from "@aws-sdk/client-lambda";
// @ts-ignore
import { NotifyClient } from "notifications-node-client";
import { ACTIVITY_TYPE } from "../assets/enum";
import { IActivitiesList, IActivity, ITestResults } from "../models";
import { IActivitiesList } from "../models";
import { Configuration } from "../utils/Configuration";
import { NotificationData } from "../utils/generateNotificationData";
import { LambdaService } from "./LambdaService";
import { NotificationService } from "./NotificationService";
import { TestStationsService } from "./TestStationsService";
import { TestResultSchema } from "@dvsa/cvs-type-definitions/types/v1/test-result";
import { ActivitySchema } from "@dvsa/cvs-type-definitions/types/v1/activity";

class SendATFReport {
public testStationsService: TestStationsService;
Expand All @@ -25,11 +27,12 @@ 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) {
public async sendATFReport(generationServiceResponse: { testResults: TestResultSchema[]; waitActivities: ActivitySchema[]; }, visit: ActivitySchema) {
// Add testResults and waitActivities in a common list and sort it by startTime
// TODO RENAME
const activitiesList = this.computeActivitiesList(generationServiceResponse.testResults, generationServiceResponse.waitActivities);

const response = await this.testStationsService.getTestStationEmail(visit.testStationPNumber);
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)}`);
Expand All @@ -40,28 +43,29 @@ class SendATFReport {
this.notifyService = new NotificationService(new NotifyClient(this.apiKey));
}

const emails = [visit.testerEmail];
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) {
emails.push(...response[0].testStationEmails);
} else {
console.log(`No email address exists for test station PNumber ${visit.testStationPNumber}`);
}
await this.notifyService.sendNotification(sendNotificationData, emails, visit.id);
await this.notifyService.sendNotification(sendNotificationData, emails, visit.id!);
}

/**
* Method to collate testResults and waitActivities into a common list
* and then sort them on startTime to display the activities in a sequence.
* @param testResultsList: testResults list
* @param waitActivitiesList: wait activities list
* @param testResultsList
* @param waitActivitiesList
*/
public computeActivitiesList(testResultsList: ITestResults[], waitActivitiesList: IActivity[]) {
public computeActivitiesList(testResultsList: TestResultSchema[], waitActivitiesList: ActivitySchema[]) {
const list: IActivitiesList[] = [];

// Adding Test activities to the list
for (const testResult of testResultsList) {
const act: IActivitiesList = {
startTime: testResult.testTypes.testTypeStartTimestamp,
startTime: testResult.testTypes[0].testTypeStartTimestamp!,
activityType: ACTIVITY_TYPE.TEST,
activity: testResult,
};
Expand Down
19 changes: 10 additions & 9 deletions src/services/TestResultsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import moment from "moment";
import { IInvokeConfig } from "../models";
import { Configuration } from "../utils/Configuration";
import { LambdaService } from "./LambdaService";
import { TestResultSchema, TestTypeSchema } from "@dvsa/cvs-type-definitions/types/v1/test-result";

class TestResultsService {
private readonly lambdaClient: LambdaService;
Expand All @@ -18,7 +19,7 @@ class TestResultsService {
* Retrieves test results based on the provided parameters
* @param params - getTestResultsByTesterStaffId query parameters
*/
public getTestResults(params: any): Promise<any> {
public getTestResults(params: any): Promise<TestResultSchema[]> {
console.debug(`inside get test results: ${JSON.stringify(params)}`);
const config: IInvokeConfig = this.config.getInvokeConfig();
const invokeParams: InvocationRequest = {
Expand All @@ -36,7 +37,7 @@ class TestResultsService {

return this.lambdaClient.invoke(invokeParams).then((response: InvocationResponse) => {
const payload: any = this.lambdaClient.validateInvocationResponse(response); // Response validation
const testResults: any[] = JSON.parse(payload.body); // Response conversion
const testResults: TestResultSchema[] = JSON.parse(payload.body); // Response conversion

console.debug(`test result response is: ${JSON.stringify(testResults)}`);
// Sort results by testTypeEndTimeStamp
Expand All @@ -61,18 +62,18 @@ class TestResultsService {
* into multiple records with a single test type
* @param testResults
*/
public expandTestResults(testResults: any): any[] {
public expandTestResults(testResults: TestResultSchema[]): TestResultSchema[] {
console.debug("Splitting test results into multiple records");
return testResults
.map((testResult: any) => {
.map((testResult: TestResultSchema) => {
// Separate each test type in a record to form multiple test results
const splittedRecords: any[] = [];
const templateRecord: any = Object.assign({}, testResult);
const splittedRecords: TestResultSchema[] = [];
const templateRecord: TestResultSchema = Object.assign({}, testResult);
Object.assign(templateRecord, {});

testResult.testTypes.forEach((testType: any, i: number, array: any[]) => {
const clonedRecord: any = Object.assign({}, templateRecord); // Create test result from template
Object.assign(clonedRecord, { testTypes: testType }); // Assign it the test type
testResult.testTypes.forEach((testType: TestTypeSchema, i: number, array: any[]) => {
const clonedRecord: TestResultSchema = Object.assign({}, templateRecord); // Create test result from template
Object.assign(clonedRecord, { testTypes: [testType] }); // Assign it the test type

splittedRecords.push(clonedRecord);
});
Expand Down
24 changes: 14 additions & 10 deletions src/utils/generateNotificationData.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,40 @@
import moment = require("moment-timezone");
import { ACTIVITY_TYPE, TIMEZONE, VEHICLE_TYPES } from "../assets/enum";
import { IActivitiesList } from "../models";
import { ActivitySchema } from "@dvsa/cvs-type-definitions/types/v1/activity";

class NotificationData {
/**
* Generates the activity details for the ATF Report template
* @param activity - activity that will be added in the email
* @param testResultsList - list of test results that will be added in the email
* @return personalization - Array that contains the entries for each activity and test result
* @param activitiesList
* @param visit
* @param activitiesList
*/
public generateActivityDetails(visit: any, activitiesList: IActivitiesList[]) {
public generateActivityDetails(visit: ActivitySchema, activitiesList: IActivitiesList[]) {
// console.log(visit)
// console.log(activitiesList)
// Populating the list details.
const personalization: any = {};
personalization.testStationPNumber = visit.testStationPNumber;
personalization.testerName = visit.testerName;
personalization.startTimeDate = this.formatDateAndTime(visit.startTime, "date");
personalization.startTime = this.formatDateAndTime(visit.startTime, "time");
personalization.endTime = this.formatDateAndTime(visit.endTime, "time");
personalization.endTime = this.formatDateAndTime(visit.endTime!, "time");
personalization.testStationName = visit.testStationName;
personalization.activityDetails = "";
for (const [index, event] of activitiesList.entries()) {
// console.log(event);
if (event.activityType === ACTIVITY_TYPE.TEST) {
const axlesSeats = event.activity.vehicleType === VEHICLE_TYPES.PSV ? event.activity.numberOfSeats : event.activity.noOfAxles;
const vrmTrailerId = event.activity.vehicleType === VEHICLE_TYPES.TRL ? event.activity.trailerId : event.activity.vrm;
personalization.activityDetails +=
`^#${this.capitalise(event.activityType)} (${vrmTrailerId})
^• Time: ${this.formatDateAndTime(event.activity.testTypes.testTypeStartTimestamp, "time")} - ${this.formatDateAndTime(event.activity.testTypes.testTypeEndTimestamp, "time")}
^• Test description: ${event.activity.testTypes.testTypeName}
^• Time: ${this.formatDateAndTime((event.activity.testTypes[0]).testTypeStartTimestamp, "time")} - ${this.formatDateAndTime((event.activity.testTypes[0]).testTypeEndTimestamp, "time")}
^• Test description: ${event.activity.testTypes[0].testTypeName}
^• Axles / Seats: ${axlesSeats}
^• Result: ${this.capitalise(event.activity.testTypes.testResult)}` +
`${event.activity.testTypes.certificateNumber ? `\n^• Certificate number: ${event.activity.testTypes.certificateNumber}` : ""}` +
`${event.activity.testTypes.testExpiryDate ? `\n^• Expiry date: ${this.formatDateAndTime(event.activity.testTypes.testExpiryDate, "date")}` : ""}` +
^• Result: ${this.capitalise((event.activity.testTypes[0]).testResult)}` +
`${(event.activity.testTypes[0]).certificateNumber ? `\n^• Certificate number: ${(event.activity.testTypes[0]).certificateNumber}` : ""}` +
`${(event.activity.testTypes[0]).testExpiryDate ? `\n^• Expiry date: ${this.formatDateAndTime((event.activity.testTypes[0]).testExpiryDate, "date")}` : ""}` +
`${index < activitiesList.length - 1 ? `\n---\n` : "\n"}`; // Add divider line if all BUT last entry
}
if (event.activityType === ACTIVITY_TYPE.TIME_NOT_TESTING) {
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/generateNotificationData.unitTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ describe("notificationData", () => {
context("when parsing the visit and the test results", () => {
it("should return a correct test stations emails", () => {
const testResultsArray = TestResultsService.prototype.expandTestResults(JSON.parse(testResultsList.body));
testResultsArray[0].testTypes.testTypeStartTimestamp = "2019-01-14T10:05:16.987Z";
testResultsArray[0].testTypes.testTypeEndTimestamp = "2019-01-14T10:40:02.987Z";
testResultsArray[0].testTypes[0].testTypeStartTimestamp = "2019-01-14T10:05:16.987Z";
testResultsArray[0].testTypes[0].testTypeEndTimestamp = "2019-01-14T10:40:02.987Z";
const waitActivitiesArray = JSON.parse(waitActivitiesList.body);
const sendNotificationData = notificationData.generateActivityDetails(visit, sendAtfReport.computeActivitiesList(testResultsArray, waitActivitiesArray));
const detailsLines = sendNotificationData.activityDetails.split("\n");
Expand Down
12 changes: 8 additions & 4 deletions tests/unit/sendATFReport.unitTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { SendATFReport } from "../../src/services/SendATFReport";
// @ts-ignore
import { NotifyClient } from "notifications-node-client";
import mockConfig from "../util/mockConfig";
import { ActivityType } from "@dvsa/cvs-type-definitions/types/v1/enums/activityType.enum";
import { TestStationTypes } from "@dvsa/cvs-type-definitions/types/v1/enums/testStationType.enum";
import { TestResultSchema } from "@dvsa/cvs-type-definitions/types/v1/test-result";
import { ActivitySchema } from "@dvsa/cvs-type-definitions/types/v1/activity";

describe("sendATFReport", () => {
mockConfig();
Expand All @@ -12,19 +16,19 @@ describe("sendATFReport", () => {
});
const visit = {
id: "5e4bd304-446e-4678-8289-d34fca9256e9",
activityType: "visit",
activityType: ActivityType.VISIT,
testStationName: "Rowe, Wunsch and Wisoky",
testStationPNumber: "87-1369569",
testStationEmail: "teststationname@dvsa.gov.uk",
testStationType: "gvts",
testStationType: TestStationTypes.GVTS,
testerName: "Gica",
testerEmail: "test@dvsa.gov.uk",
testerStaffId: "1",
startTime: "2019-01-14T08:47:33.987Z",
endTime: "2019-01-14T15:36:33.987Z",
};
const generationServiceResponse = {
waitActivities: [],
waitActivities: [] as ActivitySchema[],
testResults: [
{
testerStaffId: "1",
Expand All @@ -38,7 +42,7 @@ describe("sendATFReport", () => {
vin: "XMGDE02FS0H012345",
vehicleType: "psv",
},
],
] as unknown as TestResultSchema[],
Daniel-Searle marked this conversation as resolved.
Show resolved Hide resolved
fileName: "ATFReport_14-01-2019_0847_87-1369569_Gica.xlsx",
fileBuffer: "<Buffer 50 4b 03 04 0a 00 00 00 08 00 c6 64 c3 4e 19 f8 08 be 60 01 00 00 39 05 00 00 13 00 00 00 5b 43 6f 6e 74 65 6e 74 5f 54 79 70 65 73 5d 2e 78 6d 6c ad ... >",
};
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/testResultsService.unitTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe("TestResultsService", () => {
numberOfSeats: 45,
testStartTimestamp: "2019-01-14T10:36:33.987Z",
testEndTimestamp: "2019-01-14T10:36:33.987Z",
testTypes: {
testTypes: [{
prohibitionIssued: false,
testCode: "aas",
testNumber: "1",
Expand Down Expand Up @@ -71,7 +71,7 @@ describe("TestResultsService", () => {
name: "Annual test",
certificateLink: "http://dvsagov.co.uk",
testResult: "pass",
},
}],
vin: "XMGDE02FS0H012345",
},
];
Expand Down
Loading