Skip to content

Commit

Permalink
feat(cb2-9114): create amend vin end point (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
naathanbrown authored Oct 4, 2023
1 parent 39e6d0f commit a202703
Show file tree
Hide file tree
Showing 17 changed files with 356 additions and 26 deletions.
12 changes: 7 additions & 5 deletions src/handler/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<APIGatewayProxyResult> => {
logger.info('Update end point called');
Expand Down Expand Up @@ -54,9 +53,12 @@ export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayPr
return addHttpHeaders(statusCodeErrors);
}

const vinErrors = checkVinValidity(recordFromDB.vin, requestBody.vin);
if (vinErrors) {
return addHttpHeaders(vinErrors);
const vinUpdateCheck = requestBody.vin && recordFromDB.vin !== requestBody.vin;
if (vinUpdateCheck) {
return addHttpHeaders({
statusCode: 500,
body: formatErrorMessage('Cannot update VIN with patch end point.'),
});
}

let archiveNeeded = true;
Expand Down Expand Up @@ -95,7 +97,7 @@ export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayPr
logger.error(`${error}`);
return addHttpHeaders({
statusCode: 500,
body: formatErrorMessage('Failed to update record'),
body: formatErrorMessage(ERRORS.FAILED_UPDATE_MESSAGE),
});
}
};
74 changes: 74 additions & 0 deletions src/handler/updateVin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import 'dotenv/config';
import { UpdateVinBody } from '../models/updateVin';
import { setCreatedAuditDetails, setLastUpdatedAuditDetails } from '../services/audit';
import { getBySystemNumberAndCreatedTimestamp, updateVehicle } from '../services/database';
import { getUserDetails } from '../services/user';
import { ERRORS, StatusCode } from '../util/enum';
import { formatErrorMessage } from '../util/errorMessage';
import { addHttpHeaders } from '../util/httpHeaders';
import logger from '../util/logger';
import { createPartialVin } from '../util/partialVin';
import { checkStatusCodeValidity } from '../validators/update';
import { validateAmendVinPayloadErrors } from '../validators/updateVin';
import { checkVinValidity } from '../validators/vinValidity';

export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
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),
});
}
};
3 changes: 3 additions & 0 deletions src/models/updateVin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type UpdateVinBody = {
newVin: string;
};
4 changes: 2 additions & 2 deletions src/processors/processPostRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down
7 changes: 2 additions & 5 deletions src/processors/processUpdateRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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);
};
2 changes: 2 additions & 0 deletions src/util/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ 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',
INVALID_VRM_UPDATE = 'Cannot use update API to update the VRM',
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.',
Expand Down
6 changes: 6 additions & 0 deletions src/util/partialVin.ts
Original file line number Diff line number Diff line change
@@ -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();
};
4 changes: 2 additions & 2 deletions src/validators/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions src/validators/updateVin.ts
Original file line number Diff line number Diff line change
@@ -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;
};
6 changes: 5 additions & 1 deletion src/validators/vinValidity.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
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) {
if (newVin.length < 3
|| 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),
});
}
}
Expand Down
13 changes: 13 additions & 0 deletions template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
101 changes: 101 additions & 0 deletions tests/integration/updateVin/updateVin.int.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
5 changes: 2 additions & 3 deletions tests/unit/handler/update.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const trlPayload = {
techRecord_vehicleConfiguration: 'rigid',
techRecord_vehicleType: 'trl',
trailerId: 'C530005',
vin: '9080977997',
techRecord_bodyType_description: 'artic',
techRecord_bodyType_code: 'a',
};
Expand Down Expand Up @@ -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';
Expand Down
Loading

0 comments on commit a202703

Please sign in to comment.