From a7b62bb9a6f8bcc1b9e9ddd92e056c54ee3b223a Mon Sep 17 00:00:00 2001 From: Matt Waldron Date: Thu, 12 Sep 2024 10:01:56 +0100 Subject: [PATCH] Standardise linting to biome --- biome.json | 25 + package-lock.json | 163 ++ package.json | 6 +- src/functions/certGen.ts | 102 +- src/handler.ts | 13 +- src/models/Enums.ts | 205 +- src/models/HTTPError.ts | 26 +- src/models/IDefectChild.ts | 8 +- src/models/IDefectParent.ts | 10 +- src/models/IFlatDefect.ts | 20 +- src/models/IItem.ts | 10 +- src/models/ISecret.ts | 4 +- src/models/ITestStations.ts | 30 +- src/models/Types.ts | 97 +- src/models/index.d.ts | 480 ++-- src/models/injector/Injector.ts | 41 +- src/models/injector/ServiceDecorator.ts | 10 +- src/services/CertificateGenerationService.ts | 2690 +++++++++--------- src/services/CertificateUploadService.ts | 98 +- src/services/LambdaService.ts | 115 +- src/services/S3BucketService.ts | 150 +- src/utils/Configuration.ts | 190 +- 22 files changed, 2252 insertions(+), 2241 deletions(-) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..72c50658 --- /dev/null +++ b/biome.json @@ -0,0 +1,25 @@ +{ + "extends": ["@dvsa/biome-config/biome"], + "linter": { + "rules": { + "suspicious": { + "noExplicitAny": "off" + }, + "complexity": { + "noForEach": "off", + "noUselessCatch": "off", + "noThisInStatic": "off" + }, + "correctness": { + "noSwitchDeclarations": "off" + }, + "style": { + "noUnusedTemplateLiteral": "off", + "noNonNullAssertion": "off", + "noUselessElse": "off", + "useTemplate": "off", + "useNodejsImportProtocol": "off" + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 50f6fee6..f45699b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,8 +27,10 @@ "ts-node-register": "^1.0.0" }, "devDependencies": { + "@biomejs/biome": "^1.8.3", "@commitlint/cli": "^19.2.1", "@commitlint/config-conventional": "^19.1.0", + "@dvsa/biome-config": "^0.1.0", "@types/aws-lambda": "^8.10.34", "@types/jest": "^28.1.8", "@types/jest-plugin-context": "^2.9.2", @@ -1761,6 +1763,161 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@biomejs/biome": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.8.3.tgz", + "integrity": "sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.8.3", + "@biomejs/cli-darwin-x64": "1.8.3", + "@biomejs/cli-linux-arm64": "1.8.3", + "@biomejs/cli-linux-arm64-musl": "1.8.3", + "@biomejs/cli-linux-x64": "1.8.3", + "@biomejs/cli-linux-x64-musl": "1.8.3", + "@biomejs/cli-win32-arm64": "1.8.3", + "@biomejs/cli-win32-x64": "1.8.3" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.3.tgz", + "integrity": "sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.3.tgz", + "integrity": "sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.3.tgz", + "integrity": "sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.3.tgz", + "integrity": "sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.3.tgz", + "integrity": "sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.3.tgz", + "integrity": "sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.3.tgz", + "integrity": "sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.3.tgz", + "integrity": "sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@commitlint/cli": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.2.2.tgz", @@ -2022,6 +2179,12 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@dvsa/biome-config": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@dvsa/biome-config/-/biome-config-0.1.0.tgz", + "integrity": "sha512-eHJ5UN+klqWgwyHrcuIXQpNy0YsP8AELR8UhLaIdMr0/sPZgyZBD5YRhv54GFjh94UYWKhEbc1qyltW7imupwg==", + "dev": true + }, "node_modules/@dvsa/cvs-feature-flags": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@dvsa/cvs-feature-flags/-/cvs-feature-flags-0.13.0.tgz", diff --git a/package.json b/package.json index b23d076c..9a3be8a3 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "test:unit": "BRANCH=local AWS_XRAY_CONTEXT_MISSING=LOG_ERROR jest --testMatch=\"**/*.unitTest.ts\" --runInBand", "test": "npm run test:unit -- --coveragePathIgnorePatterns='/tests/' --coverage", "test-i": "BRANCH=local jest --testMatch=\"**/*.intTest.ts\" --runInBand", - "lint": "tslint src/**/*.ts tests/**/*.ts -q", - "lint-fix": "tslint src/**/*.ts tests/**/*.ts -q --fix", + "lint": "biome check src", + "lint:fix": "npm run lint -- --write", "security-checks": "git secrets --scan", "prepush": "npm run test && npm run build && npm run test-i", "sonar-scanner": "npm run test && sonar-scanner", @@ -67,8 +67,10 @@ "ts-node-register": "^1.0.0" }, "devDependencies": { + "@biomejs/biome": "^1.8.3", "@commitlint/cli": "^19.2.1", "@commitlint/config-conventional": "^19.1.0", + "@dvsa/biome-config": "^0.1.0", "@types/aws-lambda": "^8.10.34", "@types/jest": "^28.1.8", "@types/jest-plugin-context": "^2.9.2", diff --git a/src/functions/certGen.ts b/src/functions/certGen.ts index a8804709..96646b52 100644 --- a/src/functions/certGen.ts +++ b/src/functions/certGen.ts @@ -1,12 +1,9 @@ -import { DeleteObjectCommandOutput, PutObjectCommandOutput } from "@aws-sdk/client-s3"; -import { Callback, Context, Handler, SQSEvent, SQSRecord } from "aws-lambda"; -import { ERRORS } from "../models/Enums"; -import { Injector } from "../models/injector/Injector"; -import { - CertificateGenerationService, - IGeneratedCertificateResponse, -} from "../services/CertificateGenerationService"; -import { CertificateUploadService } from "../services/CertificateUploadService"; +import { DeleteObjectCommandOutput, PutObjectCommandOutput } from '@aws-sdk/client-s3'; +import { Callback, Context, Handler, SQSEvent, SQSRecord } from 'aws-lambda'; +import { ERRORS } from '../models/Enums'; +import { Injector } from '../models/injector/Injector'; +import { CertificateGenerationService, IGeneratedCertificateResponse } from '../services/CertificateGenerationService'; +import { CertificateUploadService } from '../services/CertificateUploadService'; type CertGenReturn = PutObjectCommandOutput | DeleteObjectCommandOutput; @@ -16,60 +13,47 @@ type CertGenReturn = PutObjectCommandOutput | DeleteObjectCommandOutput; * @param context - λ Context * @param callback - callback function */ -const certGen: Handler = async ( - event: SQSEvent, - context?: Context, - callback?: Callback -): Promise => { - if ( - !event || - !event.Records || - !Array.isArray(event.Records) || - !event.Records.length - ) { - console.error("ERROR: event is not defined."); - throw new Error("Event is empty"); - } +const certGen: Handler = async (event: SQSEvent, context?: Context, callback?: Callback): Promise => { + if (!event || !event.Records || !Array.isArray(event.Records) || !event.Records.length) { + console.error('ERROR: event is not defined.'); + throw new Error('Event is empty'); + } - const certificateGenerationService: CertificateGenerationService = - Injector.resolve( - CertificateGenerationService - ); - const certificateUploadService: CertificateUploadService = - Injector.resolve(CertificateUploadService); - const certificateUploadPromises: Array> = []; + const certificateGenerationService: CertificateGenerationService = + Injector.resolve(CertificateGenerationService); + const certificateUploadService: CertificateUploadService = + Injector.resolve(CertificateUploadService); + const certificateUploadPromises: Array> = []; - event.Records.forEach((record: SQSRecord) => { - const testResult: any = JSON.parse(record.body); - console.log(`parsed test result is ${testResult.testResultId} with the system number ${testResult.systemNumber}`); - if (testResult.testStatus === "cancelled") { - const s3DeletePromise = - certificateUploadService.removeCertificate(testResult); - certificateUploadPromises.push(s3DeletePromise); - } else if ( - testResult.testResultId.match( - "\\b[a-zA-Z0-9]{8}\\b-\\b[a-zA-Z0-9]{4}\\b-\\b[a-zA-Z0-9]{4}\\b-\\b[a-zA-Z0-9]{4}\\b-\\b[a-zA-Z0-9]{12}\\b" - ) - ) { - // Check for retroError flag for a testResult and cvsTestUpdated for the test-type and do not generate certificates if set to true - const generatedCertificateResponse: Promise = - certificateGenerationService - .generateCertificate(testResult) - .then((response: IGeneratedCertificateResponse) => { - return certificateUploadService.uploadCertificate(response); - }); + event.Records.forEach((record: SQSRecord) => { + const testResult: any = JSON.parse(record.body); + console.log(`parsed test result is ${testResult.testResultId} with the system number ${testResult.systemNumber}`); + if (testResult.testStatus === 'cancelled') { + const s3DeletePromise = certificateUploadService.removeCertificate(testResult); + certificateUploadPromises.push(s3DeletePromise); + } else if ( + testResult.testResultId.match( + '\\b[a-zA-Z0-9]{8}\\b-\\b[a-zA-Z0-9]{4}\\b-\\b[a-zA-Z0-9]{4}\\b-\\b[a-zA-Z0-9]{4}\\b-\\b[a-zA-Z0-9]{12}\\b' + ) + ) { + // Check for retroError flag for a testResult and cvsTestUpdated for the test-type and do not generate certificates if set to true + const generatedCertificateResponse: Promise = certificateGenerationService + .generateCertificate(testResult) + .then((response: IGeneratedCertificateResponse) => { + return certificateUploadService.uploadCertificate(response); + }); - certificateUploadPromises.push(generatedCertificateResponse); - } else { - console.error(`${ERRORS.TESTRESULT_ID}`, testResult.testResultId); - throw new Error("Bad Test Record: " + testResult.testResultId); - } - }); + certificateUploadPromises.push(generatedCertificateResponse); + } else { + console.error(`${ERRORS.TESTRESULT_ID}`, testResult.testResultId); + throw new Error('Bad Test Record: ' + testResult.testResultId); + } + }); - return Promise.all(certificateUploadPromises).catch((error: Error) => { - console.error(error); - throw error; - }); + return Promise.all(certificateUploadPromises).catch((error: Error) => { + console.error(error); + throw error; + }); }; export { certGen }; diff --git a/src/handler.ts b/src/handler.ts index 9c7c191b..7c31ed36 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -1,15 +1,14 @@ -import { certGen } from "./functions/certGen"; +import { certGen } from './functions/certGen'; -const isOffline: boolean = - !process.env.BRANCH || process.env.BRANCH === "local"; +const isOffline: boolean = !process.env.BRANCH || process.env.BRANCH === 'local'; let credentials = {}; if (isOffline) { - credentials = { - accessKeyId: "accessKey1", - secretAccessKey: "verySecretKey1", - }; + credentials = { + accessKeyId: 'accessKey1', + secretAccessKey: 'verySecretKey1', + }; } export { credentials }; diff --git a/src/models/Enums.ts b/src/models/Enums.ts index 13d14c25..c7751e2b 100644 --- a/src/models/Enums.ts +++ b/src/models/Enums.ts @@ -1,156 +1,125 @@ export enum ERRORS { - TESTRESULT_ID = "Record does not have valid testResultId for certificate generation.", - LAMBDA_INVOCATION_ERROR = "Lambda invocation returned error:", - EMPTY_PAYLOAD = "with empty payload.", - LAMBDA_INVOCATION_BAD_DATA = "Lambda invocation returned bad data:", - RETRO_ERROR_OR_CVS_UPDATED = "Not eligible for certificate generation.", - SECRET_ENV_VAR_NOT_EXIST = "SECRET_KEY environment variable does not exist.", - SECRET_DETAILS_NOT_FOUND = "No secret details found.", - ADDRESS_BOOLEAN_DOES_NOT_EXIST = "Payload does not include boolean value for isWelshAddress: " + TESTRESULT_ID = 'Record does not have valid testResultId for certificate generation.', + LAMBDA_INVOCATION_ERROR = 'Lambda invocation returned error:', + EMPTY_PAYLOAD = 'with empty payload.', + LAMBDA_INVOCATION_BAD_DATA = 'Lambda invocation returned bad data:', + RETRO_ERROR_OR_CVS_UPDATED = 'Not eligible for certificate generation.', + SECRET_ENV_VAR_NOT_EXIST = 'SECRET_KEY environment variable does not exist.', + SECRET_DETAILS_NOT_FOUND = 'No secret details found.', + ADDRESS_BOOLEAN_DOES_NOT_EXIST = 'Payload does not include boolean value for isWelshAddress: ', } export enum VEHICLE_TYPES { - PSV = "psv", - HGV = "hgv", - TRL = "trl", + PSV = 'psv', + HGV = 'hgv', + TRL = 'trl', } export enum TEST_RESULTS { - PASS = "pass", - FAIL = "fail", - PRS = "prs", + PASS = 'pass', + FAIL = 'fail', + PRS = 'prs', } export enum CERTIFICATE_DATA { - RWT_DATA = "RWT_DATA", - PASS_DATA = "PASS_DATA", - FAIL_DATA = "FAIL_DATA", - ADR_DATA = "ADR_DATA", - IVA_DATA = "IVA_DATA", - MSVA_DATA = "MSVA_DATA", + RWT_DATA = 'RWT_DATA', + PASS_DATA = 'PASS_DATA', + FAIL_DATA = 'FAIL_DATA', + ADR_DATA = 'ADR_DATA', + IVA_DATA = 'IVA_DATA', + MSVA_DATA = 'MSVA_DATA', } export enum LOCATION_ENGLISH { - FRONT = "front", - REAR = "rear", - UPPER = "upper", - LOWER = "lower", - NEARSIDE = "nearside", - OFFSIDE = "offside", - CENTRE = "centre", - INNER = "inner", - OUTER = "outer" + FRONT = 'front', + REAR = 'rear', + UPPER = 'upper', + LOWER = 'lower', + NEARSIDE = 'nearside', + OFFSIDE = 'offside', + CENTRE = 'centre', + INNER = 'inner', + OUTER = 'outer', } export enum LOCATION_WELSH { - FRONT = "blaen", - REAR = "cefn", - UPPER = "uchaf", - LOWER = "isaf", - NEARSIDE = "ochr mewnol", - OFFSIDE = "allanol", - CENTRE = "canol", - INNER = "mewnol", - OUTER = "allanol", - ROW_NUMBER = "Rhesi", - SEAT_NUMBER = "Seddi", - AXLE_NUMBER = "Echelau" + FRONT = 'blaen', + REAR = 'cefn', + UPPER = 'uchaf', + LOWER = 'isaf', + NEARSIDE = 'ochr mewnol', + OFFSIDE = 'allanol', + CENTRE = 'canol', + INNER = 'mewnol', + OUTER = 'allanol', + ROW_NUMBER = 'Rhesi', + SEAT_NUMBER = 'Seddi', + AXLE_NUMBER = 'Echelau', } export enum IVA_30 { - BASIC = "Basic", - NORMAL = "Normal", - EMPTY_CUSTOM_DEFECTS = "N/A" + BASIC = 'Basic', + NORMAL = 'Normal', + EMPTY_CUSTOM_DEFECTS = 'N/A', } export const ADR_TEST = { - IDS: [ - "50", - "59", - "60" - ] + IDS: ['50', '59', '60'], }; export const BASIC_IVA_TEST = { - IDS: [ - "125", - "129", - "154", - "158", - "159", - "185" - ] + IDS: ['125', '129', '154', '158', '159', '185'], }; export const IVA30_TEST = { - IDS: [ - "125", - "126", - "128", - "129", - "130", - "153", - "154", - "158", - "159", - "161", - "162", - "163", - "184", - "185", - "186", - "187", - "188", - "189", - "190", - "191", - "192", - "193", - "194", - "195", - "196", - "197", - ] + IDS: [ + '125', + '126', + '128', + '129', + '130', + '153', + '154', + '158', + '159', + '161', + '162', + '163', + '184', + '185', + '186', + '187', + '188', + '189', + '190', + '191', + '192', + '193', + '194', + '195', + '196', + '197', + ], }; export const MSVA30_TEST = { - IDS: [ - "133", - "134", - "135", - "136", - "138", - "139", - "140", - "166", - "167", - "169", - "170", - "172", - "173" - ] + IDS: ['133', '134', '135', '136', '138', '139', '140', '166', '167', '169', '170', '172', '173'], }; export const HGV_TRL_ROADWORTHINESS_TEST_TYPES = { - IDS: [ - "62", - "63", - "91", - "101", - "122" - ] + IDS: ['62', '63', '91', '101', '122'], }; export const AVAILABLE_WELSH = { - CERTIFICATES: [ - "hgv_pass", - "trl_pass", - "psv_pass", - "hgv_fail", - "trl_fail", - "psv_fail", - "hgv_prs", - "trl_prs", - "psv_prs", - ] + CERTIFICATES: [ + 'hgv_pass', + 'trl_pass', + 'psv_pass', + 'hgv_fail', + 'trl_fail', + 'psv_fail', + 'hgv_prs', + 'trl_prs', + 'psv_prs', + ], }; diff --git a/src/models/HTTPError.ts b/src/models/HTTPError.ts index c366f21a..2e457535 100644 --- a/src/models/HTTPError.ts +++ b/src/models/HTTPError.ts @@ -2,19 +2,19 @@ * Defines a throwable subclass of Error used for signaling an HTTP status code. */ class HTTPError extends Error { - private statusCode = 0; - private body = ""; - /** - * Constructor for the HTTPResponseError class - * @param statusCode the HTTP status code - * @param body - the response body - * @param headers - optional - the response headers - */ - constructor(statusCode: number, body: string) { - super(); - this.statusCode = statusCode; - this.body = body; - } + private statusCode = 0; + private body = ''; + /** + * Constructor for the HTTPResponseError class + * @param statusCode the HTTP status code + * @param body - the response body + * @param headers - optional - the response headers + */ + constructor(statusCode: number, body: string) { + super(); + this.statusCode = statusCode; + this.body = body; + } } export { HTTPError }; diff --git a/src/models/IDefectChild.ts b/src/models/IDefectChild.ts index d02d5407..bbe9c02d 100644 --- a/src/models/IDefectChild.ts +++ b/src/models/IDefectChild.ts @@ -1,6 +1,6 @@ export interface IDefectChild { - ref?: string; - deficiencyText?: string; - deficiencyTextWelsh?: string; - forVehicleType?: string[]; + ref?: string; + deficiencyText?: string; + deficiencyTextWelsh?: string; + forVehicleType?: string[]; } diff --git a/src/models/IDefectParent.ts b/src/models/IDefectParent.ts index 618dd5e5..b9a7f65b 100644 --- a/src/models/IDefectParent.ts +++ b/src/models/IDefectParent.ts @@ -1,8 +1,8 @@ -import {IItem} from "./IItem"; +import { IItem } from './IItem'; export interface IDefectParent { - imNumber?: number; - imDescription?: string; - imDescriptionWelsh?: string; - items: IItem[]; + imNumber?: number; + imDescription?: string; + imDescriptionWelsh?: string; + items: IItem[]; } diff --git a/src/models/IFlatDefect.ts b/src/models/IFlatDefect.ts index 0a69764e..7e040c83 100644 --- a/src/models/IFlatDefect.ts +++ b/src/models/IFlatDefect.ts @@ -1,12 +1,12 @@ export interface IFlatDefect { - imNumber?: number; - imDescription?: string; - imDescriptionWelsh?: string; - itemNumber?: number; - itemDescription?: string; - itemDescriptionWelsh?: string; - ref?: string; - deficiencyText?: string; - deficiencyTextWelsh?: string; - forVehicleType?: string[]; + imNumber?: number; + imDescription?: string; + imDescriptionWelsh?: string; + itemNumber?: number; + itemDescription?: string; + itemDescriptionWelsh?: string; + ref?: string; + deficiencyText?: string; + deficiencyTextWelsh?: string; + forVehicleType?: string[]; } diff --git a/src/models/IItem.ts b/src/models/IItem.ts index 3045b116..6a1d0ecf 100644 --- a/src/models/IItem.ts +++ b/src/models/IItem.ts @@ -1,8 +1,8 @@ -import {IDefectChild} from "./IDefectChild"; +import { IDefectChild } from './IDefectChild'; export interface IItem { - itemNumber?: number; - itemDescription?: string; - itemDescriptionWelsh?: string; - deficiencies: IDefectChild[]; + itemNumber?: number; + itemDescription?: string; + itemDescriptionWelsh?: string; + deficiencies: IDefectChild[]; } diff --git a/src/models/ISecret.ts b/src/models/ISecret.ts index 6758b072..892e4c0a 100644 --- a/src/models/ISecret.ts +++ b/src/models/ISecret.ts @@ -1,4 +1,4 @@ export interface ISecret { - url: string; - key: string; + url: string; + key: string; } diff --git a/src/models/ITestStations.ts b/src/models/ITestStations.ts index 4c9b4661..4a36077f 100644 --- a/src/models/ITestStations.ts +++ b/src/models/ITestStations.ts @@ -1,17 +1,17 @@ export interface ITestStation { - testStationId: string; - testStationPNumber: string; - testStationStatus: string; - testStationName: string; - testStationContactNumber: string; - testStationAccessNotes: string; - testStationGeneralNotes: string; - testStationTown: string; - testStationAddress: string; - testStationPostcode: string; - testStationCountry?: string; - testStationLongitude: number; - testStationLatitude: number; - testStationType: string; - testStationEmails: string[]; + testStationId: string; + testStationPNumber: string; + testStationStatus: string; + testStationName: string; + testStationContactNumber: string; + testStationAccessNotes: string; + testStationGeneralNotes: string; + testStationTown: string; + testStationAddress: string; + testStationPostcode: string; + testStationCountry?: string; + testStationLongitude: number; + testStationLatitude: number; + testStationType: string; + testStationEmails: string[]; } diff --git a/src/models/Types.ts b/src/models/Types.ts index 8f925ae5..937d5a6f 100644 --- a/src/models/Types.ts +++ b/src/models/Types.ts @@ -1,56 +1,53 @@ -import { TechRecordCompleteCarSchema } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/car/complete"; -import { TechRecordSkeletonCarSchema } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/car/skeleton"; -import { GETHGVTechnicalRecordV3Complete } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/complete"; -import { GETHGVTechnicalRecordV3Skeleton } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/skeleton"; -import { GETHGVTechnicalRecordV3Testable } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/testable"; -import { - TechRecordCompleteMotorcycleSchema, -} from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/motorcycle/complete"; -import { GETPSVTechnicalRecordV3Complete } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/psv/complete"; -import { GETPSVTechnicalRecordV3Skeleton } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/psv/skeleton"; -import { GETPSVTechnicalRecordV3Testable } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/psv/testable"; -import { GETTRLTechnicalRecordV3Complete } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/trl/complete"; -import { GETTRLTechnicalRecordV3Skeleton } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/trl/skeleton"; -import { GETTRLTechnicalRecordV3Testable } from "@dvsa/cvs-type-definitions/types/v3/tech-record/get/trl/testable"; +import { TechRecordCompleteCarSchema } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/car/complete'; +import { TechRecordSkeletonCarSchema } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/car/skeleton'; +import { GETHGVTechnicalRecordV3Complete } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/complete'; +import { GETHGVTechnicalRecordV3Skeleton } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/skeleton'; +import { GETHGVTechnicalRecordV3Testable } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/testable'; +import { TechRecordCompleteMotorcycleSchema } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/motorcycle/complete'; +import { GETPSVTechnicalRecordV3Complete } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/psv/complete'; +import { GETPSVTechnicalRecordV3Skeleton } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/psv/skeleton'; +import { GETPSVTechnicalRecordV3Testable } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/psv/testable'; +import { GETTRLTechnicalRecordV3Complete } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/trl/complete'; +import { GETTRLTechnicalRecordV3Skeleton } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/trl/skeleton'; +import { GETTRLTechnicalRecordV3Testable } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/trl/testable'; export type TechRecordGet = - | TechRecordCompleteCarSchema - | TechRecordSkeletonCarSchema - | GETHGVTechnicalRecordV3Complete - | GETHGVTechnicalRecordV3Testable - | GETHGVTechnicalRecordV3Skeleton - | TechRecordCompleteMotorcycleSchema - | GETPSVTechnicalRecordV3Complete - | GETPSVTechnicalRecordV3Testable - | GETPSVTechnicalRecordV3Skeleton - | GETTRLTechnicalRecordV3Complete - | GETTRLTechnicalRecordV3Testable - | GETTRLTechnicalRecordV3Skeleton; + | TechRecordCompleteCarSchema + | TechRecordSkeletonCarSchema + | GETHGVTechnicalRecordV3Complete + | GETHGVTechnicalRecordV3Testable + | GETHGVTechnicalRecordV3Skeleton + | TechRecordCompleteMotorcycleSchema + | GETPSVTechnicalRecordV3Complete + | GETPSVTechnicalRecordV3Testable + | GETPSVTechnicalRecordV3Skeleton + | GETTRLTechnicalRecordV3Complete + | GETTRLTechnicalRecordV3Testable + | GETTRLTechnicalRecordV3Skeleton; -export type TechRecordType = - T extends "car" | "lgv" - ? TechRecordCompleteCarSchema - : T extends "hgv" - ? GETHGVTechnicalRecordV3Complete | GETHGVTechnicalRecordV3Testable - : T extends "motorcycle" - ? TechRecordCompleteMotorcycleSchema - : T extends "psv" - ? GETPSVTechnicalRecordV3Complete | GETPSVTechnicalRecordV3Testable - : T extends "trl" - ? GETTRLTechnicalRecordV3Complete | GETTRLTechnicalRecordV3Testable - : never; +export type TechRecordType = T extends 'car' | 'lgv' + ? TechRecordCompleteCarSchema + : T extends 'hgv' + ? GETHGVTechnicalRecordV3Complete | GETHGVTechnicalRecordV3Testable + : T extends 'motorcycle' + ? TechRecordCompleteMotorcycleSchema + : T extends 'psv' + ? GETPSVTechnicalRecordV3Complete | GETPSVTechnicalRecordV3Testable + : T extends 'trl' + ? GETTRLTechnicalRecordV3Complete | GETTRLTechnicalRecordV3Testable + : never; export interface ISearchResult { - systemNumber: string; - createdTimestamp: string; - vin: string; - primaryVrm?: string; - trailerId?: string; - techRecord_vehicleType: string; - techRecord_manufactureYear?: number | null; - techRecord_chassisMake?: string; - techRecord_chassisModel?: string; - techRecord_make?: string; - techRecord_model?: string; - techRecord_statusCode?: string; + systemNumber: string; + createdTimestamp: string; + vin: string; + primaryVrm?: string; + trailerId?: string; + techRecord_vehicleType: string; + techRecord_manufactureYear?: number | null; + techRecord_chassisMake?: string; + techRecord_chassisModel?: string; + techRecord_make?: string; + techRecord_model?: string; + techRecord_statusCode?: string; } diff --git a/src/models/index.d.ts b/src/models/index.d.ts index 001818c9..60931ea7 100644 --- a/src/models/index.d.ts +++ b/src/models/index.d.ts @@ -1,314 +1,314 @@ interface IInvokeConfig { - params: { apiVersion: string; endpoint?: string }; - functions: { - testResults: { name: string }; - techRecords: { name: string; mock: string }; - techRecordsSearch: { name: string; mock: string }; - certGen: { name: string }; - trailerRegistration: { name: string }; - testStations: { name: string; mock: string }; - defects: { name: string }; - }; + params: { apiVersion: string; endpoint?: string }; + functions: { + testResults: { name: string }; + techRecords: { name: string; mock: string }; + techRecordsSearch: { name: string; mock: string }; + certGen: { name: string }; + trailerRegistration: { name: string }; + testStations: { name: string; mock: string }; + defects: { name: string }; + }; } interface IMOTConfig { - endpoint: string; - documentDir: "CVS"; - documentNames: { - vt20: "VT20.pdf"; - vt20w: "VT20W.pdf"; - vt30: "VT30.pdf"; - vt30w: "VT30W.pdf"; - vt32ve: "VT32VE.pdf"; - vt32vew: "VT32VEW.pdf"; - prs: "PRS.pdf"; - prsw: "PRSW.pdf"; - ct20: "CT20.pdf"; - ct30: "CT30.pdf"; - vtp20: "VTP20.pdf"; - vtp20_bilingual: "VTP20_BILINGUAL.pdf"; - vtp30: "VTP30.pdf"; - vtp30_bilingual: "VTP30_BILINGUAL.pdf"; - psv_prs: "PSV_PRS.pdf"; - psv_prs_bilingual: "PSV_PRS_BILINGUAL.pdf"; - vtg5: "VTG5.pdf"; - vtg5_bilingual: "VTG5_BILINGUAL.pdf"; - vtg5a: "VTG5A.pdf"; - vtg5a_bilingual: "VTG5A_BILINGUAL.pdf"; - vtg30: "VTG30.pdf"; - vtg30_bilingual: "VTG30_BILINGUAL.pdf"; - hgv_prs: "HGV_PRS.pdf"; - hgv_prs_bilingual: "HGV_PRS_BILINGUAL.pdf"; - trl_prs: "TRL_PRS.pdf"; - trl_prs_bilingual: "TRL_PRS_BILINGUAL.pdf"; - adr_pass: "ADR_PASS.pdf"; - rwt: "RWT.pdf;"; - iva_fail: "IVA30.pdf"; - msva_fail: "MSVA30.pdf"; - }; - api_key: string; + endpoint: string; + documentDir: 'CVS'; + documentNames: { + vt20: 'VT20.pdf'; + vt20w: 'VT20W.pdf'; + vt30: 'VT30.pdf'; + vt30w: 'VT30W.pdf'; + vt32ve: 'VT32VE.pdf'; + vt32vew: 'VT32VEW.pdf'; + prs: 'PRS.pdf'; + prsw: 'PRSW.pdf'; + ct20: 'CT20.pdf'; + ct30: 'CT30.pdf'; + vtp20: 'VTP20.pdf'; + vtp20_bilingual: 'VTP20_BILINGUAL.pdf'; + vtp30: 'VTP30.pdf'; + vtp30_bilingual: 'VTP30_BILINGUAL.pdf'; + psv_prs: 'PSV_PRS.pdf'; + psv_prs_bilingual: 'PSV_PRS_BILINGUAL.pdf'; + vtg5: 'VTG5.pdf'; + vtg5_bilingual: 'VTG5_BILINGUAL.pdf'; + vtg5a: 'VTG5A.pdf'; + vtg5a_bilingual: 'VTG5A_BILINGUAL.pdf'; + vtg30: 'VTG30.pdf'; + vtg30_bilingual: 'VTG30_BILINGUAL.pdf'; + hgv_prs: 'HGV_PRS.pdf'; + hgv_prs_bilingual: 'HGV_PRS_BILINGUAL.pdf'; + trl_prs: 'TRL_PRS.pdf'; + trl_prs_bilingual: 'TRL_PRS_BILINGUAL.pdf'; + adr_pass: 'ADR_PASS.pdf'; + rwt: 'RWT.pdf;'; + iva_fail: 'IVA30.pdf'; + msva_fail: 'MSVA30.pdf'; + }; + api_key: string; } interface IS3Config { - endpoint: string; + endpoint: string; } interface IGeneratedCertificateResponse { - fileName: string; - vrm: string; - testTypeName: string; - testTypeResult: string; - dateOfIssue: string; - certificateType: string; - fileFormat: string; - fileSize: string; - certificate: Buffer; - certificateOrder: { current: number; total: number }; - email: string; - shouldEmailCertificate: string; + fileName: string; + vrm: string; + testTypeName: string; + testTypeResult: string; + dateOfIssue: string; + certificateType: string; + fileFormat: string; + fileSize: string; + certificate: Buffer; + certificateOrder: { current: number; total: number }; + email: string; + shouldEmailCertificate: string; } interface ICertificateData { - TestNumber: string; - TestStationPNumber: string; - TestStationName: string; - CurrentOdometer: IOdometer; - IssuersName: string; - DateOfTheTest: string; - CountryOfRegistrationCode: string; - VehicleEuClassification: string; - RawVIN: string; - RawVRM: string; - ExpiryDate: string; - EarliestDateOfTheNextTest: string; - SeatBeltTested: string; - SeatBeltPreviousCheckDate: string; - SeatBeltNumber: number; + TestNumber: string; + TestStationPNumber: string; + TestStationName: string; + CurrentOdometer: IOdometer; + IssuersName: string; + DateOfTheTest: string; + CountryOfRegistrationCode: string; + VehicleEuClassification: string; + RawVIN: string; + RawVRM: string; + ExpiryDate: string; + EarliestDateOfTheNextTest: string; + SeatBeltTested: string; + SeatBeltPreviousCheckDate: string; + SeatBeltNumber: number; } interface IOdometer { - value: string; - unit: string; + value: string; + unit: string; } interface IDefects { - DangerousDefects: string[]; - MajorDefects: string[]; - PRSDefects: string[]; - MinorDefects: string[]; - AdvisoryDefects: string[]; + DangerousDefects: string[]; + MajorDefects: string[]; + PRSDefects: string[]; + MinorDefects: string[]; + AdvisoryDefects: string[]; } interface ICertificatePayload { - Watermark: string; - DATA?: any; - FAIL_DATA?: any; - RWT_DATA?: any; - ADR_DATA?: any; - IVA_DATA?: any; - MSVA_DATA?: any; - Signature: ISignature; - Reissue?: IReissue; + Watermark: string; + DATA?: any; + FAIL_DATA?: any; + RWT_DATA?: any; + ADR_DATA?: any; + IVA_DATA?: any; + MSVA_DATA?: any; + Signature: ISignature; + Reissue?: IReissue; } interface IReissue { - Reason: string; - Issuer: string; - Date: string; + Reason: string; + Issuer: string; + Date: string; } interface ISignature { - ImageType: string; - ImageData: string | null; + ImageType: string; + ImageData: string | null; } interface IRoadworthinessCertificateData { - Dgvw: number; - Weight2: number; // can be dgtw or dtaw based on vehcile type - VehicleNumber: string; // can be vin or trailer Id based on vehicle type - Vin: string; - IssuersName: string; - DateOfInspection: string; - TestStationPNumber: string; - DocumentNumber: string; - Date: string; - Defects: string[] | undefined; - IsTrailer: boolean; + Dgvw: number; + Weight2: number; // can be dgtw or dtaw based on vehcile type + VehicleNumber: string; // can be vin or trailer Id based on vehicle type + Vin: string; + IssuersName: string; + DateOfInspection: string; + TestStationPNumber: string; + DocumentNumber: string; + Date: string; + Defects: string[] | undefined; + IsTrailer: boolean; } interface IWeightDetails { - dgvw: number; - weight2: number; + dgvw: number; + weight2: number; } interface ITestResult { - vrm: string; - trailerId: string; - vin: string; - vehicleId: string; - deletionFlag: boolean; - testStationName: string; - testStationPNumber: string; - testStationType: string; - testerName: string; - testerStaffId: string; - testerEmailAddress: string; - testStartTimestamp: string; - testEndTimestamp: string; - testStatus: string; - reasonForCancellation: string; - vehicleClass: IVehicleClass; - vehicleType: string; - numberOfSeats: number; - vehicleConfiguration: string; - odometerReading: number; - odometerReadingUnits: string; - preparerId: string; - preparerName: string; - euVehicleCategory: string; - countryOfRegistration: string; - vehicleSize: string; - noOfAxles: number; - regnDate: string; - firstUseDate: string; - make?: string; - model?: string; - bodyType?: IBodyTypeModel; - testTypes: ITestType; + vrm: string; + trailerId: string; + vin: string; + vehicleId: string; + deletionFlag: boolean; + testStationName: string; + testStationPNumber: string; + testStationType: string; + testerName: string; + testerStaffId: string; + testerEmailAddress: string; + testStartTimestamp: string; + testEndTimestamp: string; + testStatus: string; + reasonForCancellation: string; + vehicleClass: IVehicleClass; + vehicleType: string; + numberOfSeats: number; + vehicleConfiguration: string; + odometerReading: number; + odometerReadingUnits: string; + preparerId: string; + preparerName: string; + euVehicleCategory: string; + countryOfRegistration: string; + vehicleSize: string; + noOfAxles: number; + regnDate: string; + firstUseDate: string; + make?: string; + model?: string; + bodyType?: IBodyTypeModel; + testTypes: ITestType; } interface ITestType { - createdAt: string; - lastUpdatedAt: string; - deletionFlag: boolean; - testCode: string; - testTypeName: string; - testTypeClassification: string; - name: string; - testTypeId: string; - testNumber: string; - certificateNumber: string; - certificateLink: string; - testExpiryDate: string; - testAnniversaryDate: string; - testTypeStartTimestamp: string; - testTypeEndTimestamp: string; - numberOfSeatbeltsFitted: number; - lastSeatbeltInstallationCheckDate: string; - seatbeltInstallationCheckDate: boolean; - testResult: string; - prohibitionIssued: boolean; - reasonForAbandoning: string; - additionalNotesRecorded: string; - additionalCommentsForAbandon: string; - modType: IVehicleClass; - emissionStandard: string; - fuelType: string; - reapplicationDate?: string; - defects: IDefect[]; - requiredStandards?: IRequiredStandard[]; - customDefects?: ICustomDefect[]; + createdAt: string; + lastUpdatedAt: string; + deletionFlag: boolean; + testCode: string; + testTypeName: string; + testTypeClassification: string; + name: string; + testTypeId: string; + testNumber: string; + certificateNumber: string; + certificateLink: string; + testExpiryDate: string; + testAnniversaryDate: string; + testTypeStartTimestamp: string; + testTypeEndTimestamp: string; + numberOfSeatbeltsFitted: number; + lastSeatbeltInstallationCheckDate: string; + seatbeltInstallationCheckDate: boolean; + testResult: string; + prohibitionIssued: boolean; + reasonForAbandoning: string; + additionalNotesRecorded: string; + additionalCommentsForAbandon: string; + modType: IVehicleClass; + emissionStandard: string; + fuelType: string; + reapplicationDate?: string; + defects: IDefect[]; + requiredStandards?: IRequiredStandard[]; + customDefects?: ICustomDefect[]; } interface IDefect { - imNumber: number; - imDescription: string; - additionalInformation: IAdditionalInformation; - itemNumber: number; - itemDescription: string; - deficiencyRef: string; - deficiencyId: string; - deficiencySubId: string; - deficiencyCategory: string; - deficiencyText: string; - stdForProhibition: boolean; - prs: boolean; - prohibitionIssued: boolean; + imNumber: number; + imDescription: string; + additionalInformation: IAdditionalInformation; + itemNumber: number; + itemDescription: string; + deficiencyRef: string; + deficiencyId: string; + deficiencySubId: string; + deficiencyCategory: string; + deficiencyText: string; + stdForProhibition: boolean; + prs: boolean; + prohibitionIssued: boolean; } -type InspectionType = "basic" | "normal"; +type InspectionType = 'basic' | 'normal'; interface IAdditionalInformation { - location: ILocation; - notes: string; + location: ILocation; + notes: string; } interface ILocation { - vertical: string; - horizontal: string; - lateral: string; - longitudinal: string; - rowNumber: number; - seatNumber: number; - axleNumber: number; + vertical: string; + horizontal: string; + lateral: string; + longitudinal: string; + rowNumber: number; + seatNumber: number; + axleNumber: number; } interface IVehicleClass { - code: string; - description: string; + code: string; + description: string; } interface ITrailerRegistration { - vinOrChassisWithMake?: string; - vin: string; - make: string; - trn: string; - certificateExpiryDate: Date; - certificateIssueDate: Date; + vinOrChassisWithMake?: string; + vin: string; + make: string; + trn: string; + certificateExpiryDate: Date; + certificateIssueDate: Date; } interface IMakeAndModel { - Make: string; - Model: string; + Make: string; + Model: string; } interface IRequiredStandard { - sectionNumber: string; - sectionDescription: string; - rsNumber: number; - requiredStandard: string; - refCalculation: string; - additionalInfo: boolean; - inspectionTypes: InspectionType[]; - prs: boolean; - additionalNotes?: string; + sectionNumber: string; + sectionDescription: string; + rsNumber: number; + requiredStandard: string; + refCalculation: string; + additionalInfo: boolean; + inspectionTypes: InspectionType[]; + prs: boolean; + additionalNotes?: string; } interface IBodyTypeModel { - code: string; - description: string; + code: string; + description: string; } interface ICustomDefect { - referenceNumber?: string; - defectName: string; - defectNotes: string; + referenceNumber?: string; + defectName: string; + defectNotes: string; } interface IFeatureFlags { - welshTranslation: { - enabled: boolean; - translatePassTestResult: boolean; - translateFailTestResult: boolean; - translatePrsTestResult: boolean; - }; + welshTranslation: { + enabled: boolean; + translatePassTestResult: boolean; + translateFailTestResult: boolean; + translatePrsTestResult: boolean; + }; } -export { - IInvokeConfig, - IMOTConfig, - IS3Config, - IGeneratedCertificateResponse, - IDefects, - ICertificatePayload, - IRoadworthinessCertificateData, - IWeightDetails, - ITestResult, - ITestType, - ITrailerRegistration, - IMakeAndModel, - IRequiredStandard, - IBodyTypeModel, - ICustomDefect, - IFeatureFlags +export type { + IInvokeConfig, + IMOTConfig, + IS3Config, + IGeneratedCertificateResponse, + IDefects, + ICertificatePayload, + IRoadworthinessCertificateData, + IWeightDetails, + ITestResult, + ITestType, + ITrailerRegistration, + IMakeAndModel, + IRequiredStandard, + IBodyTypeModel, + ICustomDefect, + IFeatureFlags, }; diff --git a/src/models/injector/Injector.ts b/src/models/injector/Injector.ts index 1fb4ec5c..0687af73 100644 --- a/src/models/injector/Injector.ts +++ b/src/models/injector/Injector.ts @@ -1,31 +1,26 @@ -import "reflect-metadata"; -import { Type } from "./GenericClassDecorator"; +import 'reflect-metadata'; +import { Type } from './GenericClassDecorator'; /** * The Injector stores services and resolves requested instances. */ export const Injector = new (class { - /** - * Resolves instances by injecting required services - * @param {Type} target - * @param {Type} injections - optionally, you can override the expected injection with another one - * @returns {T} - */ - public resolve(target: Type, injections?: Array>): T { - if (!injections) { - // tokens are required dependencies, while injections are resolved tokens from the Injector - const targetTokens = - Reflect.getMetadata("design:paramtypes", target) || []; - const targetInjections = targetTokens.map((token: any) => - Injector.resolve(token) - ); + /** + * Resolves instances by injecting required services + * @param {Type} target + * @param {Type} injections - optionally, you can override the expected injection with another one + * @returns {T} + */ + public resolve(target: Type, injections?: Array>): T { + if (!injections) { + // tokens are required dependencies, while injections are resolved tokens from the Injector + const targetTokens = Reflect.getMetadata('design:paramtypes', target) || []; + const targetInjections = targetTokens.map((token: any) => Injector.resolve(token)); - return new target(...targetInjections); - } + return new target(...targetInjections); + } - const manualInjections: any = injections.map((token: any) => - Injector.resolve(token) - ); - return new target(...manualInjections); - } + const manualInjections: any = injections.map((token: any) => Injector.resolve(token)); + return new target(...manualInjections); + } })(); diff --git a/src/models/injector/ServiceDecorator.ts b/src/models/injector/ServiceDecorator.ts index 7a120fb0..26f6e0b4 100644 --- a/src/models/injector/ServiceDecorator.ts +++ b/src/models/injector/ServiceDecorator.ts @@ -1,12 +1,12 @@ -import { GenericClassDecorator, Type } from "./GenericClassDecorator"; -import { Injector } from "./Injector"; +import { GenericClassDecorator, Type } from './GenericClassDecorator'; +import { Injector } from './Injector'; /** * @returns {GenericClassDecorator>} * @constructor */ export const Service = (): GenericClassDecorator> => { - return (target: Type) => { - Injector.resolve>(target); - }; + return (target: Type) => { + Injector.resolve>(target); + }; }; diff --git a/src/services/CertificateGenerationService.ts b/src/services/CertificateGenerationService.ts index 2b5325c9..4a1b5e70 100644 --- a/src/services/CertificateGenerationService.ts +++ b/src/services/CertificateGenerationService.ts @@ -1,1406 +1,1306 @@ -import { InvocationRequest, InvocationResponse, ServiceException } from "@aws-sdk/client-lambda"; -import { GetObjectOutput } from "@aws-sdk/client-s3"; -import { getProfile } from "@dvsa/cvs-feature-flags/profiles/vtx"; -import { toUint8Array } from "@smithy/util-utf8"; -import moment from "moment"; -import { Readable } from "stream"; +import { Readable } from 'stream'; +import { InvocationRequest, InvocationResponse, ServiceException } from '@aws-sdk/client-lambda'; +import { GetObjectOutput } from '@aws-sdk/client-s3'; +import { getProfile } from '@dvsa/cvs-feature-flags/profiles/vtx'; +import { toUint8Array } from '@smithy/util-utf8'; +import moment from 'moment'; import { - ICertificatePayload, - ICustomDefect, - IFeatureFlags, - IGeneratedCertificateResponse, - IInvokeConfig, - IMOTConfig, - IMakeAndModel, - IRoadworthinessCertificateData, - ITestResult, - ITestType, - ITrailerRegistration, - IWeightDetails, - IRequiredStandard -} from "../models"; + ICertificatePayload, + ICustomDefect, + IFeatureFlags, + IGeneratedCertificateResponse, + IInvokeConfig, + IMOTConfig, + IMakeAndModel, + IRequiredStandard, + IRoadworthinessCertificateData, + ITestResult, + ITestType, + ITrailerRegistration, + IWeightDetails, +} from '../models'; import { - ADR_TEST, - AVAILABLE_WELSH, - BASIC_IVA_TEST, - CERTIFICATE_DATA, - ERRORS, - HGV_TRL_ROADWORTHINESS_TEST_TYPES, - IVA30_TEST, - IVA_30, - LOCATION_ENGLISH, - LOCATION_WELSH, - MSVA30_TEST, - TEST_RESULTS, - VEHICLE_TYPES -} from "../models/Enums"; -import { HTTPError } from "../models/HTTPError"; -import { IDefectChild } from "../models/IDefectChild"; -import { IDefectParent } from "../models/IDefectParent"; -import { IFlatDefect } from "../models/IFlatDefect"; -import { IItem } from "../models/IItem"; -import { ITestStation } from "../models/ITestStations"; -import { ISearchResult, TechRecordGet, TechRecordType } from "../models/Types"; -import { Service } from "../models/injector/ServiceDecorator"; -import { Configuration } from "../utils/Configuration"; -import { LambdaService } from "./LambdaService"; -import { S3BucketService } from "./S3BucketService"; + ADR_TEST, + AVAILABLE_WELSH, + BASIC_IVA_TEST, + CERTIFICATE_DATA, + ERRORS, + HGV_TRL_ROADWORTHINESS_TEST_TYPES, + IVA30_TEST, + IVA_30, + LOCATION_ENGLISH, + LOCATION_WELSH, + MSVA30_TEST, + TEST_RESULTS, + VEHICLE_TYPES, +} from '../models/Enums'; +import { HTTPError } from '../models/HTTPError'; +import { IDefectChild } from '../models/IDefectChild'; +import { IDefectParent } from '../models/IDefectParent'; +import { IFlatDefect } from '../models/IFlatDefect'; +import { IItem } from '../models/IItem'; +import { ITestStation } from '../models/ITestStations'; +import { ISearchResult, TechRecordGet, TechRecordType } from '../models/Types'; +import { Service } from '../models/injector/ServiceDecorator'; +import { Configuration } from '../utils/Configuration'; +import { LambdaService } from './LambdaService'; +import { S3BucketService } from './S3BucketService'; /** * Service class for Certificate Generation */ @Service() class CertificateGenerationService { - private readonly s3Client: S3BucketService; - private readonly config: Configuration; - private readonly lambdaClient: LambdaService; - - constructor(s3Client: S3BucketService, lambdaClient: LambdaService) { - this.s3Client = s3Client; - this.config = Configuration.getInstance(); - this.lambdaClient = lambdaClient; - } - - /** - * Generates MOT certificate for a given test result - * @param testResult - source test result for certificate generation - */ - public async generateCertificate( - testResult: any - ): Promise { - const config: IMOTConfig = this.config.getMOTConfig(); - const iConfig: IInvokeConfig = this.config.getInvokeConfig(); - const testType: any = testResult.testTypes; - - const shouldTranslateTestResult = await this.shouldTranslateTestResult(testResult); - - const payload: string = JSON.stringify( - await this.generatePayload(testResult, shouldTranslateTestResult) - ); - - const certificateTypes: any = { - psv_pass: config.documentNames.vtp20, - psv_pass_bilingual: config.documentNames.vtp20_bilingual, - psv_fail: config.documentNames.vtp30, - psv_fail_bilingual: config.documentNames.vtp30_bilingual, - psv_prs: config.documentNames.psv_prs, - psv_prs_bilingual: config.documentNames.psv_prs_bilingual, - hgv_pass: config.documentNames.vtg5, - hgv_pass_bilingual: config.documentNames.vtg5_bilingual, - hgv_fail: config.documentNames.vtg30, - hgv_fail_bilingual: config.documentNames.vtg30_bilingual, - hgv_prs: config.documentNames.hgv_prs, - hgv_prs_bilingual: config.documentNames.hgv_prs_bilingual, - trl_pass: config.documentNames.vtg5a, - trl_pass_bilingual: config.documentNames.vtg5a_bilingual, - trl_fail: config.documentNames.vtg30, - trl_fail_bilingual: config.documentNames.vtg30_bilingual, - trl_prs: config.documentNames.trl_prs, - trl_prs_bilingual: config.documentNames.trl_prs_bilingual, - rwt: config.documentNames.rwt, - adr_pass: config.documentNames.adr_pass, - iva_fail: config.documentNames.iva_fail, - msva_fail: config.documentNames.msva_fail, - }; - - let vehicleTestRes: string; - if ( - CertificateGenerationService.isRoadworthinessTestType(testType.testTypeId) - ) { - // CVSB-7677 is road-worthiness test - vehicleTestRes = "rwt"; - } else if (this.isTestTypeAdr(testResult.testTypes)) { - vehicleTestRes = "adr_pass"; - } else if (this.isIvaTest(testResult.testTypes.testTypeId) && testType.testResult === "fail") { - vehicleTestRes = "iva_fail"; - } else if (this.isMsvaTest(testResult.testTypes.testTypeId) && testType.testResult === "fail") { - vehicleTestRes = "msva_fail"; - } else if (this.isWelshCertificateAvailable(testResult.vehicleType, testType.testResult) && shouldTranslateTestResult) { - vehicleTestRes = testResult.vehicleType + "_" + testType.testResult + "_bilingual"; - } else { - vehicleTestRes = testResult.vehicleType + "_" + testType.testResult; - } - - console.log(`vehicleTestRes: ${vehicleTestRes}`); - - const invokeParams: InvocationRequest = { - FunctionName: iConfig.functions.certGen.name, - InvocationType: "RequestResponse", - LogType: "Tail", - Payload: toUint8Array(JSON.stringify({ - httpMethod: "POST", - pathParameters: { - documentName: certificateTypes[vehicleTestRes], - documentDirectory: config.documentDir, - }, - json: true, - body: payload, - })), - }; - return this.lambdaClient - .invoke(invokeParams) - .then( - async (response: InvocationResponse) => { - const documentPayload: any = await this.lambdaClient.validateInvocationResponse(response); - const resBody: string = documentPayload.body; - const responseBuffer: Buffer = Buffer.from(resBody, "base64"); - console.log("return from doc gen!"); - return { - vrm: - testResult.vehicleType === VEHICLE_TYPES.TRL - ? testResult.trailerId - : testResult.vrm, - testTypeName: testResult.testTypes.testTypeName, - testTypeResult: testResult.testTypes.testResult, - dateOfIssue: moment( - testResult.testTypes.testTypeStartTimestamp - ).format("D MMMM YYYY"), - certificateType: certificateTypes[vehicleTestRes].split(".")[0], - fileFormat: "pdf", - fileName: `${testResult.testTypes.testNumber}_${testResult.vin}.pdf`, - fileSize: responseBuffer.byteLength.toString(), - certificate: responseBuffer, - certificateOrder: testResult.order, - email: - testResult.createdByEmailAddress ?? testResult.testerEmailAddress, - shouldEmailCertificate: testResult.shouldEmailCertificate ?? "true", - }; - } - ) - .catch((error: ServiceException | Error) => { - console.log(error); - throw error; - }); - } - - /** - * Handler method for retrieving feature flags and checking if test station is in Wales - * @param testResult - * @returns Promise - */ - public async shouldTranslateTestResult(testResult: any): Promise { - let shouldTranslateTestResult = false; - try { - const featureFlags: IFeatureFlags = await getProfile(); - console.log("Using feature flags ", featureFlags); - - if (this.isGlobalWelshFlagEnabled(featureFlags) && this.isTestResultFlagEnabled(testResult.testTypes.testResult, featureFlags)) { - shouldTranslateTestResult = await this.isTestStationWelsh(testResult.testStationPNumber); - } - } catch (e) { - console.error(`Failed to retrieve feature flags - ${e}`); - } - return shouldTranslateTestResult; - } - - /** - * Method to check if Welsh translation is enabled. - * @param featureFlags IFeatureFlags interface - * @returns boolean - */ - public isGlobalWelshFlagEnabled(featureFlags: IFeatureFlags): boolean { - if (!featureFlags.welshTranslation.enabled) { - console.warn(`Unable to translate any test results: global Welsh flag disabled.`); - return false; - } - return true; - } - - /** - * Method to check if Welsh translation is enabled for the given test type. - * @param featureFlags IFeatureFlags interface - * @param testResult string of result, PASS/PRS/FAIL - * @returns boolean - */ - public isTestResultFlagEnabled(testResult: string, featureFlags: IFeatureFlags): boolean { - let shouldTranslate: boolean = false; - switch (testResult) { - case TEST_RESULTS.PRS: - shouldTranslate = featureFlags.welshTranslation.translatePrsTestResult ?? false; - break; - case TEST_RESULTS.PASS: - shouldTranslate = featureFlags.welshTranslation.translatePassTestResult ?? false; - break; - case TEST_RESULTS.FAIL: - shouldTranslate = featureFlags.welshTranslation.translateFailTestResult ?? false; - break; - default: - console.warn("Translation not available for this test result type."); - return shouldTranslate; - } - if (!shouldTranslate) { - console.warn(`Unable to translate for test result: ${testResult} flag disabled`); - } - return shouldTranslate; - } - - /** - * Determines if a test station is located in Wales - * @param testStationPNumber The test station's P-number. - * @returns Promise true if the test station country is set to Wales, false otherwise - */ - public async isTestStationWelsh(testStationPNumber: string): Promise { - const testStation = await this.getTestStation(testStationPNumber); - - if (!testStation.testStationPNumber) { - console.error(`Failed to retrieve test station details for ${testStationPNumber}`); - return false; - } - - const isWelshCountry = testStation.testStationCountry?.toString().toUpperCase() === `WALES`; - console.log(`Test station country for ${testStationPNumber} is set to ${testStation.testStationCountry}`); - return isWelshCountry; - } - - /** - * Method to retrieve Test Station details from API - * @returns a test station object - */ - public async getTestStation(testStationPNumber: string): Promise { - const config: IInvokeConfig = this.config.getInvokeConfig(); - const invokeParams: InvocationRequest = { - FunctionName: config.functions.testStations.name, - InvocationType: "RequestResponse", - LogType: "Tail", - Payload: toUint8Array(JSON.stringify({ - httpMethod: "GET", - path: `/test-stations/${testStationPNumber}`, - })), - }; - let testStation: ITestStation = {} as ITestStation; - let retries = 0; - - while (retries < 3) { - try { - const response: InvocationResponse = await this.lambdaClient.invoke(invokeParams); - const payload: any = this.lambdaClient.validateInvocationResponse(response); - - testStation = JSON.parse(payload.body); - - return testStation; - } catch (error) { - retries++; - console.error(`There was an error retrieving the test station on attempt ${retries}: ${error}`); - } - } - return testStation; - } - - /** - * Retrieves a signature from the cvs-signature S3 bucket - * @param staffId - staff ID of the signature you want to retrieve - * @returns the signature as a base64 encoded string - */ - public async getSignature(staffId: string): Promise { - try { - const result: GetObjectOutput = await this.s3Client - .download(`cvs-signature-${process.env.BUCKET}`, `${staffId}.base64`); - - if (result.Body instanceof Readable) { - const chunks: Uint8Array[] = []; - for await (const chunk of result.Body) { - chunks.push(chunk); - } - const buffer = Buffer.concat(chunks); - return buffer.toString("utf-8"); - } else { - throw new Error(`Unexpected body type: ${typeof result.Body}`); - } - } catch (error) { - console.error(`Unable to fetch signature for staff id ${staffId}. ${(error as Error).message}`); - } - return null; - } - - /** - * Generates the payload for the MOT certificate generation service - * @param testResult - source test result for certificate generation - * @param isWelsh - the boolean value whether the atf where test was conducted resides in Wales - */ - public async generatePayload(testResult: any, isWelsh: boolean = false) { - let name = testResult.testerName; - - const nameArrayList: string[] = name.split(","); - - if (nameArrayList.length === 2) { - name = name.split(", ").reverse().join(" "); - testResult.testerName = name; - } - - const signature: string | null = await this.getSignature( - testResult.createdById ?? testResult.testerStaffId - ); - - let makeAndModel: any = null; - if ( - !CertificateGenerationService.isRoadworthinessTestType( - testResult.testTypes.testTypeId - ) - ) { - makeAndModel = await this.getVehicleMakeAndModel(testResult); - } - - let payload: ICertificatePayload = { - Watermark: process.env.BRANCH === "prod" ? "" : "NOT VALID", - DATA: undefined, - FAIL_DATA: undefined, - RWT_DATA: undefined, - ADR_DATA: undefined, - IVA_DATA: undefined, - MSVA_DATA: undefined, - Signature: { - ImageType: "png", - ImageData: signature, - }, - }; - - const { testTypes, vehicleType, systemNumber, testHistory } = testResult; - - if (testHistory) { - for (const history of testHistory) { - for (const testType of history.testTypes) { - if (testType.testCode === testTypes.testCode) { - payload.Reissue = { - Reason: "Replacement", - Issuer: testResult.createdByName, - Date: moment(testResult.createdAt).format("DD.MM.YYYY"), - }; - break; - } - } - } - } - - if ( - CertificateGenerationService.isHgvTrlRoadworthinessCertificate(testResult) - ) { - // CVSB-7677 for roadworthiness test for hgv or trl. - const rwtData = await this.generateCertificateData( - testResult, - CERTIFICATE_DATA.RWT_DATA - ); - payload.RWT_DATA = { ...rwtData }; - } else if ( - testResult.testTypes.testResult === TEST_RESULTS.PASS && - this.isTestTypeAdr(testResult.testTypes) - ) { - const adrData = await this.generateCertificateData( - testResult, - CERTIFICATE_DATA.ADR_DATA - ); - payload.ADR_DATA = { ...adrData, ...makeAndModel }; - } else if ( - testResult.testTypes.testResult === TEST_RESULTS.FAIL && - this.isIvaTest(testResult.testTypes.testTypeId) - ) { - const ivaData = await this.generateCertificateData( - testResult, - CERTIFICATE_DATA.IVA_DATA - ); - payload.IVA_DATA = { ...ivaData }; - } else if ( - testResult.testTypes.testResult === TEST_RESULTS.FAIL && - this.isMsvaTest(testResult.testTypes.testTypeId) - ) { - const msvaData = await this.generateCertificateData( - testResult, - CERTIFICATE_DATA.MSVA_DATA - ); - payload.MSVA_DATA = { ...msvaData }; - } else { - const odometerHistory = - vehicleType === VEHICLE_TYPES.TRL - ? undefined - : await this.getOdometerHistory(systemNumber); - const TrnObj = this.isValidForTrn(vehicleType, makeAndModel) - ? await this.getTrailerRegistrationObject( - testResult.vin, - makeAndModel.Make - ) - : undefined; - if (testTypes.testResult !== TEST_RESULTS.FAIL) { - const passData = await this.generateCertificateData( - testResult, - CERTIFICATE_DATA.PASS_DATA, - isWelsh - ); - payload.DATA = { - ...passData, - ...makeAndModel, - ...odometerHistory, - ...TrnObj, - }; - } - if (testTypes.testResult !== TEST_RESULTS.PASS) { - const failData = await this.generateCertificateData( - testResult, - CERTIFICATE_DATA.FAIL_DATA, - isWelsh - ); - payload.FAIL_DATA = { - ...failData, - ...makeAndModel, - ...odometerHistory, - ...TrnObj, - }; - } - } - // Purge undefined values - payload = JSON.parse(JSON.stringify(payload)); - - return payload; - } - - /** - * Generates certificate data for a given test result and certificate type - * @param testResult - the source test result for certificate generation - * @param type - the certificate type - * @param isWelsh - the boolean value whether the atf where test was conducted resides in Wales - */ - public async generateCertificateData(testResult: ITestResult, type: string, isWelsh: boolean = false) { - let defectListFromApi: IDefectParent[] = []; - let flattenedDefects: IFlatDefect[] = []; - if (isWelsh) { - defectListFromApi = await this.getDefectTranslations(); - flattenedDefects = this.flattenDefectsFromApi(defectListFromApi); - } - const testType: any = testResult.testTypes; - switch (type) { - case CERTIFICATE_DATA.PASS_DATA: - case CERTIFICATE_DATA.FAIL_DATA: - const defects: any = await this.generateDefects(testResult.testTypes, type, testResult.vehicleType, flattenedDefects, isWelsh); - - return { - TestNumber: testType.testNumber, - TestStationPNumber: testResult.testStationPNumber, - TestStationName: testResult.testStationName, - CurrentOdometer: { - value: testResult.odometerReading, - unit: testResult.odometerReadingUnits, - }, - IssuersName: testResult.testerName, - DateOfTheTest: moment(testResult.testEndTimestamp).format( - "DD.MM.YYYY" - ), - CountryOfRegistrationCode: testResult.countryOfRegistration, - VehicleEuClassification: testResult.euVehicleCategory.toUpperCase(), - RawVIN: testResult.vin, - RawVRM: - testResult.vehicleType === VEHICLE_TYPES.TRL - ? testResult.trailerId - : testResult.vrm, - ExpiryDate: testType.testExpiryDate - ? moment(testType.testExpiryDate).format("DD.MM.YYYY") - : undefined, - EarliestDateOfTheNextTest: - (testResult.vehicleType === VEHICLE_TYPES.HGV || - testResult.vehicleType === VEHICLE_TYPES.TRL) && - (testResult.testTypes.testResult === TEST_RESULTS.PASS || - testResult.testTypes.testResult === TEST_RESULTS.PRS) - ? moment(testType.testAnniversaryDate) - .subtract(1, "months") - .startOf("month") - .format("DD.MM.YYYY") - : moment(testType.testAnniversaryDate).format("DD.MM.YYYY"), - SeatBeltTested: testType.seatbeltInstallationCheckDate ? "Yes" : "No", - SeatBeltPreviousCheckDate: testType.lastSeatbeltInstallationCheckDate - ? moment(testType.lastSeatbeltInstallationCheckDate).format( - "DD.MM.YYYY" - ) - : "\u00A0", - SeatBeltNumber: testType.numberOfSeatbeltsFitted, - ...defects, - }; - case CERTIFICATE_DATA.RWT_DATA: - const weightDetails = await this.getWeightDetails(testResult); - let defectRWTList: any; - if (testResult.testTypes.testResult === TEST_RESULTS.FAIL) { - defectRWTList = []; - testResult.testTypes.defects.forEach((defect: any) => { - defectRWTList.push(this.formatDefect(defect)); - }); - } else { - defectRWTList = undefined; - } - - const resultPass: IRoadworthinessCertificateData = { - Dgvw: weightDetails.dgvw, - Weight2: weightDetails.weight2, - VehicleNumber: - testResult.vehicleType === VEHICLE_TYPES.TRL - ? testResult.trailerId - : testResult.vrm, - Vin: testResult.vin, - IssuersName: testResult.testerName, - DateOfInspection: moment(testType.testTypeStartTimestamp).format( - "DD.MM.YYYY" - ), - TestStationPNumber: testResult.testStationPNumber, - DocumentNumber: testType.certificateNumber, - Date: moment(testType.testTypeStartTimestamp).format("DD.MM.YYYY"), - Defects: defectRWTList, - IsTrailer: testResult.vehicleType === VEHICLE_TYPES.TRL, - }; - return resultPass; - case CERTIFICATE_DATA.ADR_DATA: - const adrDetails: TechRecordType = await this.getAdrDetails(testResult); - const docGenPayloadAdr = { - ChasisNumber: testResult.vin, - RegistrationNumber: testResult.vrm, - ApplicantDetails: { - name: adrDetails?.techRecord_applicantDetails_name, - address1: adrDetails?.techRecord_applicantDetails_address1, - address2: adrDetails?.techRecord_applicantDetails_address2, - address3: adrDetails?.techRecord_applicantDetails_address1, - postTown: adrDetails?.techRecord_applicantDetails_postTown, - postCode: adrDetails?.techRecord_applicantDetails_postCode, - telephoneNumber: adrDetails?.techRecord_applicantDetails_telephoneNumber, - emailAddress: adrDetails?.techRecord_applicantDetails_emailAddress, - }, - VehicleType: adrDetails?.techRecord_adrDetails_vehicleDetails_type, - PermittedDangerousGoods: adrDetails?.techRecord_adrDetails_permittedDangerousGoods, - BrakeEndurance: adrDetails?.techRecord_adrDetails_brakeEndurance, - Weight: adrDetails?.techRecord_adrDetails_weight, - TankManufacturer: adrDetails?.techRecord_adrDetails_tank_tankDetails_tankStatement_statement - ? adrDetails.techRecord_adrDetails_tank_tankDetails_tankManufacturer - : undefined, - Tc2InitApprovalNo: adrDetails?.techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo, - TankManufactureSerialNo: adrDetails?.techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo, - YearOfManufacture: adrDetails?.techRecord_adrDetails_tank_tankDetails_yearOfManufacture, - TankCode: adrDetails?.techRecord_adrDetails_tank_tankDetails_tankCode, - SpecialProvisions: adrDetails?.techRecord_adrDetails_tank_tankDetails_specialProvisions, - TankStatement: adrDetails?.techRecord_adrDetails_tank_tankDetails_tankStatement_statement, - ExpiryDate: testResult.testTypes.testExpiryDate, - AtfNameAtfPNumber: - testResult.testStationName + " " + testResult.testStationPNumber, - Notes: testResult.testTypes.additionalNotesRecorded, - TestTypeDate: testResult.testTypes.testTypeStartTimestamp, - }; - console.log("CHECK HERE DOCGENPAYLOAD -> ", docGenPayloadAdr); - return docGenPayloadAdr; - case CERTIFICATE_DATA.IVA_DATA: - const ivaFailDetailsForDocGen = { - vin: testResult.vin, - serialNumber: testResult.vehicleType === "trl" ? testResult.trailerId : testResult.vrm, - vehicleTrailerNrNo: testResult.vehicleType === "trl" ? testResult.trailerId : testResult.vrm, - testCategoryClass: testResult.euVehicleCategory, - testCategoryBasicNormal: this.isBasicIvaTest(testResult.testTypes.testTypeId) ? IVA_30.BASIC : IVA_30.NORMAL, - make: testResult.make, - model: testResult.model, - bodyType: testResult.bodyType?.description, - date: moment(testResult.testTypes.testTypeStartTimestamp).format("DD/MM/YYYY"), - testerName: testResult.testerName, - reapplicationDate: testResult.testTypes?.reapplicationDate ? moment(testResult.testTypes?.reapplicationDate).format("DD/MM/YYYY") : "", - station: testResult.testStationName, - additionalDefects: this.formatVehicleApprovalAdditionalDefects(testResult.testTypes.customDefects), - requiredStandards: this.sortRequiredStandards(testResult.testTypes.requiredStandards) - }; - return ivaFailDetailsForDocGen; - case CERTIFICATE_DATA.MSVA_DATA: - const msvaFailDetailsForDocGen = { - vin: testResult.vin, - serialNumber: testResult.vrm, - vehicleZNumber: testResult.vrm, - make: testResult.make, - model: testResult.model, - type: testResult.vehicleType, - testerName: testResult.testerName, - date: moment(testResult.testTypes.testTypeStartTimestamp).format("DD/MM/YYYY"), - reapplicationDate: testResult.testTypes?.reapplicationDate ? moment(testResult.testTypes?.reapplicationDate).format("DD/MM/YYYY") : "", - station: testResult.testStationName, - additionalDefects: this.formatVehicleApprovalAdditionalDefects(testResult.testTypes.customDefects), - requiredStandards: this.sortRequiredStandards(testResult.testTypes.requiredStandards) - }; - return msvaFailDetailsForDocGen; - } - } - - /** - * Formats the additional defects for IVA and MSVA test based on whether custom defects is populated - * @param customDefects - the custom defects for the test - */ - public formatVehicleApprovalAdditionalDefects = (customDefects: ICustomDefect[] | undefined): ICustomDefect[] | undefined => { - const defaultCustomDefect: ICustomDefect = { - defectName: IVA_30.EMPTY_CUSTOM_DEFECTS, - defectNotes: "" - }; - return (customDefects && customDefects.length > 0) ? customDefects : [defaultCustomDefect]; - } - - /** - * Calculates the retest date for an IVA or MSVA test - * @param testTypeStartTimestamp - the test start timestamp of the test - */ - public calculateVehicleApprovalRetestDate = (testTypeStartTimestamp: string): string => { - return moment(testTypeStartTimestamp) - .add(6, "months") - .subtract(1, "day") - .format("DD/MM/YYYY"); - } - - /** - * Retrieves the adrDetails from a techRecord searched by vin - * @param testResult - testResult from which the VIN is used to search a tech-record - */ - public getAdrDetails = async (testResult: any) => { - const searchRes = await this.callSearchTechRecords(testResult.systemNumber); - return await this.processGetCurrentProvisionalRecords(searchRes) as TechRecordType<"hgv" | "trl">; - } - - - public processGetCurrentProvisionalRecords = async (searchResult: ISearchResult[]): Promise | undefined> => { - if (searchResult) { - const processRecordsRes = this.groupRecordsByStatusCode(searchResult); - return processRecordsRes.currentCount !== 0 - ? this.callGetTechRecords(processRecordsRes.currentRecords[0].systemNumber, - processRecordsRes.currentRecords[0].createdTimestamp) - : processRecordsRes.provisionalCount === 1 - ? this.callGetTechRecords(processRecordsRes.provisionalRecords[0].systemNumber, - processRecordsRes.provisionalRecords[0].createdTimestamp) - : this.callGetTechRecords(processRecordsRes.provisionalRecords[1].systemNumber, - processRecordsRes.provisionalRecords[1].createdTimestamp); - } else { - await Promise.reject("Tech record Search returned nothing."); - } - } - - /** - * helper function is used to process records and count provisional and current records - * @param records - */ - public groupRecordsByStatusCode = (records: ISearchResult[] - ): { currentRecords: ISearchResult[]; provisionalRecords: ISearchResult[]; currentCount: number; provisionalCount: number; } => { - const currentRecords: ISearchResult[] = []; - const provisionalRecords: ISearchResult[] = []; - records.forEach((record) => { - if (record.techRecord_statusCode === "current") { - currentRecords.push(record); - } else if (record.techRecord_statusCode === "provisional") { - provisionalRecords.push(record); - } - }); - - return { - currentRecords, - provisionalRecords, - currentCount: currentRecords.length, - provisionalCount: provisionalRecords.length - }; - } - /** - * Retrieves the vehicle weight details for Roadworthisness certificates - * @param testResult - */ - public async getWeightDetails(testResult: any) { - const searchRes = await this.callSearchTechRecords(testResult.systemNumber); - const techRecord = await this.processGetCurrentProvisionalRecords(searchRes) as TechRecordType<"hgv" | "psv" | "trl">; - if (techRecord) { - const weightDetails: IWeightDetails = { - dgvw: techRecord.techRecord_grossDesignWeight ?? 0, - weight2: 0, - }; - if (testResult.vehicleType === VEHICLE_TYPES.HGV) { - weightDetails.weight2 = (techRecord as TechRecordType<"hgv">).techRecord_trainDesignWeight ?? 0; - } else { - if ( - (techRecord.techRecord_noOfAxles ?? -1) > 0 - ) { - const initialValue: number = 0; - weightDetails.weight2 = (techRecord.techRecord_axles as any).reduce( - ( - accumulator: number, - currentValue: { weights_designWeight: number } - ) => accumulator + currentValue.weights_designWeight, - initialValue - ); - } else { - throw new HTTPError( - 500, - "No axle weights for Roadworthiness test certificates!" - ); - } - } - return weightDetails; - } else { - console.log("No techRecord found for weight details"); - throw new HTTPError( - 500, - "No vehicle found for Roadworthiness test certificate!" - ); - } - } - - /** - * Retrieves the odometer history for a given VIN from the Test Results microservice - * @param systemNumber - systemNumber for which to retrieve odometer history - */ - public async getOdometerHistory(systemNumber: string) { - const config: IInvokeConfig = this.config.getInvokeConfig(); - const invokeParams: InvocationRequest = { - FunctionName: config.functions.testResults.name, - InvocationType: "RequestResponse", - LogType: "Tail", - Payload: toUint8Array(JSON.stringify({ - httpMethod: "GET", - path: `/test-results/${systemNumber}`, - pathParameters: { - systemNumber, - }, - })), - }; - - return this.lambdaClient - .invoke(invokeParams) - .then( - ( - response: InvocationResponse - ) => { - const payload: any = - this.lambdaClient.validateInvocationResponse(response); - // TODO: convert to correct type - const testResults: any[] = JSON.parse(payload.body); - - if (!testResults || testResults.length === 0) { - throw new HTTPError( - 400, - `${ERRORS.LAMBDA_INVOCATION_BAD_DATA} ${JSON.stringify(payload)}.` - ); - } - // Sort results by testEndTimestamp - testResults.sort((first: any, second: any): number => { - if ( - moment(first.testEndTimestamp).isBefore(second.testEndTimestamp) - ) { - return 1; - } - - if ( - moment(first.testEndTimestamp).isAfter(second.testEndTimestamp) - ) { - return -1; - } - - return 0; - }); - - // Remove the first result as it should be the current one. - testResults.shift(); - - // Set the array to only submitted tests (exclude cancelled) - const submittedTests = testResults.filter((testResult) => { - return testResult.testStatus === "submitted"; - }); - - const filteredTestResults = submittedTests - .filter(({ testTypes }) => - testTypes?.some( - (testType: ITestType) => - testType.testTypeClassification === - "Annual With Certificate" && - (testType.testResult === "pass" || - testType.testResult === "prs") - ) - ) - .slice(0, 3); // Only last three entries are used for the history. - - return { - OdometerHistoryList: filteredTestResults.map((testResult) => { - return { - value: testResult.odometerReading, - unit: testResult.odometerReadingUnits, - date: moment(testResult.testEndTimestamp).format("DD.MM.YYYY"), - }; - }), - }; - } - ) - .catch((error: ServiceException | Error) => { - console.log(error); - throw error; - }); - } - - /** - * Method for getting make and model based on the vehicle from a test-result - * @param testResult - the testResult for which the tech record search is done for - */ - public getVehicleMakeAndModel = async (testResult: any) => { - const searchRes = await this.callSearchTechRecords(testResult.systemNumber); - const techRecord = await this.processGetCurrentProvisionalRecords(searchRes); - // Return bodyMake and bodyModel values for PSVs - return techRecord?.techRecord_vehicleType === VEHICLE_TYPES.PSV ? { - Make: (techRecord as TechRecordType<"psv">).techRecord_chassisMake, - Model: (techRecord as TechRecordType<"psv">).techRecord_chassisModel - } : { - Make: (techRecord as TechRecordType<"hgv" | "trl">).techRecord_make, - Model: (techRecord as TechRecordType<"hgv" | "trl">).techRecord_model - }; - } - - /** - * Used to return a subset of technical record information. - * @param searchIdentifier - */ - public callSearchTechRecords = async (searchIdentifier: string) => { - const config: IInvokeConfig = this.config.getInvokeConfig(); - const invokeParams: InvocationRequest = { - FunctionName: config.functions.techRecordsSearch.name, - InvocationType: "RequestResponse", - LogType: "Tail", - Payload: toUint8Array(JSON.stringify({ - httpMethod: "GET", - path: `/v3/technical-records/search/${searchIdentifier}?searchCriteria=systemNumber`, - pathParameters: { - searchIdentifier - }, - })), - }; - try { - const lambdaResponse = await this.lambdaClient.invoke(invokeParams); - const res = await this.lambdaClient.validateInvocationResponse(lambdaResponse); - return JSON.parse(res.body); - } catch (e) { - console.log("Error searching technical records"); - console.log(JSON.stringify(e)); - return undefined; - } - } - - /** - * Used to get a singular whole technical record. - * @param systemNumber - * @param createdTimestamp - */ - public callGetTechRecords = async (systemNumber: string, createdTimestamp: string): Promise | undefined> => { - const config: IInvokeConfig = this.config.getInvokeConfig(); - const invokeParams: InvocationRequest = { - FunctionName: config.functions.techRecords.name, - InvocationType: "RequestResponse", - LogType: "Tail", - Payload: toUint8Array(JSON.stringify({ - httpMethod: "GET", - path: `/v3/technical-records/${systemNumber}/${createdTimestamp}`, - pathParameters: { - systemNumber, - createdTimestamp - } - })), - }; - try { - const lambdaResponse = await this.lambdaClient.invoke(invokeParams); - const res = await this.lambdaClient.validateInvocationResponse(lambdaResponse); - return JSON.parse(res.body); - } catch (e) { - console.log("Error in get technical record"); - console.log(JSON.stringify(e)); - return undefined; - } - } - - - /** - * To fetch trailer registration - * @param vin The vin of the trailer - * @param make The make of the trailer - * @returns A payload containing the TRN of the trailer and a boolean. - */ - public async getTrailerRegistrationObject(vin: string, make: string) { - const config: IInvokeConfig = this.config.getInvokeConfig(); - const invokeParams: InvocationRequest = { - FunctionName: config.functions.trailerRegistration.name, - InvocationType: "RequestResponse", - LogType: "Tail", - Payload: toUint8Array(JSON.stringify({ - httpMethod: "GET", - path: `/v1/trailers/${vin}`, - pathParameters: { - proxy: `/v1/trailers`, - }, - queryStringParameters: { - make, - }, - })), - }; - const response = await this.lambdaClient.invoke(invokeParams); - try { - if (!response.Payload || Buffer.from(response.Payload).toString() === "") { - throw new HTTPError( - 500, - `${ERRORS.LAMBDA_INVOCATION_ERROR} ${response.StatusCode} ${ERRORS.EMPTY_PAYLOAD}` - ); - } - const payload: any = JSON.parse(Buffer.from(response.Payload).toString()); - if (payload.statusCode === 404) { - console.debug(`vinOrChassisWithMake not found ${vin + make}`); - return { Trn: undefined, IsTrailer: true }; - } - if (payload.statusCode >= 400) { - throw new HTTPError( - 500, - `${ERRORS.LAMBDA_INVOCATION_ERROR} ${payload.statusCode} ${payload.body}` - ); - } - const trailerRegistration = JSON.parse( - payload.body - ) as ITrailerRegistration; - return { Trn: trailerRegistration.trn, IsTrailer: true }; - } catch (err) { - console.error( - `Error on fetching vinOrChassisWithMake ${vin + make}`, - err - ); - throw err; - } - } - - /** - * To check if the testResult is valid for fetching Trn. - * @param vehicleType the vehicle type - * @param makeAndModel object containing Make and Model - * @returns returns if the condition is satisfied else false - */ - public isValidForTrn( - vehicleType: string, - makeAndModel: IMakeAndModel - ): boolean { - return makeAndModel && vehicleType === VEHICLE_TYPES.TRL; - } - - /** - * Method used to retrieve the Welsh translations for the certificates - * @returns a list of defects - */ - public async getDefectTranslations(): Promise { - const config: IInvokeConfig = this.config.getInvokeConfig(); - const invokeParams: InvocationRequest = { - FunctionName: config.functions.defects.name, - InvocationType: "RequestResponse", - LogType: "Tail", - Payload: toUint8Array(JSON.stringify({ - httpMethod: "GET", - path: `/defects/`, - })), - }; - let defects: IDefectParent[] = []; - let retries = 0; - while (retries < 3) { - try { - const response: InvocationResponse = await this.lambdaClient.invoke(invokeParams); - const payload: any = this.lambdaClient.validateInvocationResponse(response); - const defectsParsed = JSON.parse(payload.body); - - if (!defectsParsed || defectsParsed.length === 0) { - throw new HTTPError(400, `${ERRORS.LAMBDA_INVOCATION_BAD_DATA} ${JSON.stringify(payload)}.`); - } - defects = defectsParsed; - return defects; - } catch (error) { - retries++; - console.error(`There was an error retrieving the welsh defect translations on attempt ${retries}: ${error}`); - } - } - return defects; - } - - /** - * Generates an object containing defects for a given test type and certificate type - * @param testTypes - the source test type for defect generation - * @param type - the certificate type - * @param vehicleType - the vehicle type from the test result - * @param flattenedDefects - the list of flattened defects after being retrieved from the defect service - * @param isWelsh - determines whether the atf in which the test result was conducted resides in Wales - */ - private generateDefects(testTypes: any, type: string, vehicleType: string, flattenedDefects: IFlatDefect[], isWelsh: boolean = false) { - const rawDefects: any = testTypes.defects; - const defects: any = { - DangerousDefects: [], - MajorDefects: [], - PRSDefects: [], - MinorDefects: [], - AdvisoryDefects: [], - DangerousDefectsWelsh: [], - MajorDefectsWelsh: [], - PRSDefectsWelsh: [], - MinorDefectsWelsh: [], - AdvisoryDefectsWelsh: [], - }; - - rawDefects.forEach((defect: any) => { - switch (defect.deficiencyCategory.toLowerCase()) { - case "dangerous": - if ( - (testTypes.testResult === TEST_RESULTS.PRS || defect.prs) && - type === CERTIFICATE_DATA.FAIL_DATA - ) { - defects.PRSDefects.push(this.formatDefect(defect)); - if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { - defects.PRSDefectsWelsh.push(this.formatDefectWelsh(defect, vehicleType, flattenedDefects)); - } - } else if (testTypes.testResult === "fail") { - defects.DangerousDefects.push(this.formatDefect(defect)); - // If the test was conducted in Wales and is valid vehicle type, format and add the welsh defects to the list - if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { - defects.DangerousDefectsWelsh.push( - this.formatDefectWelsh(defect, vehicleType, flattenedDefects) - ); - } - } - break; - case "major": - if ( - (testTypes.testResult === TEST_RESULTS.PRS || defect.prs) && - type === CERTIFICATE_DATA.FAIL_DATA - ) { - defects.PRSDefects.push(this.formatDefect(defect)); - if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { - defects.PRSDefectsWelsh.push(this.formatDefectWelsh(defect, vehicleType, flattenedDefects)); - } - } else if (testTypes.testResult === "fail") { - defects.MajorDefects.push(this.formatDefect(defect)); - // If the test was conducted in Wales and is valid vehicle type, format and add the welsh defects to the list - if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { - defects.MajorDefectsWelsh.push( - this.formatDefectWelsh(defect, vehicleType, flattenedDefects) - ); - } - } - break; - case "minor": - defects.MinorDefects.push(this.formatDefect(defect)); - if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { - defects.MinorDefectsWelsh.push( - this.formatDefectWelsh(defect, vehicleType, flattenedDefects) - ); - } - break; - case "advisory": - defects.AdvisoryDefects.push(this.formatDefect(defect)); - if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { - defects.AdvisoryDefectsWelsh.push(this.formatDefect(defect)); - } - break; - } - }); - - Object.entries(defects).forEach(([k, v]: [string, any]) => { - if (v.length === 0) { - Object.assign(defects, { [k]: undefined }); - } - }); - console.log(JSON.stringify(defects)); - return defects; - } - - /** - * Check that the test result and vehicle type are a valid combination and bilingual certificate is available - * @param vehicleType - the vehicle type from the test result - * @param testResult - the result of the test - */ - public isWelshCertificateAvailable = (vehicleType: string, testResult: string): boolean => { - return AVAILABLE_WELSH.CERTIFICATES.includes(`${vehicleType}_${testResult}`); - } - - /** - * Returns a formatted string containing data about a given defect - * @param defect - defect for which to generate the formatted string - */ - private formatDefect(defect: any) { - const toUpperFirstLetter: any = (word: string) => - word.charAt(0).toUpperCase() + word.slice(1); - - let defectString = `${defect.deficiencyRef} ${defect.itemDescription}`; - - if (defect.deficiencyText) { - defectString += ` ${defect.deficiencyText}`; - } - - if (defect.additionalInformation.location) { - Object.keys(defect.additionalInformation.location).forEach( - (location: string, index: number, array: string[]) => { - if (defect.additionalInformation.location[location]) { - switch (location) { - case "rowNumber": - defectString += ` Rows: ${defect.additionalInformation.location.rowNumber}.`; - break; - case "seatNumber": - defectString += ` Seats: ${defect.additionalInformation.location.seatNumber}.`; - break; - case "axleNumber": - defectString += ` Axles: ${defect.additionalInformation.location.axleNumber}.`; - break; - default: - defectString += ` ${toUpperFirstLetter( - defect.additionalInformation.location[location] - )}`; - break; - } - } - - if (index === array.length - 1) { - defectString += `.`; - } - } - ); - } - - if (defect.additionalInformation.notes) { - defectString += ` ${defect.additionalInformation.notes}`; - } - - return defectString; - } - - /** - * Returns a formatted welsh string containing data about a given defect - * @param defect - the defect for which to generate the formatted welsh string - * @param vehicleType - the vehicle type from the test result - * @param flattenedDefects - the list of flattened defects - */ - public formatDefectWelsh( - defect: any, - vehicleType: any, - flattenedDefects: IFlatDefect[] - ) { - const toUpperFirstLetter: any = (word: string) => - word.charAt(0).toUpperCase() + word.slice(1); - - const filteredFlatDefects: IFlatDefect[] = flattenedDefects.filter( - (x: IFlatDefect) => defect.deficiencyRef === x.ref - ); - - const filteredFlatDefect: IFlatDefect | null = this.filterFlatDefects( - filteredFlatDefects, - vehicleType - ); - - if (filteredFlatDefect !== null) { - let defectString = `${defect.deficiencyRef} ${filteredFlatDefect.itemDescriptionWelsh}`; - - if (defect.deficiencyText) { - defectString += ` ${filteredFlatDefect.deficiencyTextWelsh}`; - } - - if (defect.additionalInformation.location) { - Object.keys(defect.additionalInformation.location).forEach( - (location: string, index: number, array: string[]) => { - if (defect.additionalInformation.location[location]) { - switch (location) { - case "rowNumber": - defectString += ` ${LOCATION_WELSH.ROW_NUMBER}: ${defect.additionalInformation.location.rowNumber}.`; - break; - case "seatNumber": - defectString += ` ${LOCATION_WELSH.SEAT_NUMBER}: ${defect.additionalInformation.location.seatNumber}.`; - break; - case "axleNumber": - defectString += ` ${LOCATION_WELSH.AXLE_NUMBER}: ${defect.additionalInformation.location.axleNumber}.`; - break; - default: - const welshLocation = this.convertLocationWelsh( - defect.additionalInformation.location[location] - ); - defectString += ` ${toUpperFirstLetter(welshLocation)}`; - break; - } - } - - if (index === array.length - 1) { - defectString += `.`; - } - } - ); - } - - if (defect.additionalInformation.notes) { - defectString += ` ${defect.additionalInformation.notes}`; - } - console.log(`Welsh Defect String Generated: ${defectString}`); - return defectString; - } else { - console.log(`ERROR: Unable to find a filtered defect`); - return null; - } - } - - /** - * Returns welsh version of location - * @param locationToTranslate - */ - public convertLocationWelsh(locationToTranslate: string) { - switch (locationToTranslate) { - case LOCATION_ENGLISH.FRONT: - return LOCATION_WELSH.FRONT; - case LOCATION_ENGLISH.REAR: - return LOCATION_WELSH.REAR; - case LOCATION_ENGLISH.UPPER: - return LOCATION_WELSH.UPPER; - case LOCATION_ENGLISH.LOWER: - return LOCATION_WELSH.LOWER; - case LOCATION_ENGLISH.NEARSIDE: - return LOCATION_WELSH.NEARSIDE; - case LOCATION_ENGLISH.OFFSIDE: - return LOCATION_WELSH.OFFSIDE; - case LOCATION_ENGLISH.CENTRE: - return LOCATION_WELSH.CENTRE; - case LOCATION_ENGLISH.INNER: - return LOCATION_WELSH.INNER; - case LOCATION_ENGLISH.OUTER: - return LOCATION_WELSH.OUTER; - default: - return locationToTranslate; - } - } - - /** - * Returns filtered welsh defects - * @param filteredFlatDefects - the array of flattened defects - * @param vehicleType - the vehicle type from the test result - */ - public filterFlatDefects( - filteredFlatDefects: IFlatDefect[], - vehicleType: string - ): IFlatDefect | null { - if (filteredFlatDefects.length === 0) { - return null; - } else if (filteredFlatDefects.length === 1) { - return filteredFlatDefects[0]; - } else { - const filteredWelshDefectsOnVehicleType = filteredFlatDefects.filter( - (flatDefect: IFlatDefect) => - flatDefect.forVehicleType!.includes(vehicleType) - ); - return filteredWelshDefectsOnVehicleType[0]; - } - } - - /** - * Returns a flattened array of every deficiency that only includes the key/value pairs required for certificate generation - * @param defects - the array of defects from the api - */ - public flattenDefectsFromApi(defects: IDefectParent[]): IFlatDefect[] { - const flatDefects: IFlatDefect[] = []; - try { - // go through each defect in un-flattened array - defects.forEach((defect: IDefectParent) => { - const { imNumber, imDescription, imDescriptionWelsh, items } = defect; - if (defect.items !== undefined && defect.items.length !== 0) { - // go through each item of defect - items.forEach((item: IItem) => { - const { - itemNumber, - itemDescription, - itemDescriptionWelsh, - deficiencies, - } = item; - if ( - item.deficiencies !== undefined && - item.deficiencies.length !== 0 - ) { - // go through each deficiency and push to flatDefects array - deficiencies.forEach((deficiency: IDefectChild) => { - const { - ref, - deficiencyText, - deficiencyTextWelsh, - forVehicleType, - } = deficiency; - const lowLevelDeficiency: IFlatDefect = { - imNumber, - imDescription, - imDescriptionWelsh, - itemNumber, - itemDescription, - itemDescriptionWelsh, - ref, - deficiencyText, - deficiencyTextWelsh, - forVehicleType, - }; - flatDefects.push(lowLevelDeficiency); - }); - } - }); - } - }); - } catch (e) { - console.error(`Error flattening defects: ${e}`); - } - return flatDefects; - } - - /** - * Returns true if testType is adr and false if not - * @param testType - testType which is tested - */ - public isTestTypeAdr(testType: any): boolean { - return ADR_TEST.IDS.includes(testType.testTypeId); - } - - /** - * Returns a boolean value indicating whether the test type is a basic IVA test - * @param testTypeId - the test type ID on the test result - */ - public isBasicIvaTest = (testTypeId: string): boolean => { - return BASIC_IVA_TEST.IDS.includes(testTypeId); - } - - /** - * Returns true if testType is iva and false if not - * @param testTypeId - test type id which is being tested - */ - public isIvaTest(testTypeId: string): boolean { - return IVA30_TEST.IDS.includes(testTypeId); - } - - /** - * Returns true if testType is msva and false if not - * @param testTypeId - test type id which is being tested - */ - public isMsvaTest(testTypeId: string): boolean { - return MSVA30_TEST.IDS.includes(testTypeId); - } - - //#region Private Static Functions - - /** - * Returns true if testType is roadworthiness test for HGV or TRL and false if not - * @param testTypeId - testType which is tested - */ - private static isRoadworthinessTestType(testTypeId: string): boolean { - return HGV_TRL_ROADWORTHINESS_TEST_TYPES.IDS.includes(testTypeId); - } - - /** - * Returns true if provided testResult is HGV or TRL Roadworthiness test otherwise false - * @param testResult - testResult of the vehicle - */ - private static isHgvTrlRoadworthinessCertificate(testResult: any): boolean { - return ( - (testResult.vehicleType === VEHICLE_TYPES.HGV || - testResult.vehicleType === VEHICLE_TYPES.TRL) && - CertificateGenerationService.isRoadworthinessTestType( - testResult.testTypes.testTypeId - ) - ); - } - - /** - * Sorts required standards if present by refCalculation and then returns it - * @param requiredStandards - the requiredStandards array to sort - * @returns - the sorted requiredStandards array - */ - private sortRequiredStandards = (requiredStandards: IRequiredStandard[] | undefined): IRequiredStandard[] | undefined => { - if (!requiredStandards) { - return; - } - - const collator = new Intl.Collator("en", { numeric: true, sensitivity: "base" }); - return requiredStandards - .sort((a, b) => - collator.compare(a.refCalculation, b.refCalculation) - ); - } - //#endregion + private readonly s3Client: S3BucketService; + private readonly config: Configuration; + private readonly lambdaClient: LambdaService; + + constructor(s3Client: S3BucketService, lambdaClient: LambdaService) { + this.s3Client = s3Client; + this.config = Configuration.getInstance(); + this.lambdaClient = lambdaClient; + } + + /** + * Generates MOT certificate for a given test result + * @param testResult - source test result for certificate generation + */ + public async generateCertificate(testResult: any): Promise { + const config: IMOTConfig = this.config.getMOTConfig(); + const iConfig: IInvokeConfig = this.config.getInvokeConfig(); + const testType: any = testResult.testTypes; + + const shouldTranslateTestResult = await this.shouldTranslateTestResult(testResult); + + const payload: string = JSON.stringify(await this.generatePayload(testResult, shouldTranslateTestResult)); + + const certificateTypes: any = { + psv_pass: config.documentNames.vtp20, + psv_pass_bilingual: config.documentNames.vtp20_bilingual, + psv_fail: config.documentNames.vtp30, + psv_fail_bilingual: config.documentNames.vtp30_bilingual, + psv_prs: config.documentNames.psv_prs, + psv_prs_bilingual: config.documentNames.psv_prs_bilingual, + hgv_pass: config.documentNames.vtg5, + hgv_pass_bilingual: config.documentNames.vtg5_bilingual, + hgv_fail: config.documentNames.vtg30, + hgv_fail_bilingual: config.documentNames.vtg30_bilingual, + hgv_prs: config.documentNames.hgv_prs, + hgv_prs_bilingual: config.documentNames.hgv_prs_bilingual, + trl_pass: config.documentNames.vtg5a, + trl_pass_bilingual: config.documentNames.vtg5a_bilingual, + trl_fail: config.documentNames.vtg30, + trl_fail_bilingual: config.documentNames.vtg30_bilingual, + trl_prs: config.documentNames.trl_prs, + trl_prs_bilingual: config.documentNames.trl_prs_bilingual, + rwt: config.documentNames.rwt, + adr_pass: config.documentNames.adr_pass, + iva_fail: config.documentNames.iva_fail, + msva_fail: config.documentNames.msva_fail, + }; + + let vehicleTestRes: string; + if (CertificateGenerationService.isRoadworthinessTestType(testType.testTypeId)) { + // CVSB-7677 is road-worthiness test + vehicleTestRes = 'rwt'; + } else if (this.isTestTypeAdr(testResult.testTypes)) { + vehicleTestRes = 'adr_pass'; + } else if (this.isIvaTest(testResult.testTypes.testTypeId) && testType.testResult === 'fail') { + vehicleTestRes = 'iva_fail'; + } else if (this.isMsvaTest(testResult.testTypes.testTypeId) && testType.testResult === 'fail') { + vehicleTestRes = 'msva_fail'; + } else if ( + this.isWelshCertificateAvailable(testResult.vehicleType, testType.testResult) && + shouldTranslateTestResult + ) { + vehicleTestRes = testResult.vehicleType + '_' + testType.testResult + '_bilingual'; + } else { + vehicleTestRes = testResult.vehicleType + '_' + testType.testResult; + } + + console.log(`vehicleTestRes: ${vehicleTestRes}`); + + const invokeParams: InvocationRequest = { + FunctionName: iConfig.functions.certGen.name, + InvocationType: 'RequestResponse', + LogType: 'Tail', + Payload: toUint8Array( + JSON.stringify({ + httpMethod: 'POST', + pathParameters: { + documentName: certificateTypes[vehicleTestRes], + documentDirectory: config.documentDir, + }, + json: true, + body: payload, + }) + ), + }; + return this.lambdaClient + .invoke(invokeParams) + .then(async (response: InvocationResponse) => { + const documentPayload: any = await this.lambdaClient.validateInvocationResponse(response); + const resBody: string = documentPayload.body; + const responseBuffer: Buffer = Buffer.from(resBody, 'base64'); + console.log('return from doc gen!'); + return { + vrm: testResult.vehicleType === VEHICLE_TYPES.TRL ? testResult.trailerId : testResult.vrm, + testTypeName: testResult.testTypes.testTypeName, + testTypeResult: testResult.testTypes.testResult, + dateOfIssue: moment(testResult.testTypes.testTypeStartTimestamp).format('D MMMM YYYY'), + certificateType: certificateTypes[vehicleTestRes].split('.')[0], + fileFormat: 'pdf', + fileName: `${testResult.testTypes.testNumber}_${testResult.vin}.pdf`, + fileSize: responseBuffer.byteLength.toString(), + certificate: responseBuffer, + certificateOrder: testResult.order, + email: testResult.createdByEmailAddress ?? testResult.testerEmailAddress, + shouldEmailCertificate: testResult.shouldEmailCertificate ?? 'true', + }; + }) + .catch((error: ServiceException | Error) => { + console.log(error); + throw error; + }); + } + + /** + * Handler method for retrieving feature flags and checking if test station is in Wales + * @param testResult + * @returns Promise + */ + public async shouldTranslateTestResult(testResult: any): Promise { + let shouldTranslateTestResult = false; + try { + const featureFlags: IFeatureFlags = await getProfile(); + console.log('Using feature flags ', featureFlags); + + if ( + this.isGlobalWelshFlagEnabled(featureFlags) && + this.isTestResultFlagEnabled(testResult.testTypes.testResult, featureFlags) + ) { + shouldTranslateTestResult = await this.isTestStationWelsh(testResult.testStationPNumber); + } + } catch (e) { + console.error(`Failed to retrieve feature flags - ${e}`); + } + return shouldTranslateTestResult; + } + + /** + * Method to check if Welsh translation is enabled. + * @param featureFlags IFeatureFlags interface + * @returns boolean + */ + public isGlobalWelshFlagEnabled(featureFlags: IFeatureFlags): boolean { + if (!featureFlags.welshTranslation.enabled) { + console.warn(`Unable to translate any test results: global Welsh flag disabled.`); + return false; + } + return true; + } + + /** + * Method to check if Welsh translation is enabled for the given test type. + * @param featureFlags IFeatureFlags interface + * @param testResult string of result, PASS/PRS/FAIL + * @returns boolean + */ + public isTestResultFlagEnabled(testResult: string, featureFlags: IFeatureFlags): boolean { + let shouldTranslate = false; + switch (testResult) { + case TEST_RESULTS.PRS: + shouldTranslate = featureFlags.welshTranslation.translatePrsTestResult ?? false; + break; + case TEST_RESULTS.PASS: + shouldTranslate = featureFlags.welshTranslation.translatePassTestResult ?? false; + break; + case TEST_RESULTS.FAIL: + shouldTranslate = featureFlags.welshTranslation.translateFailTestResult ?? false; + break; + default: + console.warn('Translation not available for this test result type.'); + return shouldTranslate; + } + if (!shouldTranslate) { + console.warn(`Unable to translate for test result: ${testResult} flag disabled`); + } + return shouldTranslate; + } + + /** + * Determines if a test station is located in Wales + * @param testStationPNumber The test station's P-number. + * @returns Promise true if the test station country is set to Wales, false otherwise + */ + public async isTestStationWelsh(testStationPNumber: string): Promise { + const testStation = await this.getTestStation(testStationPNumber); + + if (!testStation.testStationPNumber) { + console.error(`Failed to retrieve test station details for ${testStationPNumber}`); + return false; + } + + const isWelshCountry = testStation.testStationCountry?.toString().toUpperCase() === `WALES`; + console.log(`Test station country for ${testStationPNumber} is set to ${testStation.testStationCountry}`); + return isWelshCountry; + } + + /** + * Method to retrieve Test Station details from API + * @returns a test station object + */ + public async getTestStation(testStationPNumber: string): Promise { + const config: IInvokeConfig = this.config.getInvokeConfig(); + const invokeParams: InvocationRequest = { + FunctionName: config.functions.testStations.name, + InvocationType: 'RequestResponse', + LogType: 'Tail', + Payload: toUint8Array( + JSON.stringify({ + httpMethod: 'GET', + path: `/test-stations/${testStationPNumber}`, + }) + ), + }; + let testStation: ITestStation = {} as ITestStation; + let retries = 0; + + while (retries < 3) { + try { + const response: InvocationResponse = await this.lambdaClient.invoke(invokeParams); + const payload: any = this.lambdaClient.validateInvocationResponse(response); + + testStation = JSON.parse(payload.body); + + return testStation; + } catch (error) { + retries++; + console.error(`There was an error retrieving the test station on attempt ${retries}: ${error}`); + } + } + return testStation; + } + + /** + * Retrieves a signature from the cvs-signature S3 bucket + * @param staffId - staff ID of the signature you want to retrieve + * @returns the signature as a base64 encoded string + */ + public async getSignature(staffId: string): Promise { + try { + const result: GetObjectOutput = await this.s3Client.download( + `cvs-signature-${process.env.BUCKET}`, + `${staffId}.base64` + ); + + if (result.Body instanceof Readable) { + const chunks: Uint8Array[] = []; + for await (const chunk of result.Body) { + chunks.push(chunk); + } + const buffer = Buffer.concat(chunks); + return buffer.toString('utf-8'); + } else { + throw new Error(`Unexpected body type: ${typeof result.Body}`); + } + } catch (error) { + console.error(`Unable to fetch signature for staff id ${staffId}. ${(error as Error).message}`); + } + return null; + } + + /** + * Generates the payload for the MOT certificate generation service + * @param testResult - source test result for certificate generation + * @param isWelsh - the boolean value whether the atf where test was conducted resides in Wales + */ + public async generatePayload(testResult: any, isWelsh = false) { + let name = testResult.testerName; + + const nameArrayList: string[] = name.split(','); + + if (nameArrayList.length === 2) { + name = name.split(', ').reverse().join(' '); + testResult.testerName = name; + } + + const signature: string | null = await this.getSignature(testResult.createdById ?? testResult.testerStaffId); + + let makeAndModel: any = null; + if (!CertificateGenerationService.isRoadworthinessTestType(testResult.testTypes.testTypeId)) { + makeAndModel = await this.getVehicleMakeAndModel(testResult); + } + + let payload: ICertificatePayload = { + Watermark: process.env.BRANCH === 'prod' ? '' : 'NOT VALID', + DATA: undefined, + FAIL_DATA: undefined, + RWT_DATA: undefined, + ADR_DATA: undefined, + IVA_DATA: undefined, + MSVA_DATA: undefined, + Signature: { + ImageType: 'png', + ImageData: signature, + }, + }; + + const { testTypes, vehicleType, systemNumber, testHistory } = testResult; + + if (testHistory) { + for (const history of testHistory) { + for (const testType of history.testTypes) { + if (testType.testCode === testTypes.testCode) { + payload.Reissue = { + Reason: 'Replacement', + Issuer: testResult.createdByName, + Date: moment(testResult.createdAt).format('DD.MM.YYYY'), + }; + break; + } + } + } + } + + if (CertificateGenerationService.isHgvTrlRoadworthinessCertificate(testResult)) { + // CVSB-7677 for roadworthiness test for hgv or trl. + const rwtData = await this.generateCertificateData(testResult, CERTIFICATE_DATA.RWT_DATA); + payload.RWT_DATA = { ...rwtData }; + } else if (testResult.testTypes.testResult === TEST_RESULTS.PASS && this.isTestTypeAdr(testResult.testTypes)) { + const adrData = await this.generateCertificateData(testResult, CERTIFICATE_DATA.ADR_DATA); + payload.ADR_DATA = { ...adrData, ...makeAndModel }; + } else if ( + testResult.testTypes.testResult === TEST_RESULTS.FAIL && + this.isIvaTest(testResult.testTypes.testTypeId) + ) { + const ivaData = await this.generateCertificateData(testResult, CERTIFICATE_DATA.IVA_DATA); + payload.IVA_DATA = { ...ivaData }; + } else if ( + testResult.testTypes.testResult === TEST_RESULTS.FAIL && + this.isMsvaTest(testResult.testTypes.testTypeId) + ) { + const msvaData = await this.generateCertificateData(testResult, CERTIFICATE_DATA.MSVA_DATA); + payload.MSVA_DATA = { ...msvaData }; + } else { + const odometerHistory = + vehicleType === VEHICLE_TYPES.TRL ? undefined : await this.getOdometerHistory(systemNumber); + const TrnObj = this.isValidForTrn(vehicleType, makeAndModel) + ? await this.getTrailerRegistrationObject(testResult.vin, makeAndModel.Make) + : undefined; + if (testTypes.testResult !== TEST_RESULTS.FAIL) { + const passData = await this.generateCertificateData(testResult, CERTIFICATE_DATA.PASS_DATA, isWelsh); + payload.DATA = { + ...passData, + ...makeAndModel, + ...odometerHistory, + ...TrnObj, + }; + } + if (testTypes.testResult !== TEST_RESULTS.PASS) { + const failData = await this.generateCertificateData(testResult, CERTIFICATE_DATA.FAIL_DATA, isWelsh); + payload.FAIL_DATA = { + ...failData, + ...makeAndModel, + ...odometerHistory, + ...TrnObj, + }; + } + } + // Purge undefined values + payload = JSON.parse(JSON.stringify(payload)); + + return payload; + } + + /** + * Generates certificate data for a given test result and certificate type + * @param testResult - the source test result for certificate generation + * @param type - the certificate type + * @param isWelsh - the boolean value whether the atf where test was conducted resides in Wales + */ + public async generateCertificateData(testResult: ITestResult, type: string, isWelsh = false) { + let defectListFromApi: IDefectParent[] = []; + let flattenedDefects: IFlatDefect[] = []; + if (isWelsh) { + defectListFromApi = await this.getDefectTranslations(); + flattenedDefects = this.flattenDefectsFromApi(defectListFromApi); + } + const testType: any = testResult.testTypes; + switch (type) { + case CERTIFICATE_DATA.PASS_DATA: + case CERTIFICATE_DATA.FAIL_DATA: + const defects: any = await this.generateDefects( + testResult.testTypes, + type, + testResult.vehicleType, + flattenedDefects, + isWelsh + ); + + return { + TestNumber: testType.testNumber, + TestStationPNumber: testResult.testStationPNumber, + TestStationName: testResult.testStationName, + CurrentOdometer: { + value: testResult.odometerReading, + unit: testResult.odometerReadingUnits, + }, + IssuersName: testResult.testerName, + DateOfTheTest: moment(testResult.testEndTimestamp).format('DD.MM.YYYY'), + CountryOfRegistrationCode: testResult.countryOfRegistration, + VehicleEuClassification: testResult.euVehicleCategory.toUpperCase(), + RawVIN: testResult.vin, + RawVRM: testResult.vehicleType === VEHICLE_TYPES.TRL ? testResult.trailerId : testResult.vrm, + ExpiryDate: testType.testExpiryDate ? moment(testType.testExpiryDate).format('DD.MM.YYYY') : undefined, + EarliestDateOfTheNextTest: + (testResult.vehicleType === VEHICLE_TYPES.HGV || testResult.vehicleType === VEHICLE_TYPES.TRL) && + (testResult.testTypes.testResult === TEST_RESULTS.PASS || + testResult.testTypes.testResult === TEST_RESULTS.PRS) + ? moment(testType.testAnniversaryDate).subtract(1, 'months').startOf('month').format('DD.MM.YYYY') + : moment(testType.testAnniversaryDate).format('DD.MM.YYYY'), + SeatBeltTested: testType.seatbeltInstallationCheckDate ? 'Yes' : 'No', + SeatBeltPreviousCheckDate: testType.lastSeatbeltInstallationCheckDate + ? moment(testType.lastSeatbeltInstallationCheckDate).format('DD.MM.YYYY') + : '\u00A0', + SeatBeltNumber: testType.numberOfSeatbeltsFitted, + ...defects, + }; + case CERTIFICATE_DATA.RWT_DATA: + const weightDetails = await this.getWeightDetails(testResult); + let defectRWTList: any; + if (testResult.testTypes.testResult === TEST_RESULTS.FAIL) { + defectRWTList = []; + testResult.testTypes.defects.forEach((defect: any) => { + defectRWTList.push(this.formatDefect(defect)); + }); + } else { + defectRWTList = undefined; + } + + const resultPass: IRoadworthinessCertificateData = { + Dgvw: weightDetails.dgvw, + Weight2: weightDetails.weight2, + VehicleNumber: testResult.vehicleType === VEHICLE_TYPES.TRL ? testResult.trailerId : testResult.vrm, + Vin: testResult.vin, + IssuersName: testResult.testerName, + DateOfInspection: moment(testType.testTypeStartTimestamp).format('DD.MM.YYYY'), + TestStationPNumber: testResult.testStationPNumber, + DocumentNumber: testType.certificateNumber, + Date: moment(testType.testTypeStartTimestamp).format('DD.MM.YYYY'), + Defects: defectRWTList, + IsTrailer: testResult.vehicleType === VEHICLE_TYPES.TRL, + }; + return resultPass; + case CERTIFICATE_DATA.ADR_DATA: + const adrDetails: TechRecordType = await this.getAdrDetails(testResult); + const docGenPayloadAdr = { + ChasisNumber: testResult.vin, + RegistrationNumber: testResult.vrm, + ApplicantDetails: { + name: adrDetails?.techRecord_applicantDetails_name, + address1: adrDetails?.techRecord_applicantDetails_address1, + address2: adrDetails?.techRecord_applicantDetails_address2, + address3: adrDetails?.techRecord_applicantDetails_address1, + postTown: adrDetails?.techRecord_applicantDetails_postTown, + postCode: adrDetails?.techRecord_applicantDetails_postCode, + telephoneNumber: adrDetails?.techRecord_applicantDetails_telephoneNumber, + emailAddress: adrDetails?.techRecord_applicantDetails_emailAddress, + }, + VehicleType: adrDetails?.techRecord_adrDetails_vehicleDetails_type, + PermittedDangerousGoods: adrDetails?.techRecord_adrDetails_permittedDangerousGoods, + BrakeEndurance: adrDetails?.techRecord_adrDetails_brakeEndurance, + Weight: adrDetails?.techRecord_adrDetails_weight, + TankManufacturer: adrDetails?.techRecord_adrDetails_tank_tankDetails_tankStatement_statement + ? adrDetails.techRecord_adrDetails_tank_tankDetails_tankManufacturer + : undefined, + Tc2InitApprovalNo: adrDetails?.techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo, + TankManufactureSerialNo: adrDetails?.techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo, + YearOfManufacture: adrDetails?.techRecord_adrDetails_tank_tankDetails_yearOfManufacture, + TankCode: adrDetails?.techRecord_adrDetails_tank_tankDetails_tankCode, + SpecialProvisions: adrDetails?.techRecord_adrDetails_tank_tankDetails_specialProvisions, + TankStatement: adrDetails?.techRecord_adrDetails_tank_tankDetails_tankStatement_statement, + ExpiryDate: testResult.testTypes.testExpiryDate, + AtfNameAtfPNumber: testResult.testStationName + ' ' + testResult.testStationPNumber, + Notes: testResult.testTypes.additionalNotesRecorded, + TestTypeDate: testResult.testTypes.testTypeStartTimestamp, + }; + console.log('CHECK HERE DOCGENPAYLOAD -> ', docGenPayloadAdr); + return docGenPayloadAdr; + case CERTIFICATE_DATA.IVA_DATA: + const ivaFailDetailsForDocGen = { + vin: testResult.vin, + serialNumber: testResult.vehicleType === 'trl' ? testResult.trailerId : testResult.vrm, + vehicleTrailerNrNo: testResult.vehicleType === 'trl' ? testResult.trailerId : testResult.vrm, + testCategoryClass: testResult.euVehicleCategory, + testCategoryBasicNormal: this.isBasicIvaTest(testResult.testTypes.testTypeId) ? IVA_30.BASIC : IVA_30.NORMAL, + make: testResult.make, + model: testResult.model, + bodyType: testResult.bodyType?.description, + date: moment(testResult.testTypes.testTypeStartTimestamp).format('DD/MM/YYYY'), + testerName: testResult.testerName, + reapplicationDate: testResult.testTypes?.reapplicationDate + ? moment(testResult.testTypes?.reapplicationDate).format('DD/MM/YYYY') + : '', + station: testResult.testStationName, + additionalDefects: this.formatVehicleApprovalAdditionalDefects(testResult.testTypes.customDefects), + requiredStandards: this.sortRequiredStandards(testResult.testTypes.requiredStandards), + }; + return ivaFailDetailsForDocGen; + case CERTIFICATE_DATA.MSVA_DATA: + const msvaFailDetailsForDocGen = { + vin: testResult.vin, + serialNumber: testResult.vrm, + vehicleZNumber: testResult.vrm, + make: testResult.make, + model: testResult.model, + type: testResult.vehicleType, + testerName: testResult.testerName, + date: moment(testResult.testTypes.testTypeStartTimestamp).format('DD/MM/YYYY'), + reapplicationDate: testResult.testTypes?.reapplicationDate + ? moment(testResult.testTypes?.reapplicationDate).format('DD/MM/YYYY') + : '', + station: testResult.testStationName, + additionalDefects: this.formatVehicleApprovalAdditionalDefects(testResult.testTypes.customDefects), + requiredStandards: this.sortRequiredStandards(testResult.testTypes.requiredStandards), + }; + return msvaFailDetailsForDocGen; + } + } + + /** + * Formats the additional defects for IVA and MSVA test based on whether custom defects is populated + * @param customDefects - the custom defects for the test + */ + public formatVehicleApprovalAdditionalDefects = ( + customDefects: ICustomDefect[] | undefined + ): ICustomDefect[] | undefined => { + const defaultCustomDefect: ICustomDefect = { + defectName: IVA_30.EMPTY_CUSTOM_DEFECTS, + defectNotes: '', + }; + return customDefects && customDefects.length > 0 ? customDefects : [defaultCustomDefect]; + }; + + /** + * Calculates the retest date for an IVA or MSVA test + * @param testTypeStartTimestamp - the test start timestamp of the test + */ + public calculateVehicleApprovalRetestDate = (testTypeStartTimestamp: string): string => { + return moment(testTypeStartTimestamp).add(6, 'months').subtract(1, 'day').format('DD/MM/YYYY'); + }; + + /** + * Retrieves the adrDetails from a techRecord searched by vin + * @param testResult - testResult from which the VIN is used to search a tech-record + */ + public getAdrDetails = async (testResult: any) => { + const searchRes = await this.callSearchTechRecords(testResult.systemNumber); + return (await this.processGetCurrentProvisionalRecords(searchRes)) as TechRecordType<'hgv' | 'trl'>; + }; + + public processGetCurrentProvisionalRecords = async ( + searchResult: ISearchResult[] + ): Promise | undefined> => { + if (searchResult) { + const processRecordsRes = this.groupRecordsByStatusCode(searchResult); + return processRecordsRes.currentCount !== 0 + ? this.callGetTechRecords( + processRecordsRes.currentRecords[0].systemNumber, + processRecordsRes.currentRecords[0].createdTimestamp + ) + : processRecordsRes.provisionalCount === 1 + ? this.callGetTechRecords( + processRecordsRes.provisionalRecords[0].systemNumber, + processRecordsRes.provisionalRecords[0].createdTimestamp + ) + : this.callGetTechRecords( + processRecordsRes.provisionalRecords[1].systemNumber, + processRecordsRes.provisionalRecords[1].createdTimestamp + ); + } else { + await Promise.reject('Tech record Search returned nothing.'); + } + }; + + /** + * helper function is used to process records and count provisional and current records + * @param records + */ + public groupRecordsByStatusCode = ( + records: ISearchResult[] + ): { + currentRecords: ISearchResult[]; + provisionalRecords: ISearchResult[]; + currentCount: number; + provisionalCount: number; + } => { + const currentRecords: ISearchResult[] = []; + const provisionalRecords: ISearchResult[] = []; + records.forEach((record) => { + if (record.techRecord_statusCode === 'current') { + currentRecords.push(record); + } else if (record.techRecord_statusCode === 'provisional') { + provisionalRecords.push(record); + } + }); + + return { + currentRecords, + provisionalRecords, + currentCount: currentRecords.length, + provisionalCount: provisionalRecords.length, + }; + }; + /** + * Retrieves the vehicle weight details for Roadworthisness certificates + * @param testResult + */ + public async getWeightDetails(testResult: any) { + const searchRes = await this.callSearchTechRecords(testResult.systemNumber); + const techRecord = (await this.processGetCurrentProvisionalRecords(searchRes)) as TechRecordType< + 'hgv' | 'psv' | 'trl' + >; + if (techRecord) { + const weightDetails: IWeightDetails = { + dgvw: techRecord.techRecord_grossDesignWeight ?? 0, + weight2: 0, + }; + if (testResult.vehicleType === VEHICLE_TYPES.HGV) { + weightDetails.weight2 = (techRecord as TechRecordType<'hgv'>).techRecord_trainDesignWeight ?? 0; + } else { + if ((techRecord.techRecord_noOfAxles ?? -1) > 0) { + const initialValue: number = 0; + weightDetails.weight2 = (techRecord.techRecord_axles as any).reduce( + (accumulator: number, currentValue: { weights_designWeight: number }) => + accumulator + currentValue.weights_designWeight, + initialValue + ); + } else { + throw new HTTPError(500, 'No axle weights for Roadworthiness test certificates!'); + } + } + return weightDetails; + } else { + console.log('No techRecord found for weight details'); + throw new HTTPError(500, 'No vehicle found for Roadworthiness test certificate!'); + } + } + + /** + * Retrieves the odometer history for a given VIN from the Test Results microservice + * @param systemNumber - systemNumber for which to retrieve odometer history + */ + public async getOdometerHistory(systemNumber: string) { + const config: IInvokeConfig = this.config.getInvokeConfig(); + const invokeParams: InvocationRequest = { + FunctionName: config.functions.testResults.name, + InvocationType: 'RequestResponse', + LogType: 'Tail', + Payload: toUint8Array( + JSON.stringify({ + httpMethod: 'GET', + path: `/test-results/${systemNumber}`, + pathParameters: { + systemNumber, + }, + }) + ), + }; + + return this.lambdaClient + .invoke(invokeParams) + .then((response: InvocationResponse) => { + const payload: any = this.lambdaClient.validateInvocationResponse(response); + // TODO: convert to correct type + const testResults: any[] = JSON.parse(payload.body); + + if (!testResults || testResults.length === 0) { + throw new HTTPError(400, `${ERRORS.LAMBDA_INVOCATION_BAD_DATA} ${JSON.stringify(payload)}.`); + } + // Sort results by testEndTimestamp + testResults.sort((first: any, second: any): number => { + if (moment(first.testEndTimestamp).isBefore(second.testEndTimestamp)) { + return 1; + } + + if (moment(first.testEndTimestamp).isAfter(second.testEndTimestamp)) { + return -1; + } + + return 0; + }); + + // Remove the first result as it should be the current one. + testResults.shift(); + + // Set the array to only submitted tests (exclude cancelled) + const submittedTests = testResults.filter((testResult) => { + return testResult.testStatus === 'submitted'; + }); + + const filteredTestResults = submittedTests + .filter(({ testTypes }) => + testTypes?.some( + (testType: ITestType) => + testType.testTypeClassification === 'Annual With Certificate' && + (testType.testResult === 'pass' || testType.testResult === 'prs') + ) + ) + .slice(0, 3); // Only last three entries are used for the history. + + return { + OdometerHistoryList: filteredTestResults.map((testResult) => { + return { + value: testResult.odometerReading, + unit: testResult.odometerReadingUnits, + date: moment(testResult.testEndTimestamp).format('DD.MM.YYYY'), + }; + }), + }; + }) + .catch((error: ServiceException | Error) => { + console.log(error); + throw error; + }); + } + + /** + * Method for getting make and model based on the vehicle from a test-result + * @param testResult - the testResult for which the tech record search is done for + */ + public getVehicleMakeAndModel = async (testResult: any) => { + const searchRes = await this.callSearchTechRecords(testResult.systemNumber); + const techRecord = await this.processGetCurrentProvisionalRecords(searchRes); + // Return bodyMake and bodyModel values for PSVs + return techRecord?.techRecord_vehicleType === VEHICLE_TYPES.PSV + ? { + Make: (techRecord as TechRecordType<'psv'>).techRecord_chassisMake, + Model: (techRecord as TechRecordType<'psv'>).techRecord_chassisModel, + } + : { + Make: (techRecord as TechRecordType<'hgv' | 'trl'>).techRecord_make, + Model: (techRecord as TechRecordType<'hgv' | 'trl'>).techRecord_model, + }; + }; + + /** + * Used to return a subset of technical record information. + * @param searchIdentifier + */ + public callSearchTechRecords = async (searchIdentifier: string) => { + const config: IInvokeConfig = this.config.getInvokeConfig(); + const invokeParams: InvocationRequest = { + FunctionName: config.functions.techRecordsSearch.name, + InvocationType: 'RequestResponse', + LogType: 'Tail', + Payload: toUint8Array( + JSON.stringify({ + httpMethod: 'GET', + path: `/v3/technical-records/search/${searchIdentifier}?searchCriteria=systemNumber`, + pathParameters: { + searchIdentifier, + }, + }) + ), + }; + try { + const lambdaResponse = await this.lambdaClient.invoke(invokeParams); + const res = await this.lambdaClient.validateInvocationResponse(lambdaResponse); + return JSON.parse(res.body); + } catch (e) { + console.log('Error searching technical records'); + console.log(JSON.stringify(e)); + return undefined; + } + }; + + /** + * Used to get a singular whole technical record. + * @param systemNumber + * @param createdTimestamp + */ + public callGetTechRecords = async ( + systemNumber: string, + createdTimestamp: string + ): Promise | undefined> => { + const config: IInvokeConfig = this.config.getInvokeConfig(); + const invokeParams: InvocationRequest = { + FunctionName: config.functions.techRecords.name, + InvocationType: 'RequestResponse', + LogType: 'Tail', + Payload: toUint8Array( + JSON.stringify({ + httpMethod: 'GET', + path: `/v3/technical-records/${systemNumber}/${createdTimestamp}`, + pathParameters: { + systemNumber, + createdTimestamp, + }, + }) + ), + }; + try { + const lambdaResponse = await this.lambdaClient.invoke(invokeParams); + const res = await this.lambdaClient.validateInvocationResponse(lambdaResponse); + return JSON.parse(res.body); + } catch (e) { + console.log('Error in get technical record'); + console.log(JSON.stringify(e)); + return undefined; + } + }; + + /** + * To fetch trailer registration + * @param vin The vin of the trailer + * @param make The make of the trailer + * @returns A payload containing the TRN of the trailer and a boolean. + */ + public async getTrailerRegistrationObject(vin: string, make: string) { + const config: IInvokeConfig = this.config.getInvokeConfig(); + const invokeParams: InvocationRequest = { + FunctionName: config.functions.trailerRegistration.name, + InvocationType: 'RequestResponse', + LogType: 'Tail', + Payload: toUint8Array( + JSON.stringify({ + httpMethod: 'GET', + path: `/v1/trailers/${vin}`, + pathParameters: { + proxy: `/v1/trailers`, + }, + queryStringParameters: { + make, + }, + }) + ), + }; + const response = await this.lambdaClient.invoke(invokeParams); + try { + if (!response.Payload || Buffer.from(response.Payload).toString() === '') { + throw new HTTPError(500, `${ERRORS.LAMBDA_INVOCATION_ERROR} ${response.StatusCode} ${ERRORS.EMPTY_PAYLOAD}`); + } + const payload: any = JSON.parse(Buffer.from(response.Payload).toString()); + if (payload.statusCode === 404) { + console.debug(`vinOrChassisWithMake not found ${vin + make}`); + return { Trn: undefined, IsTrailer: true }; + } + if (payload.statusCode >= 400) { + throw new HTTPError(500, `${ERRORS.LAMBDA_INVOCATION_ERROR} ${payload.statusCode} ${payload.body}`); + } + const trailerRegistration = JSON.parse(payload.body) as ITrailerRegistration; + return { Trn: trailerRegistration.trn, IsTrailer: true }; + } catch (err) { + console.error(`Error on fetching vinOrChassisWithMake ${vin + make}`, err); + throw err; + } + } + + /** + * To check if the testResult is valid for fetching Trn. + * @param vehicleType the vehicle type + * @param makeAndModel object containing Make and Model + * @returns returns if the condition is satisfied else false + */ + public isValidForTrn(vehicleType: string, makeAndModel: IMakeAndModel): boolean { + return makeAndModel && vehicleType === VEHICLE_TYPES.TRL; + } + + /** + * Method used to retrieve the Welsh translations for the certificates + * @returns a list of defects + */ + public async getDefectTranslations(): Promise { + const config: IInvokeConfig = this.config.getInvokeConfig(); + const invokeParams: InvocationRequest = { + FunctionName: config.functions.defects.name, + InvocationType: 'RequestResponse', + LogType: 'Tail', + Payload: toUint8Array( + JSON.stringify({ + httpMethod: 'GET', + path: `/defects/`, + }) + ), + }; + let defects: IDefectParent[] = []; + let retries = 0; + while (retries < 3) { + try { + const response: InvocationResponse = await this.lambdaClient.invoke(invokeParams); + const payload: any = this.lambdaClient.validateInvocationResponse(response); + const defectsParsed = JSON.parse(payload.body); + + if (!defectsParsed || defectsParsed.length === 0) { + throw new HTTPError(400, `${ERRORS.LAMBDA_INVOCATION_BAD_DATA} ${JSON.stringify(payload)}.`); + } + defects = defectsParsed; + return defects; + } catch (error) { + retries++; + console.error(`There was an error retrieving the welsh defect translations on attempt ${retries}: ${error}`); + } + } + return defects; + } + + /** + * Generates an object containing defects for a given test type and certificate type + * @param testTypes - the source test type for defect generation + * @param type - the certificate type + * @param vehicleType - the vehicle type from the test result + * @param flattenedDefects - the list of flattened defects after being retrieved from the defect service + * @param isWelsh - determines whether the atf in which the test result was conducted resides in Wales + */ + private generateDefects( + testTypes: any, + type: string, + vehicleType: string, + flattenedDefects: IFlatDefect[], + isWelsh = false + ) { + const rawDefects: any = testTypes.defects; + const defects: any = { + DangerousDefects: [], + MajorDefects: [], + PRSDefects: [], + MinorDefects: [], + AdvisoryDefects: [], + DangerousDefectsWelsh: [], + MajorDefectsWelsh: [], + PRSDefectsWelsh: [], + MinorDefectsWelsh: [], + AdvisoryDefectsWelsh: [], + }; + + rawDefects.forEach((defect: any) => { + switch (defect.deficiencyCategory.toLowerCase()) { + case 'dangerous': + if ((testTypes.testResult === TEST_RESULTS.PRS || defect.prs) && type === CERTIFICATE_DATA.FAIL_DATA) { + defects.PRSDefects.push(this.formatDefect(defect)); + if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { + defects.PRSDefectsWelsh.push(this.formatDefectWelsh(defect, vehicleType, flattenedDefects)); + } + } else if (testTypes.testResult === 'fail') { + defects.DangerousDefects.push(this.formatDefect(defect)); + // If the test was conducted in Wales and is valid vehicle type, format and add the welsh defects to the list + if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { + defects.DangerousDefectsWelsh.push(this.formatDefectWelsh(defect, vehicleType, flattenedDefects)); + } + } + break; + case 'major': + if ((testTypes.testResult === TEST_RESULTS.PRS || defect.prs) && type === CERTIFICATE_DATA.FAIL_DATA) { + defects.PRSDefects.push(this.formatDefect(defect)); + if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { + defects.PRSDefectsWelsh.push(this.formatDefectWelsh(defect, vehicleType, flattenedDefects)); + } + } else if (testTypes.testResult === 'fail') { + defects.MajorDefects.push(this.formatDefect(defect)); + // If the test was conducted in Wales and is valid vehicle type, format and add the welsh defects to the list + if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { + defects.MajorDefectsWelsh.push(this.formatDefectWelsh(defect, vehicleType, flattenedDefects)); + } + } + break; + case 'minor': + defects.MinorDefects.push(this.formatDefect(defect)); + if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { + defects.MinorDefectsWelsh.push(this.formatDefectWelsh(defect, vehicleType, flattenedDefects)); + } + break; + case 'advisory': + defects.AdvisoryDefects.push(this.formatDefect(defect)); + if (this.isWelshCertificateAvailable(vehicleType, testTypes.testResult) && isWelsh) { + defects.AdvisoryDefectsWelsh.push(this.formatDefect(defect)); + } + break; + } + }); + + Object.entries(defects).forEach(([k, v]: [string, any]) => { + if (v.length === 0) { + Object.assign(defects, { [k]: undefined }); + } + }); + console.log(JSON.stringify(defects)); + return defects; + } + + /** + * Check that the test result and vehicle type are a valid combination and bilingual certificate is available + * @param vehicleType - the vehicle type from the test result + * @param testResult - the result of the test + */ + public isWelshCertificateAvailable = (vehicleType: string, testResult: string): boolean => { + return AVAILABLE_WELSH.CERTIFICATES.includes(`${vehicleType}_${testResult}`); + }; + + /** + * Returns a formatted string containing data about a given defect + * @param defect - defect for which to generate the formatted string + */ + private formatDefect(defect: any) { + const toUpperFirstLetter: any = (word: string) => word.charAt(0).toUpperCase() + word.slice(1); + + let defectString = `${defect.deficiencyRef} ${defect.itemDescription}`; + + if (defect.deficiencyText) { + defectString += ` ${defect.deficiencyText}`; + } + + if (defect.additionalInformation.location) { + Object.keys(defect.additionalInformation.location).forEach((location: string, index: number, array: string[]) => { + if (defect.additionalInformation.location[location]) { + switch (location) { + case 'rowNumber': + defectString += ` Rows: ${defect.additionalInformation.location.rowNumber}.`; + break; + case 'seatNumber': + defectString += ` Seats: ${defect.additionalInformation.location.seatNumber}.`; + break; + case 'axleNumber': + defectString += ` Axles: ${defect.additionalInformation.location.axleNumber}.`; + break; + default: + defectString += ` ${toUpperFirstLetter(defect.additionalInformation.location[location])}`; + break; + } + } + + if (index === array.length - 1) { + defectString += `.`; + } + }); + } + + if (defect.additionalInformation.notes) { + defectString += ` ${defect.additionalInformation.notes}`; + } + + return defectString; + } + + /** + * Returns a formatted welsh string containing data about a given defect + * @param defect - the defect for which to generate the formatted welsh string + * @param vehicleType - the vehicle type from the test result + * @param flattenedDefects - the list of flattened defects + */ + public formatDefectWelsh(defect: any, vehicleType: any, flattenedDefects: IFlatDefect[]) { + const toUpperFirstLetter: any = (word: string) => word.charAt(0).toUpperCase() + word.slice(1); + + const filteredFlatDefects: IFlatDefect[] = flattenedDefects.filter( + (x: IFlatDefect) => defect.deficiencyRef === x.ref + ); + + const filteredFlatDefect: IFlatDefect | null = this.filterFlatDefects(filteredFlatDefects, vehicleType); + + if (filteredFlatDefect !== null) { + let defectString = `${defect.deficiencyRef} ${filteredFlatDefect.itemDescriptionWelsh}`; + + if (defect.deficiencyText) { + defectString += ` ${filteredFlatDefect.deficiencyTextWelsh}`; + } + + if (defect.additionalInformation.location) { + Object.keys(defect.additionalInformation.location).forEach( + (location: string, index: number, array: string[]) => { + if (defect.additionalInformation.location[location]) { + switch (location) { + case 'rowNumber': + defectString += ` ${LOCATION_WELSH.ROW_NUMBER}: ${defect.additionalInformation.location.rowNumber}.`; + break; + case 'seatNumber': + defectString += ` ${LOCATION_WELSH.SEAT_NUMBER}: ${defect.additionalInformation.location.seatNumber}.`; + break; + case 'axleNumber': + defectString += ` ${LOCATION_WELSH.AXLE_NUMBER}: ${defect.additionalInformation.location.axleNumber}.`; + break; + default: + const welshLocation = this.convertLocationWelsh(defect.additionalInformation.location[location]); + defectString += ` ${toUpperFirstLetter(welshLocation)}`; + break; + } + } + + if (index === array.length - 1) { + defectString += `.`; + } + } + ); + } + + if (defect.additionalInformation.notes) { + defectString += ` ${defect.additionalInformation.notes}`; + } + console.log(`Welsh Defect String Generated: ${defectString}`); + return defectString; + } else { + console.log(`ERROR: Unable to find a filtered defect`); + return null; + } + } + + /** + * Returns welsh version of location + * @param locationToTranslate + */ + public convertLocationWelsh(locationToTranslate: string) { + switch (locationToTranslate) { + case LOCATION_ENGLISH.FRONT: + return LOCATION_WELSH.FRONT; + case LOCATION_ENGLISH.REAR: + return LOCATION_WELSH.REAR; + case LOCATION_ENGLISH.UPPER: + return LOCATION_WELSH.UPPER; + case LOCATION_ENGLISH.LOWER: + return LOCATION_WELSH.LOWER; + case LOCATION_ENGLISH.NEARSIDE: + return LOCATION_WELSH.NEARSIDE; + case LOCATION_ENGLISH.OFFSIDE: + return LOCATION_WELSH.OFFSIDE; + case LOCATION_ENGLISH.CENTRE: + return LOCATION_WELSH.CENTRE; + case LOCATION_ENGLISH.INNER: + return LOCATION_WELSH.INNER; + case LOCATION_ENGLISH.OUTER: + return LOCATION_WELSH.OUTER; + default: + return locationToTranslate; + } + } + + /** + * Returns filtered welsh defects + * @param filteredFlatDefects - the array of flattened defects + * @param vehicleType - the vehicle type from the test result + */ + public filterFlatDefects(filteredFlatDefects: IFlatDefect[], vehicleType: string): IFlatDefect | null { + if (filteredFlatDefects.length === 0) { + return null; + } else if (filteredFlatDefects.length === 1) { + return filteredFlatDefects[0]; + } else { + const filteredWelshDefectsOnVehicleType = filteredFlatDefects.filter((flatDefect: IFlatDefect) => + flatDefect.forVehicleType!.includes(vehicleType) + ); + return filteredWelshDefectsOnVehicleType[0]; + } + } + + /** + * Returns a flattened array of every deficiency that only includes the key/value pairs required for certificate generation + * @param defects - the array of defects from the api + */ + public flattenDefectsFromApi(defects: IDefectParent[]): IFlatDefect[] { + const flatDefects: IFlatDefect[] = []; + try { + // go through each defect in un-flattened array + defects.forEach((defect: IDefectParent) => { + const { imNumber, imDescription, imDescriptionWelsh, items } = defect; + if (defect.items !== undefined && defect.items.length !== 0) { + // go through each item of defect + items.forEach((item: IItem) => { + const { itemNumber, itemDescription, itemDescriptionWelsh, deficiencies } = item; + if (item.deficiencies !== undefined && item.deficiencies.length !== 0) { + // go through each deficiency and push to flatDefects array + deficiencies.forEach((deficiency: IDefectChild) => { + const { ref, deficiencyText, deficiencyTextWelsh, forVehicleType } = deficiency; + const lowLevelDeficiency: IFlatDefect = { + imNumber, + imDescription, + imDescriptionWelsh, + itemNumber, + itemDescription, + itemDescriptionWelsh, + ref, + deficiencyText, + deficiencyTextWelsh, + forVehicleType, + }; + flatDefects.push(lowLevelDeficiency); + }); + } + }); + } + }); + } catch (e) { + console.error(`Error flattening defects: ${e}`); + } + return flatDefects; + } + + /** + * Returns true if testType is adr and false if not + * @param testType - testType which is tested + */ + public isTestTypeAdr(testType: any): boolean { + return ADR_TEST.IDS.includes(testType.testTypeId); + } + + /** + * Returns a boolean value indicating whether the test type is a basic IVA test + * @param testTypeId - the test type ID on the test result + */ + public isBasicIvaTest = (testTypeId: string): boolean => { + return BASIC_IVA_TEST.IDS.includes(testTypeId); + }; + + /** + * Returns true if testType is iva and false if not + * @param testTypeId - test type id which is being tested + */ + public isIvaTest(testTypeId: string): boolean { + return IVA30_TEST.IDS.includes(testTypeId); + } + + /** + * Returns true if testType is msva and false if not + * @param testTypeId - test type id which is being tested + */ + public isMsvaTest(testTypeId: string): boolean { + return MSVA30_TEST.IDS.includes(testTypeId); + } + + //#region Private Static Functions + + /** + * Returns true if testType is roadworthiness test for HGV or TRL and false if not + * @param testTypeId - testType which is tested + */ + private static isRoadworthinessTestType(testTypeId: string): boolean { + return HGV_TRL_ROADWORTHINESS_TEST_TYPES.IDS.includes(testTypeId); + } + + /** + * Returns true if provided testResult is HGV or TRL Roadworthiness test otherwise false + * @param testResult - testResult of the vehicle + */ + private static isHgvTrlRoadworthinessCertificate(testResult: any): boolean { + return ( + (testResult.vehicleType === VEHICLE_TYPES.HGV || testResult.vehicleType === VEHICLE_TYPES.TRL) && + CertificateGenerationService.isRoadworthinessTestType(testResult.testTypes.testTypeId) + ); + } + + /** + * Sorts required standards if present by refCalculation and then returns it + * @param requiredStandards - the requiredStandards array to sort + * @returns - the sorted requiredStandards array + */ + private sortRequiredStandards = ( + requiredStandards: IRequiredStandard[] | undefined + ): IRequiredStandard[] | undefined => { + if (!requiredStandards) { + return; + } + + const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }); + return requiredStandards.sort((a, b) => collator.compare(a.refCalculation, b.refCalculation)); + }; + //#endregion } export { CertificateGenerationService, IGeneratedCertificateResponse }; - diff --git a/src/services/CertificateUploadService.ts b/src/services/CertificateUploadService.ts index f6d806fb..44db7951 100644 --- a/src/services/CertificateUploadService.ts +++ b/src/services/CertificateUploadService.ts @@ -1,66 +1,64 @@ -import { PutObjectCommandOutput } from "@aws-sdk/client-s3"; -import { IGeneratedCertificateResponse } from "../models"; -import { Service } from "../models/injector/ServiceDecorator"; -import { S3BucketService } from "./S3BucketService"; +import { PutObjectCommandOutput } from '@aws-sdk/client-s3'; +import { IGeneratedCertificateResponse } from '../models'; +import { Service } from '../models/injector/ServiceDecorator'; +import { S3BucketService } from './S3BucketService'; /** * Service class for uploading certificates to S3 */ @Service() class CertificateUploadService { - private readonly s3BucketService: S3BucketService; + private readonly s3BucketService: S3BucketService; - constructor(s3BucketService: S3BucketService) { - this.s3BucketService = s3BucketService; - } + constructor(s3BucketService: S3BucketService) { + this.s3BucketService = s3BucketService; + } - /** - * Uploads a generated certificate to S3 bucket - * @param payload - */ - public uploadCertificate( - payload: IGeneratedCertificateResponse - ): Promise { - let shouldEmailCertificate = payload.shouldEmailCertificate; + /** + * Uploads a generated certificate to S3 bucket + * @param payload + */ + public uploadCertificate(payload: IGeneratedCertificateResponse): Promise { + let shouldEmailCertificate = payload.shouldEmailCertificate; - if (shouldEmailCertificate !== "false") { - shouldEmailCertificate = "true"; - } + if (shouldEmailCertificate !== 'false') { + shouldEmailCertificate = 'true'; + } - const metadata: Record = { - "vrm": payload.vrm, - "test-type-name": payload.testTypeName, - "test-type-result": payload.testTypeResult, - "date-of-issue": payload.dateOfIssue, - "cert-type": payload.certificateType, - "file-format": payload.fileFormat, - "file-size": payload.fileSize, - "cert-index": payload.certificateOrder.current.toString(), - "total-certs": payload.certificateOrder.total.toString(), - "email": payload.email, - "should-email-certificate": shouldEmailCertificate, - }; + const metadata: Record = { + vrm: payload.vrm, + 'test-type-name': payload.testTypeName, + 'test-type-result': payload.testTypeResult, + 'date-of-issue': payload.dateOfIssue, + 'cert-type': payload.certificateType, + 'file-format': payload.fileFormat, + 'file-size': payload.fileSize, + 'cert-index': payload.certificateOrder.current.toString(), + 'total-certs': payload.certificateOrder.total.toString(), + email: payload.email, + 'should-email-certificate': shouldEmailCertificate, + }; - console.log(`metadata in s3 upload: ${JSON.stringify(metadata)}`); + console.log(`metadata in s3 upload: ${JSON.stringify(metadata)}`); - return this.s3BucketService.upload( - `cvs-cert-${process.env.BUCKET}`, - payload.fileName, - payload.certificate, - metadata - ); - } + return this.s3BucketService.upload( + `cvs-cert-${process.env.BUCKET}`, + payload.fileName, + payload.certificate, + metadata + ); + } - /** - * Deletes a generated certificate to S3 bucket - * @param testResult - */ - public removeCertificate(testResult: any) { - return this.s3BucketService.delete( - `cvs-cert-${process.env.BUCKET}`, - `${testResult.testTypes.testNumber}_${testResult.vin}.pdf` - ); - } + /** + * Deletes a generated certificate to S3 bucket + * @param testResult + */ + public removeCertificate(testResult: any) { + return this.s3BucketService.delete( + `cvs-cert-${process.env.BUCKET}`, + `${testResult.testTypes.testNumber}_${testResult.vin}.pdf` + ); + } } export { CertificateUploadService }; diff --git a/src/services/LambdaService.ts b/src/services/LambdaService.ts index bb6d740d..df232c69 100644 --- a/src/services/LambdaService.ts +++ b/src/services/LambdaService.ts @@ -1,74 +1,61 @@ -import { IInvokeConfig } from "../models"; -import { Configuration } from "../utils/Configuration"; -import { InvocationRequest, InvocationResponse, LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; -import { Service } from "../models/injector/ServiceDecorator"; -import { HTTPError } from "../models/HTTPError"; -import { ERRORS } from "../models/Enums"; +import { InvocationRequest, InvocationResponse, InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; +import { IInvokeConfig } from '../models'; +import { ERRORS } from '../models/Enums'; +import { HTTPError } from '../models/HTTPError'; +import { Service } from '../models/injector/ServiceDecorator'; +import { Configuration } from '../utils/Configuration'; -import AWSXRay from "aws-xray-sdk"; +import AWSXRay from 'aws-xray-sdk'; /** * Service class for invoking external lambda functions */ @Service() class LambdaService { - public readonly lambdaClient: LambdaClient; - - constructor(lambdaClient: LambdaClient) { - const config: IInvokeConfig = Configuration.getInstance().getInvokeConfig(); - this.lambdaClient = AWSXRay.captureAWSv3Client(new LambdaClient({ ...lambdaClient, ...config.params })); - } - - /** - * Invokes a lambda function based on the given parameters - * @param params - InvocationRequest params - */ - public async invoke( - params: InvocationRequest - ): Promise { - try { - return await this.lambdaClient.send(new InvokeCommand(params)); - } catch (err) { - throw err; - } - } - - /** - * Validates the invocation response - * @param response - the invocation response - */ - public validateInvocationResponse( - response: InvocationResponse - ): Promise { - if ( - !response.Payload || - Buffer.from(response.Payload).toString() === "" || - (response.StatusCode && response.StatusCode >= 400) - ) { - throw new HTTPError( - 500, - `${ERRORS.LAMBDA_INVOCATION_ERROR} ${response.StatusCode} ${ERRORS.EMPTY_PAYLOAD}` - ); - } - - const payload: any = JSON.parse(Buffer.from(response.Payload).toString()); - - if (payload.statusCode >= 400) { - throw new HTTPError( - 500, - `${ERRORS.LAMBDA_INVOCATION_ERROR} ${payload.statusCode} ${payload.body}` - ); - } - - if (!payload.body) { - throw new HTTPError( - 400, - `${ERRORS.LAMBDA_INVOCATION_BAD_DATA} ${JSON.stringify(payload)}.` - ); - } - - return payload; - } + public readonly lambdaClient: LambdaClient; + + constructor(lambdaClient: LambdaClient) { + const config: IInvokeConfig = Configuration.getInstance().getInvokeConfig(); + this.lambdaClient = AWSXRay.captureAWSv3Client(new LambdaClient({ ...lambdaClient, ...config.params })); + } + + /** + * Invokes a lambda function based on the given parameters + * @param params - InvocationRequest params + */ + public async invoke(params: InvocationRequest): Promise { + try { + return await this.lambdaClient.send(new InvokeCommand(params)); + } catch (err) { + throw err; + } + } + + /** + * Validates the invocation response + * @param response - the invocation response + */ + public validateInvocationResponse(response: InvocationResponse): Promise { + if ( + !response.Payload || + Buffer.from(response.Payload).toString() === '' || + (response.StatusCode && response.StatusCode >= 400) + ) { + throw new HTTPError(500, `${ERRORS.LAMBDA_INVOCATION_ERROR} ${response.StatusCode} ${ERRORS.EMPTY_PAYLOAD}`); + } + + const payload: any = JSON.parse(Buffer.from(response.Payload).toString()); + + if (payload.statusCode >= 400) { + throw new HTTPError(500, `${ERRORS.LAMBDA_INVOCATION_ERROR} ${payload.statusCode} ${payload.body}`); + } + + if (!payload.body) { + throw new HTTPError(400, `${ERRORS.LAMBDA_INVOCATION_BAD_DATA} ${JSON.stringify(payload)}.`); + } + + return payload; + } } export { LambdaService }; diff --git a/src/services/S3BucketService.ts b/src/services/S3BucketService.ts index 0e7592f3..4a224c82 100644 --- a/src/services/S3BucketService.ts +++ b/src/services/S3BucketService.ts @@ -1,90 +1,92 @@ -import {Service} from "../models/injector/ServiceDecorator"; -import {Readable} from "stream"; -import {Configuration} from "../utils/Configuration"; -import {IS3Config} from "../models"; -import AWSXRay from "aws-xray-sdk"; -import {DeleteObjectCommand, DeleteObjectCommandOutput, GetObjectCommand, GetObjectCommandOutput, PutObjectCommand, PutObjectCommandOutput, S3Client} from "@aws-sdk/client-s3"; +import { Readable } from 'stream'; +import { + DeleteObjectCommand, + DeleteObjectCommandOutput, + GetObjectCommand, + GetObjectCommandOutput, + PutObjectCommand, + PutObjectCommandOutput, + S3Client, +} from '@aws-sdk/client-s3'; +import AWSXRay from 'aws-xray-sdk'; +import { IS3Config } from '../models'; +import { Service } from '../models/injector/ServiceDecorator'; +import { Configuration } from '../utils/Configuration'; /** * Service class for communicating with Simple Storage Service */ @Service() class S3BucketService { - public readonly s3Client: S3Client; + public readonly s3Client: S3Client; - constructor(s3Client: S3Client) { - const config: IS3Config = Configuration.getInstance().getS3Config(); - this.s3Client = AWSXRay.captureAWSv3Client(new S3Client({ ...s3Client, ...config })); - } + constructor(s3Client: S3Client) { + const config: IS3Config = Configuration.getInstance().getS3Config(); + this.s3Client = AWSXRay.captureAWSv3Client(new S3Client({ ...s3Client, ...config })); + } - /** - * Uploads a file to an S3 bucket - * @param bucketName - the bucket to upload to - * @param fileName - the name of the file - * @param content - contents of the file - * @param metadata - optional metadata - */ - public async upload( - bucketName: string, - fileName: string, - content: Buffer | Uint8Array | Blob | string | Readable, - metadata?: Record - ): Promise { - const command = new PutObjectCommand({ - Bucket: bucketName, - Key: `${process.env.BRANCH}/${fileName}`, - Body: content, - Metadata: metadata, - }); + /** + * Uploads a file to an S3 bucket + * @param bucketName - the bucket to upload to + * @param fileName - the name of the file + * @param content - contents of the file + * @param metadata - optional metadata + */ + public async upload( + bucketName: string, + fileName: string, + content: Buffer | Uint8Array | Blob | string | Readable, + metadata?: Record + ): Promise { + const command = new PutObjectCommand({ + Bucket: bucketName, + Key: `${process.env.BRANCH}/${fileName}`, + Body: content, + Metadata: metadata, + }); - try { - return await this.s3Client.send(command); - } catch (err) { - throw err; - } - } + try { + return await this.s3Client.send(command); + } catch (err) { + throw err; + } + } - /** - * Downloads a file from an S3 bucket - * @param bucketName - the bucket from which to download - * @param fileName - the name of the file - */ - public async download( - bucketName: string, - fileName: string - ): Promise { - const command = new GetObjectCommand({ - Bucket: bucketName, - Key: `${process.env.BRANCH}/${fileName}`, - }); + /** + * Downloads a file from an S3 bucket + * @param bucketName - the bucket from which to download + * @param fileName - the name of the file + */ + public async download(bucketName: string, fileName: string): Promise { + const command = new GetObjectCommand({ + Bucket: bucketName, + Key: `${process.env.BRANCH}/${fileName}`, + }); - try { - return await this.s3Client.send(command); - } catch (err) { - throw err; - } - } + try { + return await this.s3Client.send(command); + } catch (err) { + throw err; + } + } - /** - * Deletes a file from an S3 bucket - * @param bucketName - the bucket from which to download - * @param fileName - the name of the file - */ - public delete( - bucketName: string, - fileName: string - ): Promise { - const command = new DeleteObjectCommand({ - Bucket: bucketName, - Key: `${process.env.BRANCH}/${fileName}`, - }); + /** + * Deletes a file from an S3 bucket + * @param bucketName - the bucket from which to download + * @param fileName - the name of the file + */ + public delete(bucketName: string, fileName: string): Promise { + const command = new DeleteObjectCommand({ + Bucket: bucketName, + Key: `${process.env.BRANCH}/${fileName}`, + }); - try { - return this.s3Client.send(command); - } catch (err) { - throw err; - } - } + try { + return this.s3Client.send(command); + } catch (err) { + throw err; + } + } } export { S3BucketService }; diff --git a/src/utils/Configuration.ts b/src/utils/Configuration.ts index f49e2e4c..236e4b25 100644 --- a/src/utils/Configuration.ts +++ b/src/utils/Configuration.ts @@ -1,109 +1,99 @@ // @ts-ignore -import * as yml from "node-yaml"; -import { IInvokeConfig, IMOTConfig, IS3Config } from "../models"; +import * as yml from 'node-yaml'; +import { IInvokeConfig, IMOTConfig, IS3Config } from '../models'; /** * Configuration class for retrieving project config */ class Configuration { - private static instance: Configuration; - private readonly config: any; - - private constructor(configPath: string) { - const config = yml.readSync(configPath); - - // Replace environment variable references - let stringifiedConfig: string = JSON.stringify(config); - const envRegex: RegExp = /\${(\w+\b):?(\w+\b)?}/g; - const matches: RegExpMatchArray | null = stringifiedConfig.match(envRegex); - - if (matches) { - matches.forEach((match: string) => { - envRegex.lastIndex = 0; - const captureGroups: RegExpExecArray = envRegex.exec( - match - ) as RegExpExecArray; - - // Insert the environment variable if available. If not, insert placeholder. If no placeholder, leave it as is. - stringifiedConfig = stringifiedConfig.replace( - match, - process.env[captureGroups[1]] || captureGroups[2] || captureGroups[1] - ); - }); - } - - this.config = JSON.parse(stringifiedConfig); - } - - /** - * Retrieves the singleton instance of Configuration - * @returns Configuration - */ - public static getInstance(): Configuration { - if (!this.instance) { - this.instance = new Configuration("../config/config.yml"); - } - - return Configuration.instance; - } - - /** - * Retrieves the entire config as an object - * @returns any - */ - public getConfig(): any { - return this.config; - } - - /** - * Retrieves the Lambda Invoke config - * @returns IInvokeConfig - */ - public getInvokeConfig(): IInvokeConfig { - if (!this.config.invoke) { - throw new Error( - "Lambda Invoke config is not defined in the config file." - ); - } - - // Not defining BRANCH will default to local - const env: string = - !process.env.BRANCH || process.env.BRANCH === "local" - ? "local" - : "remote"; - - return this.config.invoke[env]; - } - - /** - * Retrieves the S3 config - * @returns IS3Config - */ - public getS3Config(): IS3Config { - if (!this.config.s3) { - throw new Error("DynamoDB config is not defined in the config file."); - } - - // Not defining BRANCH will default to local - const env: string = - !process.env.BRANCH || process.env.BRANCH === "local" - ? "local" - : "remote"; - - return this.config.s3[env]; - } - - /** - * Retrieves the MOT config - * @returns IMOTConfig - */ - public getMOTConfig(): IMOTConfig { - if (!this.config.mot) { - throw new Error("The MOT config is not defined in the config file."); - } - - return this.config.mot; - } + private static instance: Configuration; + private readonly config: any; + + private constructor(configPath: string) { + const config = yml.readSync(configPath); + + // Replace environment variable references + let stringifiedConfig: string = JSON.stringify(config); + const envRegex: RegExp = /\${(\w+\b):?(\w+\b)?}/g; + const matches: RegExpMatchArray | null = stringifiedConfig.match(envRegex); + + if (matches) { + matches.forEach((match: string) => { + envRegex.lastIndex = 0; + const captureGroups: RegExpExecArray = envRegex.exec(match) as RegExpExecArray; + + // Insert the environment variable if available. If not, insert placeholder. If no placeholder, leave it as is. + stringifiedConfig = stringifiedConfig.replace( + match, + process.env[captureGroups[1]] || captureGroups[2] || captureGroups[1] + ); + }); + } + + this.config = JSON.parse(stringifiedConfig); + } + + /** + * Retrieves the singleton instance of Configuration + * @returns Configuration + */ + public static getInstance(): Configuration { + if (!this.instance) { + this.instance = new Configuration('../config/config.yml'); + } + + return Configuration.instance; + } + + /** + * Retrieves the entire config as an object + * @returns any + */ + public getConfig(): any { + return this.config; + } + + /** + * Retrieves the Lambda Invoke config + * @returns IInvokeConfig + */ + public getInvokeConfig(): IInvokeConfig { + if (!this.config.invoke) { + throw new Error('Lambda Invoke config is not defined in the config file.'); + } + + // Not defining BRANCH will default to local + const env: string = !process.env.BRANCH || process.env.BRANCH === 'local' ? 'local' : 'remote'; + + return this.config.invoke[env]; + } + + /** + * Retrieves the S3 config + * @returns IS3Config + */ + public getS3Config(): IS3Config { + if (!this.config.s3) { + throw new Error('DynamoDB config is not defined in the config file.'); + } + + // Not defining BRANCH will default to local + const env: string = !process.env.BRANCH || process.env.BRANCH === 'local' ? 'local' : 'remote'; + + return this.config.s3[env]; + } + + /** + * Retrieves the MOT config + * @returns IMOTConfig + */ + public getMOTConfig(): IMOTConfig { + if (!this.config.mot) { + throw new Error('The MOT config is not defined in the config file.'); + } + + return this.config.mot; + } } export { Configuration };