Skip to content

Commit e7be472

Browse files
Bring get letters in line with patch letter
1 parent efcbeb3 commit e7be472

File tree

11 files changed

+128
-148
lines changed

11 files changed

+128
-148
lines changed

lambdas/api-handler/src/contracts/errors.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export enum ApiErrorDetail {
3333
InvalidRequestMissingBody = 'The request is missing the body',
3434
InvalidRequestMissingLetterIdPathParameter = 'The request is missing the letter id path parameter',
3535
InvalidRequestLetterIdsMismatch = 'The letter ID in the request body does not match the letter ID path parameter',
36-
InvalidRequestBody = 'The request body is invalid'
36+
InvalidRequestBody = 'The request body is invalid',
37+
InvalidRequestLimitNotANumber = "The limit parameter is not a number",
38+
InvalidRequestLimitNotPositive = "The limit parameter is not positive"
3739
}
3840

3941
export function buildApiError(params: {

lambdas/api-handler/src/contracts/letter-api.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ export const LetterApiStatusSchema = z.enum([
1818
export type LetterApiStatus = z.infer<typeof LetterApiStatusSchema>;
1919

2020
export const LetterApiAttributesSchema = z.object({
21-
reasonCode: z.number().optional(),
22-
reasonText: z.string().optional(),
23-
requestedProductionStatus: z.enum(["ACTIVE", "HOLD", "CANCEL"]).optional(),
2421
specificationId: z.string(),
2522
status: LetterApiStatusSchema,
23+
reasonCode: z.number().optional(),
24+
reasonText: z.string().optional(),
25+
groupId: z.string().optional()
2626
});
2727

2828
export type LetterApiAttributes = z.infer<typeof LetterApiAttributesSchema>;
@@ -36,7 +36,13 @@ export const LetterApiResourceSchema = z.object({
3636
export type LetterApiResource = z.infer<typeof LetterApiResourceSchema>;
3737

3838
export const LetterApiDocumentSchema = z.object({
39-
data: LetterApiResourceSchema,
39+
data: LetterApiResourceSchema
40+
});
41+
42+
export const LettersApiDocumentSchema = z.object({
43+
data: z.array(LetterApiResourceSchema)
4044
});
4145

4246
export type LetterApiDocument = z.infer<typeof LetterApiDocumentSchema>;
47+
48+
export type LettersApiDocument = z.infer<typeof LettersApiDocumentSchema>;

lambdas/api-handler/src/handlers/__tests__/get-letters.test.ts

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import { getLetters } from '../../index';
2-
import type { Context } from 'aws-lambda';
2+
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
33
import { mockDeep } from 'jest-mock-extended';
44
import { makeApiGwEvent } from './utils/test-utils';
55
import * as letterService from '../../services/letter-operations';
6+
import { mapErrorToResponse } from '../../mappers/error-mapper';
7+
8+
jest.mock('../../mappers/error-mapper');
9+
const mockedMapErrorToResponse = jest.mocked(mapErrorToResponse);
10+
const expectedErrorResponse: APIGatewayProxyResult = {
11+
statusCode: 400,
12+
body: "Error"
13+
};
14+
mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
615

716
jest.mock('../../services/letter-operations');
817

@@ -15,7 +24,7 @@ jest.mock("../../config/lambda-config", () => ({
1524
describe('API Lambda handler', () => {
1625

1726
beforeEach(() => {
18-
jest.resetAllMocks();
27+
jest.clearAllMocks();
1928
});
2029

2130
it('returns 200 OK with basic paginated resources', async () => {
@@ -52,18 +61,18 @@ describe('API Lambda handler', () => {
5261
{
5362
id: "l1",
5463
type: "Letter",
55-
attributes: { specificationId: "s1", groupId: 'g1', status: "PENDING" },
64+
attributes: { reasonCode: 123, reasonText: "Reason text", specificationId: "s1", status: "PENDING", groupId: 'g1' },
5665
},
5766
{
5867
id: "l2",
5968
type: "Letter",
60-
attributes: { specificationId: "s1", groupId: 'g1', status: "PENDING" },
69+
attributes: { reasonCode: 123, reasonText: "Reason text", specificationId: "s1", status: "PENDING", groupId: 'g1' },
6170
},
6271
{
6372
id: "l3",
6473
type: "Letter",
65-
attributes: { specificationId: "s1", groupId: 'g1', status: "PENDING" },
66-
},
74+
attributes: { reasonCode: 123, reasonText: "Reason text", specificationId: "s1", status: "PENDING", groupId: 'g1' },
75+
}
6776
],
6877
};
6978

@@ -73,46 +82,30 @@ describe('API Lambda handler', () => {
7382
});
7483
});
7584

76-
it('returns 404 Not Found for an unknown path', async () => {
77-
const event = makeApiGwEvent({ path: '/unknown' });
78-
const context = mockDeep<Context>();
79-
const callback = jest.fn();
80-
const result = await getLetters(event, context, callback);
81-
82-
expect(result).toEqual({
83-
statusCode: 404,
84-
body: 'Not Found',
85-
});
86-
});
87-
8885
it("returns 400 if the limit parameter is not a number", async () => {
8986
const event = makeApiGwEvent({
9087
path: "/letters",
9188
queryStringParameters: { limit: "1%" },
92-
});
89+
headers: {'nhsd-supplier-id': 'supplier1'}});
90+
9391
const context = mockDeep<Context>();
9492
const callback = jest.fn();
9593
const result = await getLetters(event, context, callback);
9694

97-
expect(result).toEqual({
98-
statusCode: 400,
99-
body: "Bad Request: limit parameter is not a number",
100-
});
95+
expect(result).toEqual(expectedErrorResponse);
10196
});
10297

10398
it("returns 400 if the limit parameter is not positive", async () => {
10499
const event = makeApiGwEvent({
105100
path: "/letters",
106101
queryStringParameters: { limit: "-1" },
107-
});
102+
headers: {'nhsd-supplier-id': 'supplier1'}});
103+
108104
const context = mockDeep<Context>();
109105
const callback = jest.fn();
110106
const result = await getLetters(event, context, callback);
111107

112-
expect(result).toEqual({
113-
statusCode: 400,
114-
body: "Bad Request: limit parameter is not positive",
115-
});
108+
expect(result).toEqual(expectedErrorResponse);
116109
});
117110

118111
it('returns 400 for missing supplier ID (empty headers)', async () => {
@@ -121,10 +114,7 @@ describe('API Lambda handler', () => {
121114
const callback = jest.fn();
122115
const result = await getLetters(event, context, callback);
123116

124-
expect(result).toEqual({
125-
statusCode: 400,
126-
body: 'Bad Request: Missing supplier ID',
127-
});
117+
expect(result).toEqual(expectedErrorResponse);
128118
});
129119

130120
it('returns 400 for missing supplier ID (undefined headers)', async () => {
@@ -133,9 +123,6 @@ describe('API Lambda handler', () => {
133123
const callback = jest.fn();
134124
const result = await getLetters(event, context, callback);
135125

136-
expect(result).toEqual({
137-
statusCode: 400,
138-
body: 'Bad Request: Missing supplier ID',
139-
});
126+
expect(result).toEqual(expectedErrorResponse);
140127
});
141128
});

lambdas/api-handler/src/handlers/__tests__/patch-letters.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ function makeLetterApiDocument(id: string, status: LetterApiStatus) : LetterApiD
3333
attributes: {
3434
reasonCode: 123,
3535
reasonText: "Reason text",
36-
requestedProductionStatus: "ACTIVE",
3736
specificationId: "spec1",
3837
status
3938
},
Lines changed: 52 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,26 @@
1-
import { APIGatewayProxyHandler } from "aws-lambda";
1+
import { APIGatewayProxyEventQueryStringParameters, APIGatewayProxyHandler } from "aws-lambda";
22
import { getLettersForSupplier } from "../services/letter-operations";
33
import { createLetterRepository } from "../infrastructure/letter-repo-factory";
44
import { LetterBase } from "../../../../internal/datastore/src";
5+
import { assertNotEmpty } from "../utils/validation";
6+
import * as errors from '../contracts/errors';
7+
import { lambdaConfig } from "../config/lambda-config";
58
import pino from 'pino';
9+
import { mapErrorToResponse } from "../mappers/error-mapper";
10+
import { ValidationError } from "../errors";
11+
import { mapLetterBaseToApiDocument, mapLetterBaseToApiResource } from "../mappers/letter-mapper";
612

713
const letterRepo = createLetterRepository();
814
const log = pino();
915

16+
// The endpoint should only return pending letters for now
17+
const status = "PENDING";
18+
1019
export const getLetters: APIGatewayProxyHandler = async (event) => {
11-
if (event.path === "/letters") {
12-
const supplierId = event.headers ? event.headers["NHSD-Supplier-ID"] : undefined;
13-
14-
if (!supplierId) {
15-
log.info({
16-
description: 'Supplier ID not provided'
17-
});
18-
return {
19-
statusCode: 400,
20-
body: "Bad Request: Missing supplier ID",
21-
};
22-
}
23-
24-
// The endpoint should only return pending letters for now
25-
const status = "PENDING";
26-
27-
let limit = event.queryStringParameters?.limit;
28-
29-
if (!limit) {
30-
limit = "10";
31-
}
32-
33-
let limitNumber = Number(limit);
34-
35-
if (isNaN(limitNumber)) {
36-
log.info({
37-
description: "limit parameter is not a number",
38-
limit,
39-
});
40-
return {
41-
statusCode: 400,
42-
body: "Bad Request: limit parameter is not a number",
43-
};
44-
}
45-
46-
if (limitNumber < 0) {
47-
log.info({
48-
description: "limit parameter is not positive",
49-
limit,
50-
});
51-
return {
52-
statusCode: 400,
53-
body: "Bad Request: limit parameter is not positive",
54-
};
55-
}
20+
21+
try {
22+
const supplierId = assertNotEmpty(event.headers[lambdaConfig.SUPPLIER_ID_HEADER], errors.ApiErrorDetail.InvalidRequestMissingSupplierId);
23+
const limitNumber = validateLimit(event.queryStringParameters);
5624

5725
const letters = await getLettersForSupplier(
5826
supplierId,
@@ -61,12 +29,14 @@ export const getLetters: APIGatewayProxyHandler = async (event) => {
6129
letterRepo,
6230
);
6331

64-
const response = createGetLettersResponse(letters);
32+
const response = {
33+
data: letters.map((letter: LetterBase) => (mapLetterBaseToApiResource(letter)))
34+
};
6535

6636
log.info({
6737
description: 'Pending letters successfully fetched',
6838
supplierId,
69-
limit,
39+
limitNumber,
7040
status,
7141
lettersCount: letters.length
7242
});
@@ -76,44 +46,43 @@ export const getLetters: APIGatewayProxyHandler = async (event) => {
7646
body: JSON.stringify(response, null, 2),
7747
};
7848
}
49+
catch (error) {
50+
return mapErrorToResponse(error);
51+
}
52+
};
7953

80-
log.warn({
81-
description: 'Unsupported event path',
82-
path: event.path
83-
});
54+
function validateLimit(queryStringParameters: APIGatewayProxyEventQueryStringParameters | null) : number {
8455

85-
return {
86-
statusCode: 404,
87-
body: "Not Found",
88-
};
89-
};
56+
let limit = queryStringParameters?.limit;
9057

91-
interface GetLettersResponse {
92-
data: Array<{
93-
type: "Letter";
94-
id: string;
95-
attributes: {
96-
specificationId: string;
97-
groupId: string;
98-
status: string;
99-
reasonCode?: number;
100-
reasonText?: string;
101-
};
102-
}>;
58+
if (!limit) {
59+
limit = "10";
60+
}
61+
62+
let limitNumber = Number(limit);
63+
64+
assertIsNumber(limitNumber, limit);
65+
assertIsPositive(limitNumber, limit);
66+
67+
return limitNumber;
10368
}
10469

105-
function createGetLettersResponse(letters: LetterBase[]): GetLettersResponse {
106-
return {
107-
data: letters.map((letter) => ({
108-
id: letter.id,
109-
type: "Letter",
110-
attributes: {
111-
specificationId: letter.specificationId,
112-
groupId: letter.groupId,
113-
status: letter.status,
114-
reasonCode: letter.reasonCode,
115-
reasonText: letter.reasonText,
116-
},
117-
})),
118-
};
70+
function assertIsNumber(limitNumber: number, limit: string) {
71+
if (isNaN(limitNumber)) {
72+
log.info({
73+
description: "limit parameter is not a number",
74+
limit,
75+
});
76+
throw new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotANumber);
77+
}
78+
}
79+
80+
function assertIsPositive(limitNumber: number, limit?: string) {
81+
if (limitNumber < 0) {
82+
log.info({
83+
description: "limit parameter is not positive",
84+
limit,
85+
});
86+
throw new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotPositive);
87+
}
11988
}

lambdas/api-handler/src/handlers/patch-letters.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as errors from '../contracts/errors';
66
import { ValidationError } from '../errors';
77
import { mapErrorToResponse } from '../mappers/error-mapper';
88
import { lambdaConfig } from "../config/lambda-config";
9+
import { assertNotEmpty } from '../utils/validation';
910

1011
const letterRepo = createLetterRepository();
1112
export const patchLetters: APIGatewayProxyHandler = async (event) => {
@@ -37,10 +38,3 @@ export const patchLetters: APIGatewayProxyHandler = async (event) => {
3738
return mapErrorToResponse(error);
3839
}
3940
};
40-
41-
function assertNotEmpty(value: string | null | undefined, detail: errors.ApiErrorDetail): string {
42-
if (!value || value.trim() === '') {
43-
throw new ValidationError(detail);
44-
}
45-
return value;
46-
}

lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { toApiLetter } from '../letter-mapper';
1+
import { mapLetterBaseToApiDocument } from '../letter-mapper';
22
import { Letter } from '../../../../../internal/datastore';
33
import { LetterApiDocument } from '../../contracts/letter-api';
44

5-
describe('toApiLetter', () => {
5+
describe('mapLetterBaseToApiDocument', () => {
66
it('maps a Letter to LetterApiDocument', () => {
77
const letter: Letter = {
88
id: 'abc123',
@@ -18,7 +18,7 @@ describe('toApiLetter', () => {
1818
ttl: 123
1919
};
2020

21-
const result: LetterApiDocument = toApiLetter(letter);
21+
const result: LetterApiDocument = mapLetterBaseToApiDocument(letter);
2222

2323
expect(result).toEqual({
2424
data: {
@@ -27,9 +27,9 @@ describe('toApiLetter', () => {
2727
attributes: {
2828
reasonCode: 123,
2929
reasonText: 'Reason text',
30-
requestedProductionStatus: 'ACTIVE',
3130
specificationId: 'spec123',
32-
status: 'PENDING'
31+
status: 'PENDING',
32+
groupId: 'group123'
3333
}
3434
}
3535
});

0 commit comments

Comments
 (0)