diff --git a/infrastructure/terraform/modules/backend-api/README.md b/infrastructure/terraform/modules/backend-api/README.md
index c91abab92..1a3c44461 100644
--- a/infrastructure/terraform/modules/backend-api/README.md
+++ b/infrastructure/terraform/modules/backend-api/README.md
@@ -56,6 +56,7 @@ No requirements.
| [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 |
| [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 |
| [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 |
+| [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 |
| [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 |
| [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 |
| [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 |
diff --git a/infrastructure/terraform/modules/backend-api/iam_role_api_gateway_execution_role.tf b/infrastructure/terraform/modules/backend-api/iam_role_api_gateway_execution_role.tf
index e8e1e56f6..1c119fff1 100644
--- a/infrastructure/terraform/modules/backend-api/iam_role_api_gateway_execution_role.tf
+++ b/infrastructure/terraform/modules/backend-api/iam_role_api_gateway_execution_role.tf
@@ -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,
diff --git a/infrastructure/terraform/modules/backend-api/locals.tf b/infrastructure/terraform/modules/backend-api/locals.tf
index 710b625bc..ddc7f4566 100644
--- a/infrastructure/terraform/modules/backend-api/locals.tf
+++ b/infrastructure/terraform/modules/backend-api/locals.tf
@@ -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
diff --git a/infrastructure/terraform/modules/backend-api/module_get_proofing_request_lambda.tf b/infrastructure/terraform/modules/backend-api/module_get_proofing_request_lambda.tf
new file mode 100644
index 000000000..113e26206
--- /dev/null
+++ b/infrastructure/terraform/modules/backend-api/module_get_proofing_request_lambda.tf
@@ -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,
+ ]
+ }
+}
diff --git a/infrastructure/terraform/modules/backend-api/spec.tmpl.json b/infrastructure/terraform/modules/backend-api/spec.tmpl.json
index a46461db3..d09e7fb0a 100644
--- a/infrastructure/terraform/modules/backend-api/spec.tmpl.json
+++ b/infrastructure/terraform/modules/backend-api/spec.tmpl.json
@@ -3290,6 +3290,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",
diff --git a/lambdas/backend-api/README.md b/lambdas/backend-api/README.md
index b1afbc190..99d9895cb 100644
--- a/lambdas/backend-api/README.md
+++ b/lambdas/backend-api/README.md
@@ -360,4 +360,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"
+```
+
diff --git a/lambdas/backend-api/build.sh b/lambdas/backend-api/build.sh
index af3e550bc..86e6321a1 100755
--- a/lambdas/backend-api/build.sh
+++ b/lambdas/backend-api/build.sh
@@ -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 \
diff --git a/lambdas/backend-api/src/__tests__/api/get-proofing-request.test.ts b/lambdas/backend-api/src/__tests__/api/get-proofing-request.test.ts
new file mode 100644
index 000000000..02624826d
--- /dev/null
+++ b/lambdas/backend-api/src/__tests__/api/get-proofing-request.test.ts
@@ -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();
+ 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({
+ requestContext: { authorizer: ctx },
+ pathParameters,
+ });
+
+ const result = await handler(event, mock(), 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({
+ requestContext: {
+ authorizer: { internalUserId: 'user-1234', clientId: 'client-id' },
+ },
+ pathParameters: { proofingRequestId: 'proofing-request-123' },
+ });
+
+ const result = await handler(event, mock(), 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({
+ requestContext: {
+ authorizer: { internalUserId: 'user-1234', clientId: 'client-id' },
+ },
+ pathParameters: { proofingRequestId: 'proofing-request-123' },
+ body: undefined,
+ });
+
+ const result = await handler(event, mock(), 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({
+ requestContext: {
+ authorizer: {
+ internalUserId: 'user-1234',
+ clientId: 'client-id',
+ },
+ },
+ pathParameters: { proofingRequestId: 'proofing-request-123' },
+ });
+
+ const result = await handler(event, mock(), 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' }
+ );
+ });
+});
diff --git a/lambdas/backend-api/src/__tests__/app/proofing-request-client.test.ts b/lambdas/backend-api/src/__tests__/app/proofing-request-client.test.ts
index 2b8ca5ba0..e033a3591 100644
--- a/lambdas/backend-api/src/__tests__/app/proofing-request-client.test.ts
+++ b/lambdas/backend-api/src/__tests__/app/proofing-request-client.test.ts
@@ -514,4 +514,37 @@ describe('ProofingRequestClient', () => {
}
);
});
+
+ describe('get', () => {
+ it('returns error when repository returns error', async () => {
+ const { client, mocks } = setup();
+
+ const failure = {
+ error: {
+ errorMeta: {
+ code: 500,
+ description: 'Error fetching proofing request from database.',
+ },
+ },
+ };
+
+ mocks.proofRequestRepository.getById.mockResolvedValueOnce(failure);
+
+ const result = await client.get('proofing-request-123', user);
+
+ expect(result).toBe(failure);
+ });
+
+ it('returns proof request for proofingRequestId', async () => {
+ const { client, mocks } = setup();
+
+ mocks.proofRequestRepository.getById.mockResolvedValueOnce({
+ data: proofRequest,
+ });
+
+ const result = await client.get('proofing-request-123', user);
+
+ expect(result).toEqual({ data: proofRequest });
+ });
+ });
});
diff --git a/lambdas/backend-api/src/__tests__/infra/proof-request-repository.test.ts b/lambdas/backend-api/src/__tests__/infra/proof-request-repository.test.ts
index d07351619..f691eef96 100644
--- a/lambdas/backend-api/src/__tests__/infra/proof-request-repository.test.ts
+++ b/lambdas/backend-api/src/__tests__/infra/proof-request-repository.test.ts
@@ -1,5 +1,9 @@
import crypto from 'node:crypto';
-import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
+import {
+ DynamoDBDocumentClient,
+ GetCommand,
+ PutCommand,
+} from '@aws-sdk/lib-dynamodb';
import { mockClient } from 'aws-sdk-client-mock';
import 'aws-sdk-client-mock-jest';
import type { User } from 'nhs-notify-web-template-management-utils';
@@ -105,4 +109,60 @@ describe('ProofRequestRepository', () => {
);
});
});
+
+ describe('getById', () => {
+ it('returns proof request when found', async () => {
+ const { repo, mocks } = setup();
+
+ const proofRequestItem = {
+ ...requestParams,
+ id: randomUuid,
+ createdAt: NOW.toISOString(),
+ createdBy: `INTERNAL_USER#${user.internalUserId}`,
+ };
+
+ mocks.dynamodb.on(GetCommand).resolvesOnce({
+ Item: proofRequestItem,
+ });
+
+ const result = await repo.getById(randomUuid, user);
+
+ expect(result.data).toEqual(proofRequestItem);
+ expect(mocks.dynamodb).toHaveReceivedCommandWith(GetCommand, {
+ TableName: tableName,
+ Key: {
+ id: randomUuid,
+ owner: `INTERNAL_USER#${user.internalUserId}`,
+ },
+ });
+ });
+
+ it('returns not found error when no record found', async () => {
+ const { repo, mocks } = setup();
+
+ mocks.dynamodb.on(GetCommand).resolvesOnce({});
+
+ const result = await repo.getById(randomUuid, user);
+
+ expect(result.data).toBeUndefined();
+ expect(result.error?.errorMeta.code).toBe(404);
+ expect(result.error?.errorMeta.description).toBe(
+ 'Proofing request not found.'
+ );
+ });
+
+ it('returns internal error when DynamoDB read fails', async () => {
+ const { repo, mocks } = setup();
+
+ mocks.dynamodb.on(GetCommand).rejects(new Error('DynamoDB error'));
+
+ const result = await repo.getById(randomUuid, user);
+
+ expect(result.data).toBeUndefined();
+ expect(result.error?.errorMeta.code).toBe(500);
+ expect(result.error?.errorMeta.description).toBe(
+ 'Error fetching proofing request from database.'
+ );
+ });
+ });
});
diff --git a/lambdas/backend-api/src/api/get-proofing-request.ts b/lambdas/backend-api/src/api/get-proofing-request.ts
new file mode 100644
index 000000000..0e1a509a4
--- /dev/null
+++ b/lambdas/backend-api/src/api/get-proofing-request.ts
@@ -0,0 +1,48 @@
+import type { APIGatewayProxyHandler } from 'aws-lambda';
+import { logger } from 'nhs-notify-web-template-management-utils/logger';
+import type { ProofingRequestClient } from '@backend-api/app/proofing-request-client';
+import { apiFailure, apiSuccess } from '@backend-api/api/responses';
+import { ErrorCase } from 'nhs-notify-backend-client/types';
+
+type Dependencies = {
+ proofingRequestClient: ProofingRequestClient;
+};
+
+export function createHandler({
+ proofingRequestClient,
+}: Dependencies): APIGatewayProxyHandler {
+ return async function handler(event) {
+ const { internalUserId, clientId } = event.requestContext.authorizer ?? {};
+
+ const user = { internalUserId, clientId };
+
+ const proofingRequestId = event.pathParameters?.proofingRequestId;
+
+ const log = logger.child({ ...user, proofingRequestId });
+
+ if (!internalUserId || !clientId || !proofingRequestId) {
+ log.error('Invalid event received from API Gateway');
+
+ return apiFailure(ErrorCase.INTERNAL, 'Invalid request');
+ }
+
+ const { data, error } = await proofingRequestClient.get(
+ proofingRequestId,
+ user
+ );
+
+ if (error) {
+ log
+ .child(error.errorMeta)
+ .error('Failed to fetch proofing request', error.actualError);
+
+ return apiFailure(
+ error.errorMeta.code,
+ error.errorMeta.description,
+ error.errorMeta.details
+ );
+ }
+
+ return apiSuccess(200, data);
+ };
+}
diff --git a/lambdas/backend-api/src/api/responses.ts b/lambdas/backend-api/src/api/responses.ts
index 4ce70aaef..db1b0d7d9 100644
--- a/lambdas/backend-api/src/api/responses.ts
+++ b/lambdas/backend-api/src/api/responses.ts
@@ -4,6 +4,7 @@ import type {
RoutingConfig,
RoutingConfigReference,
LetterVariant,
+ ProofRequest,
ContactDetailDto,
} from 'nhs-notify-web-template-management-types';
@@ -16,6 +17,7 @@ export const apiSuccess = <
| Count
| LetterVariant
| LetterVariant[]
+ | ProofRequest
| RoutingConfig
| RoutingConfig[]
| RoutingConfigReference[]
diff --git a/lambdas/backend-api/src/app/proofing-request-client.ts b/lambdas/backend-api/src/app/proofing-request-client.ts
index b7cba684f..d989a8858 100644
--- a/lambdas/backend-api/src/app/proofing-request-client.ts
+++ b/lambdas/backend-api/src/app/proofing-request-client.ts
@@ -87,6 +87,13 @@ export class ProofingRequestClient {
}
}
+ async get(
+ proofingRequestId: string,
+ user: User
+ ): Promise> {
+ return await this.proofRequestRepository.getById(proofingRequestId, user);
+ }
+
private async handleApp(
{
contactDetailId,
diff --git a/lambdas/backend-api/src/get-proofing-request.ts b/lambdas/backend-api/src/get-proofing-request.ts
new file mode 100644
index 000000000..9399bfb1b
--- /dev/null
+++ b/lambdas/backend-api/src/get-proofing-request.ts
@@ -0,0 +1,4 @@
+import { createHandler } from '@backend-api/api/get-proofing-request';
+import { proofingRequestContainer } from '@backend-api/container/proofing-request';
+
+export const handler = createHandler(proofingRequestContainer());
diff --git a/lambdas/backend-api/src/infra/proof-request-repository.ts b/lambdas/backend-api/src/infra/proof-request-repository.ts
index a979db98e..ff5e9a097 100644
--- a/lambdas/backend-api/src/infra/proof-request-repository.ts
+++ b/lambdas/backend-api/src/infra/proof-request-repository.ts
@@ -1,5 +1,9 @@
import { randomUUID } from 'node:crypto';
-import { PutCommand, type DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
+import {
+ GetCommand,
+ PutCommand,
+ type DynamoDBDocumentClient,
+} from '@aws-sdk/lib-dynamodb';
import { $ProofRequest } from 'nhs-notify-backend-client/schemas';
import { ErrorCase } from 'nhs-notify-backend-client/types';
import type { ProofRequest } from 'nhs-notify-web-template-management-types';
@@ -17,7 +21,7 @@ export class ProofRequestRepository {
user: User
): Promise> {
const now = new Date().toISOString();
- const userKey = `INTERNAL_USER#${user.internalUserId}`;
+ const userKey = this.getOwnerKey(user);
const record = $ProofRequest.parse({
...params,
@@ -47,4 +51,39 @@ export class ProofRequestRepository {
);
}
}
+
+ async getById(
+ proofingRequestId: string,
+ user: User
+ ): Promise> {
+ try {
+ const response = await this.dynamodb.send(
+ new GetCommand({
+ TableName: this.tableName,
+ Key: {
+ id: proofingRequestId,
+ owner: this.getOwnerKey(user),
+ },
+ })
+ );
+
+ if (!response?.Item) {
+ return failure(ErrorCase.NOT_FOUND, 'Proofing request not found.');
+ }
+
+ const proofRequestItem = $ProofRequest.parse(response.Item);
+
+ return success(proofRequestItem);
+ } catch (error) {
+ return failure(
+ ErrorCase.INTERNAL,
+ 'Error fetching proofing request from database.',
+ error
+ );
+ }
+ }
+
+ private getOwnerKey(user: User) {
+ return `INTERNAL_USER#${user.internalUserId}`;
+ }
}
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index a0b2952e3..14546312b 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -67,6 +67,11 @@ export type {
GetV1LetterVariantByLetterVariantIdErrors,
GetV1LetterVariantByLetterVariantIdResponse,
GetV1LetterVariantByLetterVariantIdResponses,
+ GetV1ProofingRequestByProofingRequestIdData,
+ GetV1ProofingRequestByProofingRequestIdError,
+ GetV1ProofingRequestByProofingRequestIdErrors,
+ GetV1ProofingRequestByProofingRequestIdResponse,
+ GetV1ProofingRequestByProofingRequestIdResponses,
GetV1RoutingConfigurationByRoutingConfigIdData,
GetV1RoutingConfigurationByRoutingConfigIdError,
GetV1RoutingConfigurationByRoutingConfigIdErrors,
diff --git a/packages/types/src/types.gen.ts b/packages/types/src/types.gen.ts
index d3bd2a610..b80e5e3be 100644
--- a/packages/types/src/types.gen.ts
+++ b/packages/types/src/types.gen.ts
@@ -1297,6 +1297,38 @@ export type PostV1TemplateByTemplateIdProofingRequestResponses = {
export type PostV1TemplateByTemplateIdProofingRequestResponse =
PostV1TemplateByTemplateIdProofingRequestResponses[keyof PostV1TemplateByTemplateIdProofingRequestResponses];
+export type GetV1ProofingRequestByProofingRequestIdData = {
+ body?: never;
+ path: {
+ /**
+ * ID of the proofing request to get
+ */
+ proofingRequestId: string;
+ };
+ query?: never;
+ url: '/v1/proofing-request/{proofingRequestId}';
+};
+
+export type GetV1ProofingRequestByProofingRequestIdErrors = {
+ /**
+ * Error
+ */
+ default: Failure;
+};
+
+export type GetV1ProofingRequestByProofingRequestIdError =
+ GetV1ProofingRequestByProofingRequestIdErrors[keyof GetV1ProofingRequestByProofingRequestIdErrors];
+
+export type GetV1ProofingRequestByProofingRequestIdResponses = {
+ /**
+ * 200 response
+ */
+ 200: ProofRequestSuccess;
+};
+
+export type GetV1ProofingRequestByProofingRequestIdResponse =
+ GetV1ProofingRequestByProofingRequestIdResponses[keyof GetV1ProofingRequestByProofingRequestIdResponses];
+
export type GetV1TemplateByTemplateIdRoutingConfigurationsData = {
body?: never;
path: {
diff --git a/tests/test-team/template-mgmt-api-tests/get-proofing-request.api.spec.ts b/tests/test-team/template-mgmt-api-tests/get-proofing-request.api.spec.ts
new file mode 100644
index 000000000..ae911c9fe
--- /dev/null
+++ b/tests/test-team/template-mgmt-api-tests/get-proofing-request.api.spec.ts
@@ -0,0 +1,231 @@
+import { test, expect, APIRequestContext } from '@playwright/test';
+import { type TestUser, testUsers } from '../helpers/auth/cognito-auth-helper';
+import { TemplateStorageHelper } from '../helpers/db/template-storage-helper';
+import { ContactDetailHelper } from '../helpers/db/contact-details-helper';
+import { TemplateAPIPayloadFactory } from '../helpers/factories/template-api-payload-factory';
+import { getTestContext } from 'helpers/context/context';
+import { ProofRequest } from 'nhs-notify-web-template-management-types';
+import { makeVerifiedContactDetail } from 'helpers/factories/contact-details-factory';
+
+test.describe('GET /v1/proofing-request/:proofingRequestId', () => {
+ const context = getTestContext();
+ const templateStorageHelper = new TemplateStorageHelper();
+ const contactDetailHelper = new ContactDetailHelper();
+
+ let user: TestUser;
+ let userSharedClient: TestUser;
+
+ test.beforeAll(async () => {
+ user = await context.auth.getTestUser(
+ testUsers.UserDigitalProofingEnabled.userId
+ );
+
+ userSharedClient = await context.auth.getTestUser(
+ testUsers.UserDigitalProofingEnabledSharedClient.userId
+ );
+ });
+
+ test.afterAll(async () => {
+ await templateStorageHelper.deleteAdHocTemplates();
+ await templateStorageHelper.deleteSeededTemplates();
+ await contactDetailHelper.cleanup();
+ });
+
+ const createTemplate = async (
+ request: APIRequestContext,
+ templateType: 'NHS_APP' | 'EMAIL' | 'SMS',
+ testUser: TestUser
+ ) => {
+ const createResponse = await request.post(
+ `${process.env.API_BASE_URL}/v1/template`,
+ {
+ headers: {
+ Authorization: await testUser.getAccessToken(),
+ },
+ data: TemplateAPIPayloadFactory.getCreateTemplatePayload({
+ templateType,
+ }),
+ }
+ );
+
+ expect(createResponse.status()).toBe(201);
+ const created = await createResponse.json();
+
+ templateStorageHelper.addAdHocTemplateKey({
+ templateId: created.data.id,
+ clientId: testUser.clientId,
+ });
+
+ return created.data;
+ };
+
+ const createProofingRequest = async (
+ request: APIRequestContext,
+ templateId: string,
+ testUser: TestUser,
+ personalisation?: Record,
+ contactDetailId?: string
+ ) => {
+ const proofingResponse = await request.post(
+ `${process.env.API_BASE_URL}/v1/template/${templateId}/proofing-request`,
+ {
+ headers: {
+ Authorization: await testUser.getAccessToken(),
+ },
+ data: {
+ testPatientNhsNumber: '9000000009',
+ personalisation,
+ contactDetailId,
+ },
+ }
+ );
+
+ const debug = JSON.stringify(await proofingResponse.json());
+ expect(proofingResponse.status(), debug).toBe(201);
+ const result = await proofingResponse.json();
+
+ return result.data as ProofRequest;
+ };
+
+ test.describe('happy path', () => {
+ test('returns statusCode 200 with proofing request', async ({
+ request,
+ }) => {
+ const template = await createTemplate(request, 'NHS_APP', user);
+ const createdProofingRequest = await createProofingRequest(
+ request,
+ template.id,
+ user
+ );
+
+ const response = await request.get(
+ `${process.env.API_BASE_URL}/v1/proofing-request/${createdProofingRequest.id}`,
+ {
+ headers: {
+ Authorization: await user.getAccessToken(),
+ },
+ }
+ );
+
+ expect(response.status()).toBe(200);
+
+ const result = await response.json();
+
+ expect(result).toEqual({
+ statusCode: 200,
+ data: createdProofingRequest,
+ });
+ });
+
+ test('returns statusCode 200 with proofing request with personalisation', async ({
+ request,
+ }) => {
+ const template = await createTemplate(request, 'NHS_APP', user);
+
+ const createdProofingRequest = await createProofingRequest(
+ request,
+ template.id,
+ user,
+ {
+ name: 'John',
+ }
+ );
+
+ const response = await request.get(
+ `${process.env.API_BASE_URL}/v1/proofing-request/${createdProofingRequest.id}`,
+ {
+ headers: {
+ Authorization: await user.getAccessToken(),
+ },
+ }
+ );
+
+ expect(response.status()).toBe(200);
+
+ const result = await response.json();
+
+ expect(result).toEqual({
+ statusCode: 200,
+ data: createdProofingRequest,
+ });
+ });
+ });
+
+ test.describe('auth', () => {
+ test('returns 401 if no auth token', async ({ request }) => {
+ const response = await request.get(
+ `${process.env.API_BASE_URL}/v1/proofing-request/anu-proof-id`
+ );
+
+ expect(response.status()).toBe(401);
+ expect(await response.json()).toEqual({
+ message: 'Unauthorized',
+ });
+ });
+ });
+
+ test.describe('validation', () => {
+ test('returns 404 if no proofing request exists with the given proofingRequestId', async ({
+ request,
+ }) => {
+ const response = await request.get(
+ `${process.env.API_BASE_URL}/v1/proofing-request/invalid-id`,
+ {
+ headers: {
+ Authorization: await user.getAccessToken(),
+ },
+ }
+ );
+
+ const result = await response.json();
+ const debug = JSON.stringify(result, null, 2);
+
+ expect(response.status(), debug).toBe(404);
+ expect(result).toEqual({
+ statusCode: 404,
+ technicalMessage: 'Proofing request not found.',
+ });
+ });
+ });
+
+ test.describe('ownership', () => {
+ test('returns 404 if contact detail is owned by a different user', async ({
+ request,
+ }) => {
+ const template = await createTemplate(request, 'EMAIL', userSharedClient);
+
+ const contactDetail = makeVerifiedContactDetail({
+ type: 'EMAIL',
+ value: 'other-user@example.com',
+ owner: userSharedClient.internalUserId,
+ });
+ await contactDetailHelper.seed([contactDetail]);
+
+ const proofingRequest = await createProofingRequest(
+ request,
+ template.id,
+ userSharedClient,
+ undefined,
+ contactDetail.id
+ );
+
+ const response = await request.get(
+ `${process.env.API_BASE_URL}/v1/proofing-request/${proofingRequest.id}`,
+ {
+ headers: {
+ Authorization: await user.getAccessToken(),
+ },
+ }
+ );
+
+ const result = await response.json();
+ const debug = JSON.stringify(result, null, 2);
+
+ expect(response.status(), debug).toBe(404);
+ expect(result).toEqual({
+ statusCode: 404,
+ technicalMessage: 'Proofing request not found.',
+ });
+ });
+ });
+});