Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2ae8486
Add post letters endpoint
francisco-videira-nhs Nov 10, 2025
3df81bb
Fix names
francisco-videira-nhs Nov 10, 2025
eca22a0
Fix event mapper
francisco-videira-nhs Nov 10, 2025
8afa97c
Fix sqs
francisco-videira-nhs Nov 10, 2025
da2ba4b
Fix arn
francisco-videira-nhs Nov 10, 2025
8c16496
Fix sqs name
francisco-videira-nhs Nov 10, 2025
7f1706d
Fix param
francisco-videira-nhs Nov 10, 2025
f07b4ea
Fix attempt
francisco-videira-nhs Nov 10, 2025
e5b7bec
Fix attempt sqs
francisco-videira-nhs Nov 11, 2025
918f9c7
Fix attempt await
francisco-videira-nhs Nov 11, 2025
eb86a5b
Clean up tests
francisco-videira-nhs Nov 11, 2025
4a08c77
Merge remote-tracking branch 'origin/main' into feature/CCM-12097
francisco-videira-nhs Nov 11, 2025
f05987d
Naming and minor refactor
francisco-videira-nhs Nov 12, 2025
2536071
Add duplicate validation
francisco-videira-nhs Nov 14, 2025
f4dfe28
Change handler name
francisco-videira-nhs Nov 17, 2025
a93b823
Merge remote-tracking branch 'origin/main' into feature/CCM-12097
francisco-videira-nhs Nov 17, 2025
269ecb3
Merge remote-tracking branch 'origin/main' into feature/CCM-12097
francisco-videira-nhs Nov 17, 2025
dbed0dc
Fix unit tests
francisco-videira-nhs Nov 18, 2025
298f2e7
Change patch to return 202 and use sqs
francisco-videira-nhs Nov 18, 2025
c3f5681
Merge remote-tracking branch 'origin/main' into feature/CCM-12915
francisco-videira-nhs Nov 18, 2025
a5f1603
Merge remote-tracking branch 'origin/main' into feature/CCM-12915
francisco-videira-nhs Nov 18, 2025
b41b1f4
Add await
francisco-videira-nhs Nov 18, 2025
35a173d
Change mapper name
francisco-videira-nhs Nov 19, 2025
5a92c9d
Merge remote-tracking branch 'origin/main' into feature/CCM-12915
francisco-videira-nhs Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ module "patch_letter" {
log_destination_arn = local.destination_arn
log_subscription_role_arn = local.acct.log_subscription_role_arn

lambda_env_vars = merge(local.common_lambda_env_vars, {})
lambda_env_vars = merge(local.common_lambda_env_vars, {
QUEUE_URL = module.letter_status_updates_queue.sqs_queue_url
})
}

data "aws_iam_policy_document" "patch_letter_lambda" {
Expand All @@ -54,21 +56,16 @@ data "aws_iam_policy_document" "patch_letter_lambda" {
}

statement {
sid = "AllowDynamoDBAccess"
sid = "AllowQueueAccess"
effect = "Allow"

actions = [
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:UpdateItem",
"sqs:SendMessage",
"sqs:GetQueueAttributes",
]

resources = [
aws_dynamodb_table.letters.arn,
module.letter_status_updates_queue.sqs_queue_arn
]
}
}
39 changes: 18 additions & 21 deletions lambdas/api-handler/src/handlers/__tests__/patch-letter.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// mock service
jest.mock('../../services/letter-operations');
import * as letterService from '../../services/letter-operations';
const mockedPatchLetterStatus = jest.mocked(letterService.patchLetterStatus);
const mockedBatchUpdateStatus = jest.mocked(letterService.enqueueLetterUpdateRequests);

// mock mapper
jest.mock('../../mappers/error-mapper');
Expand Down Expand Up @@ -59,7 +59,7 @@ describe('patchLetter API Handler', () => {
} as unknown as EnvVars
} as Deps;

it('returns 200 OK with updated resource', async () => {
it('returns 202 Accepted', async () => {
const event = makeApiGwEvent({
path: '/letters/id1',
body: requestBody,
Expand All @@ -86,14 +86,14 @@ describe('patchLetter API Handler', () => {
}
}
};
mockedPatchLetterStatus.mockResolvedValue(updateLetterServiceResponse);
mockedBatchUpdateStatus.mockResolvedValue();

const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);

expect(result).toEqual({
statusCode: 200,
body: JSON.stringify(updateLetterServiceResponse, null, 2)
statusCode: 202,
body: ''
});
});

Expand Down Expand Up @@ -137,16 +137,12 @@ describe('patchLetter API Handler', () => {
expect(result).toEqual(expectedErrorResponse);
});

it('returns error response when error is thrown by service', async () => {
const error = new Error('Service error');
mockedPatchLetterStatus.mockRejectedValue(error);

it('returns error when supplier id is missing', async () => {
const event = makeApiGwEvent({
path: '/letters/id1',
body: requestBody,
pathParameters: {id: 'id1'},
headers: {
'nhsd-supplier-id': 'supplier1',
'nhsd-correlation-id': 'correlationId',
'x-request-id': 'requestId'
}
Expand All @@ -157,16 +153,17 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);

expect(mockedProcessError).toHaveBeenCalledWith(error, 'correlationId', mockedDeps.logger);
expect(mockedProcessError).toHaveBeenCalledWith(new Error('The supplier ID is missing from the request'), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});

it('returns error when supplier id is missing', async () => {
it('returns error when request body does not have correct shape', async () => {
const event = makeApiGwEvent({
path: '/letters/id1',
body: requestBody,
body: "{test: 'test'}",
pathParameters: {id: 'id1'},
headers: {
'nhsd-supplier-id': 'supplier1',
'nhsd-correlation-id': 'correlationId',
'x-request-id': 'requestId'
}
Expand All @@ -177,14 +174,14 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);

expect(mockedProcessError).toHaveBeenCalledWith(new Error('The supplier ID is missing from the request'), 'correlationId', mockedDeps.logger);
expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});

it('returns error when request body does not have correct shape', async () => {
it('returns error when request body is not json', async () => {
const event = makeApiGwEvent({
path: '/letters/id1',
body: "{test: 'test'}",
body: '{#invalidJSON',
pathParameters: {id: 'id1'},
headers: {
'nhsd-supplier-id': 'supplier1',
Expand All @@ -202,11 +199,11 @@ describe('patchLetter API Handler', () => {
expect(result).toEqual(expectedErrorResponse);
});

it('returns error when request body is not json', async () => {
it('returns error if path letterId and body letterId do not match', async () => {
const event = makeApiGwEvent({
path: '/letters/id1',
body: '{#invalidJSON',
pathParameters: {id: 'id1'},
path: '/letters/id2',
body: requestBody,
pathParameters: {id: 'id2'},
headers: {
'nhsd-supplier-id': 'supplier1',
'nhsd-correlation-id': 'correlationId',
Expand All @@ -219,7 +216,7 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);

expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId', mockedDeps.logger);
expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLetterIdsMismatch), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});

Expand Down
16 changes: 11 additions & 5 deletions lambdas/api-handler/src/handlers/patch-letter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import { patchLetterStatus } from '../services/letter-operations';
import { PatchLetterRequest, PatchLetterRequestSchema } from '../contracts/letters';
import { enqueueLetterUpdateRequests } from '../services/letter-operations';
import { LetterDto, PatchLetterRequest, PatchLetterRequestSchema } from '../contracts/letters';
import { ApiErrorDetail } from '../contracts/errors';
import { ValidationError } from '../errors';
import { processError } from '../mappers/error-mapper';
Expand Down Expand Up @@ -35,11 +35,17 @@ export function createPatchLetterHandler(deps: Deps): APIGatewayProxyHandler {
else throw error;
}

const updatedLetter = await patchLetterStatus(mapPatchLetterToDto(patchLetterRequest, commonHeadersResult.value.supplierId), letterId, deps.letterRepo);
const letterToUpdate: LetterDto = mapPatchLetterToDto(patchLetterRequest, commonHeadersResult.value.supplierId);

if (letterToUpdate.id !== letterId) {
throw new ValidationError(ApiErrorDetail.InvalidRequestLetterIdsMismatch);
}

await enqueueLetterUpdateRequests([letterToUpdate], commonHeadersResult.value.correlationId, deps);

return {
statusCode: 200,
body: JSON.stringify(updatedLetter, null, 2)
statusCode: 202,
body: ''
};

} catch (error) {
Expand Down
7 changes: 6 additions & 1 deletion lambdas/api-handler/src/handlers/post-letters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ValidationError } from '../errors';
import { processError } from '../mappers/error-mapper';
import { assertNotEmpty, requireEnvVar, validateCommonHeaders } from '../utils/validation';
import type { Deps } from "../config/deps";
import { mapPostLetterRequestToLetterDtoArray } from '../mappers/letter-mapper';

export function createPostLettersHandler(deps: Deps): APIGatewayProxyHandler {

Expand Down Expand Up @@ -42,7 +43,11 @@ export function createPostLettersHandler(deps: Deps): APIGatewayProxyHandler {
throw new ValidationError(ApiErrorDetail.InvalidRequestDuplicateLetterId);
}

await enqueueLetterUpdateRequests(postLettersRequest, commonHeadersResult.value.supplierId, commonHeadersResult.value.correlationId, deps);
await enqueueLetterUpdateRequests(
mapPostLetterRequestToLetterDtoArray(postLettersRequest, commonHeadersResult.value.supplierId),
commonHeadersResult.value.correlationId,
deps
);

return {
statusCode: 202,
Expand Down
14 changes: 7 additions & 7 deletions lambdas/api-handler/src/mappers/letter-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ export function mapPatchLetterToDto(request: PatchLetterRequest, supplierId: str
};
}

export function mapPostLetterResourceToDto(request: PostLettersRequestResource, supplierId: string): LetterDto {
return {
id: request.id,
export function mapPostLetterRequestToLetterDtoArray(request: PostLettersRequest, supplierId: string): LetterDto[] {
Comment thread
francisco-videira-nhs marked this conversation as resolved.
Outdated
return request.data.map( (letterToUpdate: PostLettersRequestResource) => ({
id: letterToUpdate.id,
supplierId,
status: LetterStatus.parse(request.attributes.status),
reasonCode: request.attributes.reasonCode,
reasonText: request.attributes.reasonText,
};
status: LetterStatus.parse(letterToUpdate.attributes.status),
reasonCode: letterToUpdate.attributes.reasonCode,
reasonText: letterToUpdate.attributes.reasonText,
}));
}

export function mapToPatchLetterResponse(letter: LetterBase): PatchLetterResponse {
Expand Down
Loading
Loading