From a202703db542cd11a531c3a5d53803eaf0a2c822 Mon Sep 17 00:00:00 2001 From: Nathan Brown Date: Wed, 4 Oct 2023 10:12:25 +0100 Subject: [PATCH] feat(cb2-9114): create amend vin end point (#71) --- src/handler/update.ts | 12 ++- src/handler/updateVin.ts | 74 +++++++++++++ src/models/updateVin.ts | 3 + src/processors/processPostRequest.ts | 4 +- src/processors/processUpdateRequest.ts | 7 +- src/util/enum.ts | 2 + src/util/partialVin.ts | 6 ++ src/validators/update.ts | 4 +- src/validators/updateVin.ts | 37 +++++++ src/validators/vinValidity.ts | 6 +- template.yml | 13 +++ .../updateVin/updateVin.int.test.ts | 101 ++++++++++++++++++ tests/unit/handler/update.unit.test.ts | 5 +- tests/unit/handler/updateVin.unit.test.ts | 91 ++++++++++++++++ tests/unit/validators/update.unit.test.ts | 6 +- .../unit/validators/vinValidity.unit.test.ts | 9 +- webpack/webpack.production.js | 2 +- 17 files changed, 356 insertions(+), 26 deletions(-) create mode 100644 src/handler/updateVin.ts create mode 100644 src/models/updateVin.ts create mode 100644 src/util/partialVin.ts create mode 100644 src/validators/updateVin.ts create mode 100644 tests/integration/updateVin/updateVin.int.test.ts create mode 100644 tests/unit/handler/updateVin.unit.test.ts diff --git a/src/handler/update.ts b/src/handler/update.ts index 980a4fd1..26f71720 100644 --- a/src/handler/update.ts +++ b/src/handler/update.ts @@ -13,7 +13,6 @@ import logger from '../util/logger'; import { validateAgainstSkeletonSchema } from '../validators/post'; import { validateSysNumTimestampPathParams } from '../validators/sysNumTimestamp'; import { checkStatusCodeValidity, validateUpdateErrors } from '../validators/update'; -import { checkVinValidity } from '../validators/vinValidity'; export const handler = async (event: APIGatewayProxyEvent): Promise => { logger.info('Update end point called'); @@ -54,9 +53,12 @@ export const handler = async (event: APIGatewayProxyEvent): Promise => { + try { + logger.debug('Amend VIN Called'); + + const amendVinPayloadErrors = validateAmendVinPayloadErrors(event); + + if (amendVinPayloadErrors) return addHttpHeaders(amendVinPayloadErrors); + + const userDetails = getUserDetails(event.headers.Authorization ?? ''); + const systemNumber = decodeURIComponent(event.pathParameters?.systemNumber ?? ''); + const createdTimestamp = decodeURIComponent(event.pathParameters?.createdTimestamp ?? ''); + const body = await JSON.parse(event.body ?? '') as UpdateVinBody; + + const recordFromDB = await getBySystemNumberAndCreatedTimestamp(systemNumber, createdTimestamp); + + const statusCodeErrors = checkStatusCodeValidity(recordFromDB.techRecord_statusCode); + if (statusCodeErrors) return addHttpHeaders(statusCodeErrors); + + const vinValidityErrors = checkVinValidity(recordFromDB.vin, body.newVin); + if (vinValidityErrors) return addHttpHeaders(vinValidityErrors); + + const partialVin = createPartialVin(body.newVin); + + const updatedRecord = { + ...recordFromDB, vin: body.newVin.toUpperCase(), partialVin, techRecord_reasonForCreation: 'VIN updated.', + }; + + const date = new Date().toISOString(); + const updatedNewRecord = setCreatedAuditDetails( + updatedRecord, + userDetails.username, + userDetails.msOid, + date, + updatedRecord.techRecord_statusCode as StatusCode, + ); + + const updatedRecordFromDB = setLastUpdatedAuditDetails( + recordFromDB, + userDetails.username, + userDetails.msOid, + date, + StatusCode.ARCHIVED, + ); + + await updateVehicle([updatedRecordFromDB], [updatedNewRecord]); + logger.debug(JSON.stringify(updatedNewRecord)); + + return addHttpHeaders({ + statusCode: 200, + body: JSON.stringify(updatedNewRecord), + }); + } catch (err) { + return addHttpHeaders({ + statusCode: 500, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + body: formatErrorMessage(ERRORS.FAILED_UPDATE_MESSAGE), + }); + } +}; diff --git a/src/models/updateVin.ts b/src/models/updateVin.ts new file mode 100644 index 00000000..3a1362f2 --- /dev/null +++ b/src/models/updateVin.ts @@ -0,0 +1,3 @@ +export type UpdateVinBody = { + newVin: string; +}; diff --git a/src/processors/processPostRequest.ts b/src/processors/processPostRequest.ts index 51910d64..7069ab94 100644 --- a/src/processors/processPostRequest.ts +++ b/src/processors/processPostRequest.ts @@ -6,6 +6,7 @@ import { addVehicleClassCode } from '../services/vehicleClass'; import { ERRORS, HttpMethod, StatusCode } from '../util/enum'; import { flattenArrays } from '../util/formatTechRecord'; import logger from '../util/logger'; +import { createPartialVin } from '../util/partialVin'; import { isObjectEmpty } from '../validators/emptyObject'; import { validateAndComputeRecordCompleteness } from '../validators/recordCompleteness'; @@ -40,8 +41,7 @@ export const processPostRequest = async (input: TechRecordType<'put'>, userDetai requestAsGet.techRecord_statusCode as StatusCode, ); updatedNewRecord.systemNumber = systemNumber; - updatedNewRecord.partialVin = updatedNewRecord.vin.length < 6 - ? updatedNewRecord.vin : updatedNewRecord.vin.substring(updatedNewRecord.vin.length - 6); + updatedNewRecord.partialVin = createPartialVin(updatedNewRecord.vin); addVehicleClassCode(updatedNewRecord); logger.info('Successfully Processed Record'); return updatedNewRecord; diff --git a/src/processors/processUpdateRequest.ts b/src/processors/processUpdateRequest.ts index 7bd9e3bd..73ecc37a 100644 --- a/src/processors/processUpdateRequest.ts +++ b/src/processors/processUpdateRequest.ts @@ -4,6 +4,7 @@ import { UserDetails } from '../services/user'; import { addVehicleClassCode } from '../services/vehicleClass'; import { HttpMethod, StatusCode, UpdateType } from '../util/enum'; import { flattenArrays } from '../util/formatTechRecord'; +import { createPartialVin } from '../util/partialVin'; import { validateAndComputeRecordCompleteness } from '../validators/recordCompleteness'; export const processUpdateRequest = ( @@ -59,9 +60,5 @@ export const addVehicleIdentifiers = (recordFromDB: TechRecordType<'get'>, techR const newVin = techRecord.vin ?? recordFromDB.vin; techRecord.vin = newVin.toUpperCase(); - if (newVin.length < 6) { - (techRecord as TechRecordType<'get'>).partialVin = newVin.toUpperCase(); - } else { - (techRecord as TechRecordType<'get'>).partialVin = newVin.substring(Math.max(newVin.length - 6)).toUpperCase(); - } + (techRecord as TechRecordType<'get'>).partialVin = createPartialVin(newVin); }; diff --git a/src/util/enum.ts b/src/util/enum.ts index 5605459d..d61aa27e 100644 --- a/src/util/enum.ts +++ b/src/util/enum.ts @@ -33,6 +33,7 @@ export enum ERRORS { MISSING_PAYLOAD = 'Missing payload!', MISSING_USER_DETAILS = 'Missing user details', MISSING_REASON_FOR_ARCHIVING = 'Reason for archiving not provided', + MISSING_REASON_FOR_UPDATING = 'Reason for updating not provided', VEHICLE_TYPE_ERROR = '"vehicleType" must be one of [hgv, psv, trl, lgv, car, motorcycle]', MISSING_AUTH_HEADER = 'Missing authorization header', VIN_ERROR = 'New VIN is invalid', @@ -40,6 +41,7 @@ export enum ERRORS { INVALID_TRAILER_ID_UPDATE = 'Cannot use update API to update the trailer ID', MORE_THAN_TWO_NON_ARCHIVED_TECH_RECORDS = 'The vehicle has more than two non archived Tech records.', CANNOT_FIND_RECORD = 'Cannot find record', + FAILED_UPDATE_MESSAGE = 'Failed to update record', } export enum ReasonForCreation { EU_VEHICLE_CATEGORY_UPDATE = 'EU Vehicle Catergory updated.', diff --git a/src/util/partialVin.ts b/src/util/partialVin.ts new file mode 100644 index 00000000..06bbe32c --- /dev/null +++ b/src/util/partialVin.ts @@ -0,0 +1,6 @@ +export const createPartialVin = (vin: string): string => { + if (vin.length < 6) { + return vin.toUpperCase(); + } + return vin.substring(Math.max(vin.length - 6)).toUpperCase(); +}; diff --git a/src/validators/update.ts b/src/validators/update.ts index b9cd7ee4..d88bb65b 100644 --- a/src/validators/update.ts +++ b/src/validators/update.ts @@ -26,13 +26,13 @@ export const checkStatusCodeValidity = (oldStatus: string | undefined | null, ne if (oldStatus === StatusCode.ARCHIVED) { return { statusCode: 400, - body: ERRORS.CANNOT_UPDATE_ARCHIVED_RECORD, + body: formatErrorMessage(ERRORS.CANNOT_UPDATE_ARCHIVED_RECORD), }; } if (newStatus === StatusCode.ARCHIVED) { return { statusCode: 400, - body: ERRORS.CANNOT_USE_UPDATE_TO_ARCHIVE, + body: formatErrorMessage(ERRORS.CANNOT_USE_UPDATE_TO_ARCHIVE), }; } return false; diff --git a/src/validators/updateVin.ts b/src/validators/updateVin.ts new file mode 100644 index 00000000..278521cd --- /dev/null +++ b/src/validators/updateVin.ts @@ -0,0 +1,37 @@ +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { UpdateVinBody } from '../models/updateVin'; +import { ERRORS } from '../util/enum'; +import { formatErrorMessage } from '../util/errorMessage'; +import { validateSysNumTimestampPathParams } from './sysNumTimestamp'; + +export const validateAmendVinPayloadErrors = (event: APIGatewayProxyEvent) => { + const pathParametersErrors = validateSysNumTimestampPathParams(event); + + if (pathParametersErrors) { + return pathParametersErrors; + } + + if (!event.body || !Object.keys(event.body).length) { + return { + statusCode: 400, + body: formatErrorMessage(ERRORS.MISSING_PAYLOAD), + }; + } + if (!event.headers.Authorization) { + return { + statusCode: 400, + body: formatErrorMessage(ERRORS.MISSING_AUTH_HEADER), + }; + } + + const body = JSON.parse(event.body) as UpdateVinBody; + + if (!body.newVin) { + return { + statusCode: 400, + body: formatErrorMessage('No new VIN provided'), + }; + } + + return undefined; +}; diff --git a/src/validators/vinValidity.ts b/src/validators/vinValidity.ts index d6539cae..341fa99d 100644 --- a/src/validators/vinValidity.ts +++ b/src/validators/vinValidity.ts @@ -1,4 +1,5 @@ import { ERRORS } from '../util/enum'; +import { formatErrorMessage } from '../util/errorMessage'; export const checkVinValidity = (currentVin: string, newVin: (string | undefined | null)) => { if ((newVin !== undefined && newVin !== null) && newVin !== currentVin) { @@ -6,10 +7,13 @@ export const checkVinValidity = (currentVin: string, newVin: (string | undefined || newVin.length > 21 || typeof newVin !== 'string' || !(/^[0-9a-z]+$/i).test(newVin) + || newVin.toUpperCase().includes('O') + || newVin.toUpperCase().includes('I') + || newVin.toUpperCase().includes('Q') ) { return ({ statusCode: 400, - body: ERRORS.VIN_ERROR, + body: formatErrorMessage(ERRORS.VIN_ERROR), }); } } diff --git a/template.yml b/template.yml index e875d3bd..49dde412 100644 --- a/template.yml +++ b/template.yml @@ -101,6 +101,19 @@ Resources: Properties: Path: /v3/technical-records/updateVrm/{systemNumber}/{createdTimestamp} Method: patch + UpdateVinFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: src/handler/ + Handler: updateVin.handler + Runtime: nodejs18.x + Timeout: 20 + Events: + PatchLambda: + Type: Api + Properties: + Path: /v3/technical-records/updateVin/{systemNumber}/{createdTimestamp} + Method: patch GeneratePlateFunction: Type: 'AWS::Serverless::Function' Properties: diff --git a/tests/integration/updateVin/updateVin.int.test.ts b/tests/integration/updateVin/updateVin.int.test.ts new file mode 100644 index 00000000..d8742949 --- /dev/null +++ b/tests/integration/updateVin/updateVin.int.test.ts @@ -0,0 +1,101 @@ +import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-verb'; +import { seedTables } from '../../../scripts/setup-local-tables'; +import { tableName } from '../../../src/config'; +import techRecordData from '../../resources/technical-records-v3.json'; +import { mockToken } from '../../unit/util/mockToken'; + +describe('updateVin', () => { + beforeEach(async () => { + await seedTables([{ + table: tableName, + data: techRecordData, + }]); + }); + describe('happy path', () => { + it('should update a vin and archive the old record', async () => { + jest.setTimeout(20000); + const systemNumber = '11000162'; + const createdTimestamp = '2023-09-13T13:06:51.221Z'; + + const response = await fetch( + `http:/127.0.0.1:3000/v3/technical-records/updateVin/${systemNumber}/${createdTimestamp}`, + { + method: 'PATCH', + body: JSON.stringify({ newVin: '123456789' }), + headers: { + Authorization: mockToken, + }, + }, + ); + + const json = await response.json() as TechRecordType<'get'>; + + expect(json.vin).toBe('123456789'); + expect(json.techRecord_statusCode).toBe('provisional'); + + const checkOldRecord = await fetch( + `http:/127.0.0.1:3000/v3/technical-records/${systemNumber}/${createdTimestamp}`, + { + method: 'GET', + headers: { + Authorization: mockToken, + }, + }, + ); + + const jsonOldRecord = await checkOldRecord.json() as TechRecordType<'get'>; + + console.log(jsonOldRecord); + + expect(jsonOldRecord.vin).not.toBe('123456789'); + expect(jsonOldRecord.techRecord_statusCode).toBe('archived'); + }); + }); + + describe('unhappy path', () => { + it('should error if the record is already archived', async () => { + const systemNumber = '8AJWFM00066'; + const createdTimestamp = '2019-06-15T10:36:12.903Z'; + + const response = await fetch( + `http:/127.0.0.1:3000/v3/technical-records/updateVin/${systemNumber}/${createdTimestamp}`, + { + method: 'PATCH', + body: JSON.stringify({ newVin: '123456789' }), + headers: { + Authorization: mockToken, + }, + }, + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const json = await response.json(); + + expect(json).toEqual({ errors: ['Cannot update an archived record'] }); + expect(response.status).toBe(400); + }); + }); + + it('should error if the VIN is invalid', async () => { + const systemNumber = '11100136'; + const createdTimestamp = '2023-09-20T15:56:43.608Z'; + + const response = await fetch( + `http:/127.0.0.1:3000/v3/technical-records/updateVin/${systemNumber}/${createdTimestamp}`, + { + method: 'PATCH', + body: JSON.stringify({ newVin: 'OIQ123' }), + headers: { + Authorization: mockToken, + }, + }, + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const json = await response.json(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(json).toEqual({ errors: ['New VIN is invalid'] }); + expect(response.status).toBe(400); + }); +}); diff --git a/tests/unit/handler/update.unit.test.ts b/tests/unit/handler/update.unit.test.ts index 98058824..3fe63654 100644 --- a/tests/unit/handler/update.unit.test.ts +++ b/tests/unit/handler/update.unit.test.ts @@ -21,7 +21,6 @@ const trlPayload = { techRecord_vehicleConfiguration: 'rigid', techRecord_vehicleType: 'trl', trailerId: 'C530005', - vin: '9080977997', techRecord_bodyType_description: 'artic', techRecord_bodyType_code: 'a', }; @@ -110,8 +109,8 @@ describe('update handler', () => { vin: 'testVin', }); const result = await handler(request as unknown as APIGatewayProxyEvent); - expect(result.statusCode).toBe(400); - expect(result.body).toEqual(ERRORS.VIN_ERROR); + expect(result.statusCode).toBe(500); + expect(result.body).toBe('{"errors":["Cannot update VIN with patch end point."]}'); }); it('should throw error if transaction fails', async () => { process.env.AWS_SAM_LOCAL = 'true'; diff --git a/tests/unit/handler/updateVin.unit.test.ts b/tests/unit/handler/updateVin.unit.test.ts new file mode 100644 index 00000000..bea8dc0c --- /dev/null +++ b/tests/unit/handler/updateVin.unit.test.ts @@ -0,0 +1,91 @@ +/* eslint-disable import/first */ +const mockGetBySystemNumberAndCreatedTimestamp = jest.fn(); +const mockUpdateVehicle = jest.fn(); + +import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-verb'; +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { handler } from '../../../src/handler/updateVin'; +import * as UserDetails from '../../../src/services/user'; +import { ERRORS } from '../../../src/util/enum'; +import { formatErrorMessage } from '../../../src/util/errorMessage'; +import hgvData from '../../resources/techRecordHGVPost.json'; +import { mockToken } from '../util/mockToken'; + +jest.mock('../../../src/services/database.ts', () => ({ + getBySystemNumberAndCreatedTimestamp: mockGetBySystemNumberAndCreatedTimestamp, + updateVehicle: mockUpdateVehicle, +})); + +const payload = { + newVin: '987654321', +}; + +const mockUserDetails = { + username: 'Test User', msOid: 'QWERTY', email: 'testUser@test.com', +}; + +describe('update vin handler', () => { + let request: APIGatewayProxyEvent; + beforeEach(() => { + request = { + headers: { + Authorization: mockToken, + }, + pathParameters: { + systemNumber: '10000067', + createdTimestamp: '2023-06-16T11:26:30.196Z', + }, + body: JSON.stringify(payload), + } as unknown as APIGatewayProxyEvent; + jest.resetAllMocks(); + jest.resetModules(); + }); + describe('successful change', () => { + it('should pass validation, return a 200', async () => { + process.env.AWS_SAM_LOCAL = 'true'; + + jest.spyOn(UserDetails, 'getUserDetails').mockReturnValueOnce(mockUserDetails); + mockGetBySystemNumberAndCreatedTimestamp.mockResolvedValueOnce(hgvData); + const newRecord = { ...hgvData, ...JSON.parse(request.body ?? '') } as TechRecordType<'put'>; + mockUpdateVehicle.mockResolvedValueOnce(newRecord); + const result = await handler(request); + + expect(result.statusCode).toBe(200); + + expect(mockGetBySystemNumberAndCreatedTimestamp).toHaveBeenCalledTimes(1); + expect(mockUpdateVehicle).toHaveBeenCalledTimes(1); + expect(result.body).not.toBeNull(); + }); + }); + + describe('error handling', () => { + it('should error when no body is given', async () => { + request.body = null; + const result = await handler(request); + expect(result.statusCode).toBe(400); + expect(result.body).toBe('{"errors":["Missing payload!"]}'); + }); + + it('should error if no new vin is given', async () => { + request.body = JSON.stringify({ newVin: null }); + const result = await handler(request); + expect(result.statusCode).toBe(400); + expect(result.body).toBe('{"errors":["No new VIN provided"]}'); + }); + + it('should error if the record is archived', async () => { + mockGetBySystemNumberAndCreatedTimestamp.mockResolvedValueOnce({ techRecord_statusCode: 'archived' }); + const result = await handler(request); + expect(result.statusCode).toBe(400); + expect(result.body).toEqual(formatErrorMessage(ERRORS.CANNOT_UPDATE_ARCHIVED_RECORD)); + }); + + it('should error if the new vin is invalid', async () => { + request.body = JSON.stringify({ newVin: 'OIQ1' }); + mockGetBySystemNumberAndCreatedTimestamp.mockResolvedValueOnce({ techRecord_statusCode: 'current' }); + const result = await handler(request); + expect(result.statusCode).toBe(400); + expect(result.body).toEqual(formatErrorMessage(ERRORS.VIN_ERROR)); + }); + }); +}); diff --git a/tests/unit/validators/update.unit.test.ts b/tests/unit/validators/update.unit.test.ts index 5f4b1be2..95aac32b 100644 --- a/tests/unit/validators/update.unit.test.ts +++ b/tests/unit/validators/update.unit.test.ts @@ -1,8 +1,8 @@ import { APIGatewayProxyEvent } from 'aws-lambda'; import { ERRORS, StatusCode } from '../../../src/util/enum'; +import { formatErrorMessage } from '../../../src/util/errorMessage'; import { checkStatusCodeValidity, validateUpdateErrors, validateUpdateVrmRequest } from '../../../src/validators/update'; import { mockToken } from '../util/mockToken'; -import { formatErrorMessage } from '../../../src/util/errorMessage'; const trlPayload = { techRecord_vehicleConfiguration: 'other', @@ -38,13 +38,13 @@ describe('checkStatusCodeValidity', () => { it('throws error if trying to update archived record', () => { expect(checkStatusCodeValidity(StatusCode.ARCHIVED)).toEqual({ statusCode: 400, - body: ERRORS.CANNOT_UPDATE_ARCHIVED_RECORD, + body: formatErrorMessage(ERRORS.CANNOT_UPDATE_ARCHIVED_RECORD), }); }); it('throws error if trying to archive a record', () => { expect(checkStatusCodeValidity(undefined, StatusCode.ARCHIVED)).toEqual({ statusCode: 400, - body: ERRORS.CANNOT_USE_UPDATE_TO_ARCHIVE, + body: formatErrorMessage(ERRORS.CANNOT_USE_UPDATE_TO_ARCHIVE), }); }); it('returns false if there are no errors', () => { diff --git a/tests/unit/validators/vinValidity.unit.test.ts b/tests/unit/validators/vinValidity.unit.test.ts index efc3361b..71796a41 100644 --- a/tests/unit/validators/vinValidity.unit.test.ts +++ b/tests/unit/validators/vinValidity.unit.test.ts @@ -1,26 +1,27 @@ import { ERRORS } from '../../../src/util/enum'; +import { formatErrorMessage } from '../../../src/util/errorMessage'; import { checkVinValidity } from '../../../src/validators/vinValidity'; describe('checkVinValidity', () => { it('throws error if new vin is invalid', () => { expect(checkVinValidity('1234', '12')).toEqual({ statusCode: 400, - body: ERRORS.VIN_ERROR, + body: formatErrorMessage(ERRORS.VIN_ERROR), }); expect(checkVinValidity('1234', '123456789123456789123456789')).toEqual({ statusCode: 400, - body: ERRORS.VIN_ERROR, + body: formatErrorMessage(ERRORS.VIN_ERROR), }); expect(checkVinValidity('1234', '')).toEqual({ statusCode: 400, - body: ERRORS.VIN_ERROR, + body: formatErrorMessage(ERRORS.VIN_ERROR), }); }); it('should return an error if newVin contains special characters', () => { const result = checkVinValidity('12345', '!newvin'); expect(result).toEqual({ statusCode: 400, - body: ERRORS.VIN_ERROR, + body: formatErrorMessage(ERRORS.VIN_ERROR), }); }); it('returns false if no errors', () => { diff --git a/webpack/webpack.production.js b/webpack/webpack.production.js index bdd17fd3..5d11e939 100644 --- a/webpack/webpack.production.js +++ b/webpack/webpack.production.js @@ -8,7 +8,7 @@ const AwsSamPlugin = require("aws-sam-webpack-plugin"); const LAMBDA_NAMES = ['SearchLambdaFunction', 'GetLambdaFunction', 'PostLambdaFunction', 'PatchLambdaFunction', - 'ArchiveLambdaFunction', 'UnarchiveLambdaFunction', 'PromoteLambdaFunction', 'UpdateVrmFunction', , 'GeneratePlateFunction', 'GenerateLetterFunction', 'SyncTestResultInfoFunction']; + 'ArchiveLambdaFunction', 'UnarchiveLambdaFunction', 'PromoteLambdaFunction', 'UpdateVrmFunction', 'UpdateVinFunction', 'GeneratePlateFunction', 'GenerateLetterFunction', 'SyncTestResultInfoFunction']; const OUTPUT_FOLDER = './' const REPO_NAME = 'cvs-svc-technical-records-v3'; const BRANCH_NAME = branchName().replace(/\//g, "-");