Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f427b3d
init
alexnuttall May 12, 2026
465e7ba
tf
alexnuttall May 12, 2026
30da0f5
add nhs number to proof request validator
alexnuttall May 12, 2026
53be4a9
unhappy path api tests
alexnuttall May 12, 2026
670b4dd
coverage
alexnuttall May 12, 2026
e3cc439
fmt
alexnuttall May 12, 2026
12f8824
add nhs number validation
alexnuttall May 12, 2026
eeb31b0
add personalisation to tests
alexnuttall May 12, 2026
9f7e444
coverage
alexnuttall May 12, 2026
09bd5af
cleanup
alexnuttall May 13, 2026
1a70874
use latest lambda mod
alexnuttall May 13, 2026
4109b5e
endpoint summary
alexnuttall May 13, 2026
06ab5de
fix comp test assertions
alexnuttall May 13, 2026
fbbbf24
Merge remote-tracking branch 'origin/main' into feature/CCM-7941-crea…
alexnuttall May 13, 2026
8725754
comments
alexnuttall May 13, 2026
047e00f
nhs number api tests
alexnuttall May 14, 2026
6ec77e4
use normal z.refine
alexnuttall May 14, 2026
f212037
add createdBy
alexnuttall May 14, 2026
0dcc0c0
type fix
alexnuttall May 14, 2026
9d4c921
fmt
alexnuttall May 14, 2026
af6c0ba
CCM-18132: Add get endpoint to spec
m-salaudeen May 14, 2026
19dd435
CCM-18132: Add GET by ID endpoint for proofing request
m-salaudeen May 15, 2026
ec3abc8
CCM-18132: Merge conflicts
m-salaudeen May 15, 2026
503688c
CCM-18132: Add unit and component tests
m-salaudeen May 18, 2026
0a6597b
CCM-18132: Update terraform docs
m-salaudeen May 19, 2026
02af03d
CCM-18132: Update terraform docs
m-salaudeen May 19, 2026
d66e762
CCM-18132: Delete sandbox readme in infrastructure directory
m-salaudeen May 19, 2026
cb76174
CCM-18132: Fix review comments
m-salaudeen May 19, 2026
739de30
CCM-18132: Fix review comments
m-salaudeen May 19, 2026
df3d3f7
CCM-18132: Fix review comments
m-salaudeen May 19, 2026
3c096a5
Merge remote-tracking branch 'origin' into feature/CCM-18132_get-proo…
m-salaudeen May 20, 2026
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
1 change: 1 addition & 0 deletions infrastructure/terraform/modules/backend-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ No requirements.
| <a name="module_get_client_lambda"></a> [get\_client\_lambda](#module\_get\_client\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_get_contact_details_lambda"></a> [get\_contact\_details\_lambda](#module\_get\_contact\_details\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.0/terraform-lambda.zip | n/a |
| <a name="module_get_letter_variant_lambda"></a> [get\_letter\_variant\_lambda](#module\_get\_letter\_variant\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
| <a name="module_get_proofing_request_lambda"></a> [get\_proofing\_request\_lambda](#module\_get\_proofing\_request\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.6/terraform-lambda.zip | n/a |
| <a name="module_get_routing_config_lambda"></a> [get\_routing\_config\_lambda](#module\_get\_routing\_config\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_get_routing_configs_by_template_id_lambda"></a> [get\_routing\_configs\_by\_template\_id\_lambda](#module\_get\_routing\_configs\_by\_template\_id\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_get_template_lambda"></a> [get\_template\_lambda](#module\_get\_template\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
module.get_routing_config_lambda.function_arn,
module.get_routing_configs_by_template_id_lambda.function_arn,
module.get_template_lambda.function_arn,
module.get_proofing_request_lambda.function_arn,
module.get_template_letter_variants_lambda.function_arn,
module.list_contact_details_lambda.function_arn,
module.list_routing_configs_lambda.function_arn,
Expand Down
1 change: 1 addition & 0 deletions infrastructure/terraform/modules/backend-api/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ locals {
GET_ROUTING_CONFIGS_BY_TEMPLATE_ID_LAMBDA_ARN = module.get_routing_configs_by_template_id_lambda.function_arn
GET_TEMPLATE_LAMBDA_ARN = module.get_template_lambda.function_arn
GET_TEMPLATE_LETTER_VARIANTS_LAMBDA_ARN = module.get_template_letter_variants_lambda.function_arn
GET_PROOFING_REQUEST_LAMBDA_ARN = module.get_proofing_request_lambda.function_arn
LIST_CONTACT_DETAILS_LAMBDA_ARN = module.list_contact_details_lambda.function_arn
LIST_ROUTING_CONFIGS_LAMBDA_ARN = module.list_routing_configs_lambda.function_arn
LIST_TEMPLATES_LAMBDA_ARN = module.list_template_lambda.function_arn
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module "get_proofing_request_lambda" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.6/terraform-lambda.zip"

project = var.project
environment = var.environment
component = var.component
aws_account_id = var.aws_account_id
region = var.region

kms_key_arn = var.kms_key_arn

function_name = "get_routing_config_lambda-proofing-request"

function_module_name = "get-proofing-request"
handler_function_name = "handler"
description = "Get proofing request API endpoint"

memory = 2048
timeout = 20
runtime = "nodejs22.x"

log_retention_in_days = var.log_retention_in_days
iam_policy_document = {
body = data.aws_iam_policy_document.get_proofing_request.json
}

lambda_env_vars = local.backend_lambda_environment_variables
function_s3_bucket = var.function_s3_bucket
function_code_base_path = local.lambdas_dir
function_code_dir = "backend-api/dist/get-proofing-request"

send_to_firehose = var.send_to_firehose
log_destination_arn = var.log_destination_arn
log_subscription_role_arn = var.log_subscription_role_arn
}

data "aws_iam_policy_document" "get_proofing_request" {
statement {
sid = "AllowKMSAccess"
effect = "Allow"

actions = [
"kms:Decrypt",
"kms:DescribeKey",
"kms:Encrypt",
"kms:GenerateDataKey*",
"kms:ReEncrypt*",
]

resources = [
var.kms_key_arn
]
}

statement {
sid = "AllowProofingRequestsRead"
effect = "Allow"

actions = [
"dynamodb:GetItem",
]

resources = [
aws_dynamodb_table.proof_requests.arn,
]
}
}
65 changes: 65 additions & 0 deletions infrastructure/terraform/modules/backend-api/spec.tmpl.json
Original file line number Diff line number Diff line change
Expand Up @@ -3278,6 +3278,71 @@
}
}
},
"/v1/proofing-request/{proofingRequestId}": {
"get": {
"description": "Get the proofing request for a digital-channel template",
"parameters": [
{
"description": "ID of the proofing request to get",
"in": "path",
"name": "proofingRequestId",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProofRequestSuccess"
}
}
},
"description": "200 response",
"headers": {
"Content-Type": {
"schema": {
"type": "string"
}
}
}
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Failure"
}
}
},
"description": "Error"
}
},
"security": [
{
"authorizer": []
}
],
"summary": "Get the proofing request for a digital-channel template",
"x-amazon-apigateway-integration": {
"contentHandling": "CONVERT_TO_TEXT",
"credentials": "${APIG_EXECUTION_ROLE_ARN}",
"httpMethod": "POST",
"passthroughBehavior": "WHEN_NO_TEMPLATES",
"responses": {
".*": {
"statusCode": "200"
}
},
"timeoutInMillis": 29000,
"type": "AWS_PROXY",
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${GET_PROOFING_REQUEST_LAMBDA_ARN}/invocations"
}
}
},
"/v1/template/{templateId}/routing-configurations": {
"get": {
"description": "Get all routing configurations that reference a specific template",
Expand Down
11 changes: 11 additions & 0 deletions lambdas/backend-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,4 +351,15 @@ curl -X POST --location "${APIG_STAGE}/v1/template/${TEMPLATE_ID}/proofing-reque
}
}'
```

### GET - /v1/proofing-request/:proofingRequestId - Get a digital proofing request by id

Gets a digital proof request by id

```bash
curl -X GET --location "${APIG_STAGE}/v1/proofing-request/${PROOF_REQUEST_ID}" \
--header 'Accept: application/json' \
--header "Authorization: $SANDBOX_TOKEN"
```

<!-- vale on -->
1 change: 1 addition & 0 deletions lambdas/backend-api/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ npx esbuild \
src/get-routing-config.ts \
src/get-routing-configs-by-template-id.ts \
src/get-template-letter-variants.ts \
src/get-proofing-request.ts \
src/get.ts \
src/generate-letter-proof.ts \
src/list-contact-details.ts \
Expand Down
174 changes: 174 additions & 0 deletions lambdas/backend-api/src/__tests__/api/get-proofing-request.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { mock } from 'jest-mock-extended';
import type { ProofRequest } from 'nhs-notify-web-template-management-types';
import { createHandler } from '@backend-api/api/get-proofing-request';
import type { ProofingRequestClient } from '@backend-api/app/proofing-request-client';

function setup() {
const proofingRequestClient = mock<ProofingRequestClient>();
const mocks = { proofingRequestClient };
const handler = createHandler(mocks);

return { handler, mocks };
}

describe('Get Proofing Request Handler', () => {
beforeEach(jest.resetAllMocks);

test.each([
[
'authorizer is undefined',
undefined,
{ proofingRequestId: 'proofing-request-123' },
],
[
'internalUserId is missing',
{ clientId: 'client-id', internalUserId: undefined },
{ proofingRequestId: 'proofing-request-123' },
],
[
'clientId is missing',
{ clientId: undefined, internalUserId: 'user-1234' },
{ proofingRequestId: 'proofing-request-123' },
],
[
'proofingRequestId is missing',
{ clientId: 'client-id', internalUserId: 'user-1234' },
{ proofingRequestId: undefined },
],
])(
'should return 500 - Invalid request when %s',
async (_, ctx, pathParameters) => {
const { handler, mocks } = setup();

const event = mock<APIGatewayProxyEvent>({
requestContext: { authorizer: ctx },
pathParameters,
});

const result = await handler(event, mock<Context>(), jest.fn());

expect(result).toEqual({
statusCode: 500,
body: JSON.stringify({
statusCode: 500,
technicalMessage: 'Invalid request',
}),
});

expect(mocks.proofingRequestClient.get).not.toHaveBeenCalled();
}
);

test('should return error when client returns error', async () => {
const { handler, mocks } = setup();

mocks.proofingRequestClient.get.mockResolvedValueOnce({
error: {
errorMeta: {
code: 500,
description: 'Internal server error',
},
},
});

const event = mock<APIGatewayProxyEvent>({
requestContext: {
authorizer: { internalUserId: 'user-1234', clientId: 'client-id' },
},
pathParameters: { proofingRequestId: 'proofing-request-123' },
});

const result = await handler(event, mock<Context>(), jest.fn());

expect(result).toEqual({
statusCode: 500,
body: JSON.stringify({
statusCode: 500,
technicalMessage: 'Internal server error',
}),
});

expect(mocks.proofingRequestClient.get).toHaveBeenCalledWith(
'proofing-request-123',
{ internalUserId: 'user-1234', clientId: 'client-id' }
);
});

test('should pass validation error details from client', async () => {
const { handler, mocks } = setup();

mocks.proofingRequestClient.get.mockResolvedValueOnce({
error: {
errorMeta: {
code: 400,
description: 'Request failed validation',
details: {
proofingRequestId: 'Required',
},
},
},
});

const event = mock<APIGatewayProxyEvent>({
requestContext: {
authorizer: { internalUserId: 'user-1234', clientId: 'client-id' },
},
pathParameters: { proofingRequestId: 'proofing-request-123' },
body: undefined,
});

const result = await handler(event, mock<Context>(), jest.fn());

expect(result).toEqual({
statusCode: 400,
body: JSON.stringify({
statusCode: 400,
technicalMessage: 'Request failed validation',
details: {
proofingRequestId: 'Required',
},
}),
});
});

test('should return 200 with proof request', async () => {
const { handler, mocks } = setup();

const proofRequest: ProofRequest = {
contactDetailValue: 'test-patient-nhs-number',
createdAt: new Date().toISOString(),
createdBy: 'user-1234',
id: 'proofing-request-123',
templateId: 'template-123',
templateType: 'NHS_APP',
testPatientNhsNumber: 'test-patient-nhs-number',
};

mocks.proofingRequestClient.get.mockResolvedValueOnce({
data: proofRequest,
});

const event = mock<APIGatewayProxyEvent>({
requestContext: {
authorizer: {
internalUserId: 'user-1234',
clientId: 'client-id',
},
},
pathParameters: { proofingRequestId: 'proofing-request-123' },
});

const result = await handler(event, mock<Context>(), jest.fn());

expect(result).toEqual({
statusCode: 200,
body: JSON.stringify({ statusCode: 200, data: proofRequest }),
});

expect(mocks.proofingRequestClient.get).toHaveBeenCalledWith(
'proofing-request-123',
{ internalUserId: 'user-1234', clientId: 'client-id' }
);
});
});
Loading
Loading