diff --git a/infrastructure/terraform/components/sbx/README.md b/infrastructure/terraform/components/sbx/README.md
index 216eae80c..78679a95e 100644
--- a/infrastructure/terraform/components/sbx/README.md
+++ b/infrastructure/terraform/components/sbx/README.md
@@ -42,6 +42,7 @@
| [download\_bucket\_name](#output\_download\_bucket\_name) | n/a |
| [events\_sns\_topic\_arn](#output\_events\_sns\_topic\_arn) | n/a |
| [internal\_bucket\_name](#output\_internal\_bucket\_name) | n/a |
+| [proof\_requests\_table\_name](#output\_proof\_requests\_table\_name) | n/a |
| [quarantine\_bucket\_name](#output\_quarantine\_bucket\_name) | n/a |
| [request\_proof\_queue\_url](#output\_request\_proof\_queue\_url) | n/a |
| [routing\_config\_table\_name](#output\_routing\_config\_table\_name) | n/a |
diff --git a/infrastructure/terraform/components/sbx/outputs.tf b/infrastructure/terraform/components/sbx/outputs.tf
index cab82d420..8f6710538 100644
--- a/infrastructure/terraform/components/sbx/outputs.tf
+++ b/infrastructure/terraform/components/sbx/outputs.tf
@@ -73,3 +73,7 @@ output "routing_config_table_name" {
output "events_sns_topic_arn" {
value = module.eventpub.sns_topic.arn
}
+
+output "proof_requests_table_name" {
+ value = module.backend_api.proof_requests_table_name
+}
diff --git a/infrastructure/terraform/modules/backend-api/README.md b/infrastructure/terraform/modules/backend-api/README.md
index b3ffa7f60..e4caa4208 100644
--- a/infrastructure/terraform/modules/backend-api/README.md
+++ b/infrastructure/terraform/modules/backend-api/README.md
@@ -68,6 +68,7 @@ No requirements.
| [s3bucket\_internal](#module\_s3bucket\_internal) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.4/terraform-s3bucket.zip | n/a |
| [s3bucket\_quarantine](#module\_s3bucket\_quarantine) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.4/terraform-s3bucket.zip | n/a |
| [sqs\_letter\_render](#module\_sqs\_letter\_render) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-sqs.zip | n/a |
+| [sqs\_proof\_requests\_table\_events\_pipe\_dlq](#module\_sqs\_proof\_requests\_table\_events\_pipe\_dlq) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-sqs.zip | n/a |
| [sqs\_routing\_config\_table\_events\_pipe\_dlq](#module\_sqs\_routing\_config\_table\_events\_pipe\_dlq) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-sqs.zip | n/a |
| [sqs\_sftp\_upload](#module\_sqs\_sftp\_upload) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-sqs.zip | n/a |
| [sqs\_template\_mgmt\_events](#module\_sqs\_template\_mgmt\_events) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-sqs.zip | n/a |
@@ -88,6 +89,7 @@ No requirements.
| [download\_bucket\_name](#output\_download\_bucket\_name) | n/a |
| [download\_bucket\_regional\_domain\_name](#output\_download\_bucket\_regional\_domain\_name) | n/a |
| [internal\_bucket\_name](#output\_internal\_bucket\_name) | n/a |
+| [proof\_requests\_table\_name](#output\_proof\_requests\_table\_name) | n/a |
| [quarantine\_bucket\_name](#output\_quarantine\_bucket\_name) | n/a |
| [request\_proof\_queue\_url](#output\_request\_proof\_queue\_url) | n/a |
| [routing\_config\_table\_name](#output\_routing\_config\_table\_name) | n/a |
diff --git a/infrastructure/terraform/modules/backend-api/cloudwatch_log_group_pipe_proof_requests_table_events.tf b/infrastructure/terraform/modules/backend-api/cloudwatch_log_group_pipe_proof_requests_table_events.tf
new file mode 100644
index 000000000..e652d480c
--- /dev/null
+++ b/infrastructure/terraform/modules/backend-api/cloudwatch_log_group_pipe_proof_requests_table_events.tf
@@ -0,0 +1,5 @@
+resource "aws_cloudwatch_log_group" "pipe_proof_requests_table_events" {
+ name = "/aws/vendedlogs/pipes/${local.csi}-proof-requests-table-events"
+ kms_key_id = var.kms_key_arn
+ retention_in_days = var.log_retention_in_days
+}
diff --git a/infrastructure/terraform/modules/backend-api/dynamodb_table_proof_requests.tf b/infrastructure/terraform/modules/backend-api/dynamodb_table_proof_requests.tf
new file mode 100644
index 000000000..8183bbbed
--- /dev/null
+++ b/infrastructure/terraform/modules/backend-api/dynamodb_table_proof_requests.tf
@@ -0,0 +1,44 @@
+resource "aws_dynamodb_table" "proof_requests" {
+ name = "${local.csi}-proof-requests"
+ billing_mode = "PAY_PER_REQUEST"
+
+ hash_key = "owner"
+ range_key = "id"
+
+ attribute {
+ name = "owner"
+ type = "S"
+ }
+
+ attribute {
+ name = "id"
+ type = "S"
+ }
+
+ ttl {
+ attribute_name = "ttl"
+ enabled = true
+ }
+
+ point_in_time_recovery {
+ enabled = true
+ }
+
+ server_side_encryption {
+ enabled = true
+ kms_key_arn = var.kms_key_arn
+ }
+
+ tags = {
+ "NHSE-Enable-Dynamo-Backup" = var.enable_backup ? "True" : "False"
+ }
+
+ lifecycle {
+ ignore_changes = [
+ name, # To support backup and restore which will result in a new name otherwise
+ ]
+ }
+
+ stream_enabled = true
+ stream_view_type = "NEW_AND_OLD_IMAGES"
+}
diff --git a/infrastructure/terraform/modules/backend-api/module_lambda_event_publisher.tf b/infrastructure/terraform/modules/backend-api/module_lambda_event_publisher.tf
index 8ab88e03b..dadd507a2 100644
--- a/infrastructure/terraform/modules/backend-api/module_lambda_event_publisher.tf
+++ b/infrastructure/terraform/modules/backend-api/module_lambda_event_publisher.tf
@@ -26,6 +26,7 @@ module "lambda_event_publisher" {
lambda_env_vars = {
EVENT_SOURCE = "//notify.nhs.uk/${var.component}/${var.group}/${var.environment}"
+ PROOF_REQUEST_TABLE_NAME = aws_dynamodb_table.proof_requests.name
ROUTING_CONFIG_TABLE_NAME = aws_dynamodb_table.routing_configuration.name
SNS_TOPIC_ARN = coalesce(var.sns_topic_arn, aws_sns_topic.main.arn)
TEMPLATES_TABLE_NAME = aws_dynamodb_table.templates.name
diff --git a/infrastructure/terraform/modules/backend-api/module_sqs_proof_requests_table_events_pipe_dlq.tf b/infrastructure/terraform/modules/backend-api/module_sqs_proof_requests_table_events_pipe_dlq.tf
new file mode 100644
index 000000000..79856eff1
--- /dev/null
+++ b/infrastructure/terraform/modules/backend-api/module_sqs_proof_requests_table_events_pipe_dlq.tf
@@ -0,0 +1,12 @@
+module "sqs_proof_requests_table_events_pipe_dlq" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-sqs.zip"
+
+ aws_account_id = var.aws_account_id
+ component = var.component
+ environment = var.environment
+ project = var.project
+ region = var.region
+ name = "proof-requests-table-events-pipe-dead-letter"
+ sqs_kms_key_arn = var.kms_key_arn
+ message_retention_seconds = 1209600
+}
diff --git a/infrastructure/terraform/modules/backend-api/outputs.tf b/infrastructure/terraform/modules/backend-api/outputs.tf
index 0c43341b2..ed6030b55 100644
--- a/infrastructure/terraform/modules/backend-api/outputs.tf
+++ b/infrastructure/terraform/modules/backend-api/outputs.tf
@@ -45,3 +45,7 @@ output "quarantine_bucket_name" {
output "routing_config_table_name" {
value = aws_dynamodb_table.routing_configuration.name
}
+
+output "proof_requests_table_name" {
+ value = aws_dynamodb_table.proof_requests.name
+}
diff --git a/infrastructure/terraform/modules/backend-api/pipes_pipe_proof_requests_table_events.tf b/infrastructure/terraform/modules/backend-api/pipes_pipe_proof_requests_table_events.tf
new file mode 100644
index 000000000..16551e0d3
--- /dev/null
+++ b/infrastructure/terraform/modules/backend-api/pipes_pipe_proof_requests_table_events.tf
@@ -0,0 +1,113 @@
+resource "aws_pipes_pipe" "proof_requests_table_events" {
+ depends_on = [module.sqs_proof_requests_table_events_pipe_dlq]
+
+ name = "${local.csi}-proof-requests-table-events"
+ role_arn = aws_iam_role.pipe_proof_requests_table_events.arn
+ source = aws_dynamodb_table.proof_requests.stream_arn
+ target = module.sqs_template_mgmt_events.sqs_queue_arn
+ desired_state = "RUNNING"
+ kms_key_identifier = var.kms_key_arn
+
+ source_parameters {
+ dynamodb_stream_parameters {
+ starting_position = "TRIM_HORIZON"
+ on_partial_batch_item_failure = "AUTOMATIC_BISECT"
+ batch_size = 10
+ maximum_batching_window_in_seconds = 5
+ maximum_retry_attempts = 5
+ maximum_record_age_in_seconds = -1
+
+ dead_letter_config {
+ arn = module.sqs_proof_requests_table_events_pipe_dlq.sqs_queue_arn
+ }
+ }
+ }
+
+ target_parameters {
+ input_template = <<-EOF
+ {
+ "dynamodb": <$.dynamodb>,
+ "eventID": <$.eventID>,
+ "eventName": <$.eventName>,
+ "eventSource": <$.eventSource>,
+ "tableName": "${aws_dynamodb_table.proof_requests.name}"
+ }
+ EOF
+
+ sqs_queue_parameters {
+ message_group_id = "$.dynamodb.Keys.id.S"
+ message_deduplication_id = "$.eventID"
+ }
+ }
+
+ log_configuration {
+ level = "ERROR"
+ include_execution_data = ["ALL"]
+
+ cloudwatch_logs_log_destination {
+ log_group_arn = aws_cloudwatch_log_group.pipe_proof_requests_table_events.arn
+ }
+ }
+}
+
+resource "aws_iam_role" "pipe_proof_requests_table_events" {
+ name = "${local.csi}-pipe-proof-requests-table-events"
+ description = "IAM Role for Pipe to forward proof requests table stream events to SQS"
+ assume_role_policy = data.aws_iam_policy_document.pipes_proof_requests_trust_policy.json
+}
+
+data "aws_iam_policy_document" "pipes_proof_requests_trust_policy" {
+ statement {
+ sid = "PipesAssumeRole"
+ effect = "Allow"
+ actions = ["sts:AssumeRole"]
+
+ principals {
+ type = "Service"
+ identifiers = ["pipes.amazonaws.com"]
+ }
+ }
+}
+
+resource "aws_iam_role_policy" "pipe_proof_requests_table_events" {
+ name = "${local.csi}-pipe-proof-requests-table-events"
+ role = aws_iam_role.pipe_proof_requests_table_events.id
+ policy = data.aws_iam_policy_document.pipe_proof_requests_table_events.json
+}
+
+data "aws_iam_policy_document" "pipe_proof_requests_table_events" {
+ version = "2012-10-17"
+
+ statement {
+ sid = "AllowDynamoStreamRead"
+ effect = "Allow"
+ actions = [
+ "dynamodb:DescribeStream",
+ "dynamodb:GetRecords",
+ "dynamodb:GetShardIterator",
+ "dynamodb:ListStreams",
+ ]
+ resources = [aws_dynamodb_table.proof_requests.stream_arn]
+ }
+
+ statement {
+ sid = "AllowSqsSendMessage"
+ effect = "Allow"
+ actions = ["sqs:SendMessage"]
+ resources = [
+ module.sqs_template_mgmt_events.sqs_queue_arn,
+ module.sqs_proof_requests_table_events_pipe_dlq.sqs_queue_arn,
+ ]
+ }
+
+ statement {
+ sid = "AllowKmsUsage"
+ effect = "Allow"
+ actions = [
+ "kms:Decrypt",
+ "kms:Encrypt",
+ "kms:GenerateDataKey*"
+ ]
+ resources = [var.kms_key_arn]
+ }
+}
diff --git a/lambdas/event-publisher/src/__tests__/domain/event-builder.test.ts b/lambdas/event-publisher/src/__tests__/domain/event-builder.test.ts
index ab5ba1615..b5da3f675 100644
--- a/lambdas/event-publisher/src/__tests__/domain/event-builder.test.ts
+++ b/lambdas/event-publisher/src/__tests__/domain/event-builder.test.ts
@@ -23,11 +23,13 @@ const { logger: mockLogger } = createMockLogger();
const tables = {
templates: 'templates-table',
routing: 'routing-config-table',
+ proofRequests: 'proof-request-table',
};
const eventBuilder = new EventBuilder(
tables.templates,
tables.routing,
+ tables.proofRequests,
'event-source',
mockLogger
);
@@ -378,6 +380,67 @@ const expectedRoutingConfigEvent = (
},
});
+const publishableProofRequestEventRecord = (): PublishableEventRecord => ({
+ dynamodb: {
+ SequenceNumber: '4',
+ NewImage: {
+ id: {
+ S: '92b676e9-470f-4d04-ab14-965ef145e15d',
+ },
+ templateId: {
+ S: 'bed3398c-bbe3-435d-80c1-58154d4bf7dd',
+ },
+ templateType: {
+ S: 'SMS',
+ },
+ testPatientNhsNumber: {
+ S: '9000000009',
+ },
+ contactDetails: {
+ M: {
+ sms: {
+ S: '07700900000',
+ },
+ },
+ },
+ personalisation: {
+ M: {
+ firstName: {
+ S: 'Jane',
+ },
+ },
+ },
+ },
+ },
+ eventID: '7f2ae4b0-82c2-4911-9b84-8997d7f3f40d',
+ tableName: tables.proofRequests,
+});
+
+const expectedProofRequestedEvent = () => ({
+ id: '7f2ae4b0-82c2-4911-9b84-8997d7f3f40d',
+ datacontenttype: 'application/json',
+ time: '2022-01-01T09:00:00.000Z',
+ source: 'event-source',
+ type: 'uk.nhs.notify.template-management.ProofRequested.v1',
+ specversion: '1.0',
+ dataschema: 'https://notify.nhs.uk/events/schemas/ProofRequested/v1.json',
+ dataschemaversion: VERSION,
+ plane: 'data',
+ subject: '92b676e9-470f-4d04-ab14-965ef145e15d',
+ data: {
+ id: '92b676e9-470f-4d04-ab14-965ef145e15d',
+ templateId: 'bed3398c-bbe3-435d-80c1-58154d4bf7dd',
+ templateType: 'SMS',
+ testPatientNhsNumber: '9000000009',
+ contactDetails: {
+ sms: '07700900000',
+ },
+ personalisation: {
+ firstName: 'Jane',
+ },
+ },
+});
+
test('errors on unrecognised event table source', () => {
const invalidpublishableTemplateEventRecord = {
...publishableTemplateEventRecord('SUBMITTED'),
@@ -634,3 +697,55 @@ describe('routing config events', () => {
expect(event).toEqual(undefined);
});
});
+
+describe('proof request events', () => {
+ test('builds proof requested event', () => {
+ const event = eventBuilder.buildEvent(publishableProofRequestEventRecord());
+
+ expect(event).toEqual(expectedProofRequestedEvent());
+ });
+
+ test('errors on output schema validation failure after input parsing', () => {
+ const valid = publishableProofRequestEventRecord();
+
+ const invalidDomainEventRecord: PublishableEventRecord = {
+ ...valid,
+ dynamodb: {
+ ...valid.dynamodb,
+ NewImage: {
+ ...valid.dynamodb.NewImage,
+ templateType: { S: 'LETTER' },
+ },
+ },
+ };
+
+ expect(() => eventBuilder.buildEvent(invalidDomainEventRecord)).toThrow(
+ expect.objectContaining({
+ name: 'ZodError',
+ issues: [
+ expect.objectContaining({
+ code: 'invalid_value',
+ values: ['NHS_APP', 'SMS', 'EMAIL'],
+ path: ['data', 'templateType'],
+ }),
+ ],
+ })
+ );
+ });
+
+ test('does not build proof request event on hard delete', () => {
+ const hardDeletePublishableProofRequestEventRecord = {
+ ...publishableProofRequestEventRecord(),
+ dynamodb: {
+ SequenceNumber: '4',
+ NewImage: undefined,
+ },
+ };
+
+ const event = eventBuilder.buildEvent(
+ hardDeletePublishableProofRequestEventRecord
+ );
+
+ expect(event).toEqual(undefined);
+ });
+});
diff --git a/lambdas/event-publisher/src/config.ts b/lambdas/event-publisher/src/config.ts
index 977ec2660..27060b776 100644
--- a/lambdas/event-publisher/src/config.ts
+++ b/lambdas/event-publisher/src/config.ts
@@ -5,6 +5,7 @@ const $Config = z.object({
ROUTING_CONFIG_TABLE_NAME: z.string(),
SNS_TOPIC_ARN: z.string(),
TEMPLATES_TABLE_NAME: z.string(),
+ PROOF_REQUEST_TABLE_NAME: z.string(),
});
export const loadConfig = () => {
diff --git a/lambdas/event-publisher/src/container.ts b/lambdas/event-publisher/src/container.ts
index 3199d25cd..9c9c0da48 100644
--- a/lambdas/event-publisher/src/container.ts
+++ b/lambdas/event-publisher/src/container.ts
@@ -11,6 +11,7 @@ export const createContainer = () => {
ROUTING_CONFIG_TABLE_NAME,
SNS_TOPIC_ARN,
TEMPLATES_TABLE_NAME,
+ PROOF_REQUEST_TABLE_NAME,
} = loadConfig();
const snsClient = new SNSClient({ region: 'eu-west-2' });
@@ -20,6 +21,7 @@ export const createContainer = () => {
const eventBuilder = new EventBuilder(
TEMPLATES_TABLE_NAME,
ROUTING_CONFIG_TABLE_NAME,
+ PROOF_REQUEST_TABLE_NAME,
EVENT_SOURCE,
logger
);
diff --git a/lambdas/event-publisher/src/domain/event-builder.ts b/lambdas/event-publisher/src/domain/event-builder.ts
index d6d401333..6576c0166 100644
--- a/lambdas/event-publisher/src/domain/event-builder.ts
+++ b/lambdas/event-publisher/src/domain/event-builder.ts
@@ -5,6 +5,7 @@ import {
} from '@nhsdigital/nhs-notify-event-schemas-template-management';
import { Logger } from 'nhs-notify-web-template-management-utils/logger';
import {
+ $DynamoDBProofRequest,
$DynamoDBRoutingConfig,
$DynamoDBTemplate,
PublishableEventRecord,
@@ -16,6 +17,7 @@ export class EventBuilder {
constructor(
private readonly templatesTableName: string,
private readonly routingConfigTableName: string,
+ private readonly proofRequestTableName: string,
private readonly eventSource: string,
private readonly logger: Logger
) {}
@@ -52,14 +54,19 @@ export class EventBuilder {
}
}
- private buildEventMetadata(id: string, type: string, subject: string) {
+ private buildEventMetadata(
+ id: string,
+ type: string,
+ subject: string,
+ plane: 'control' | 'data' = 'control'
+ ) {
return {
id,
datacontenttype: 'application/json',
time: new Date().toISOString(),
source: this.eventSource,
specversion: '1.0',
- plane: 'control',
+ plane,
subject,
type: `uk.nhs.notify.template-management.${type}.v${MAJOR_VERSION}`,
dataschema: `https://notify.nhs.uk/events/schemas/${type}/v${MAJOR_VERSION}.json`,
@@ -165,6 +172,45 @@ export class EventBuilder {
return event.data;
}
+ private buildProofRequestedEvent(
+ publishableEventRecord: PublishableEventRecord
+ ): Event | undefined {
+ if (!publishableEventRecord.dynamodb.NewImage) {
+ // No need to publish an event if a proof request has been deleted.
+ this.logger.debug({
+ description: 'No new image found',
+ publishableEventRecord,
+ });
+
+ return undefined;
+ }
+
+ const dynamoRecord = unmarshall(publishableEventRecord.dynamodb.NewImage);
+
+ const databaseProofRequest = $DynamoDBProofRequest.parse(dynamoRecord);
+
+ try {
+ return $Event.parse({
+ ...this.buildEventMetadata(
+ publishableEventRecord.eventID,
+ 'ProofRequested',
+ databaseProofRequest.id,
+ 'data'
+ ),
+ data: dynamoRecord,
+ });
+ } catch (error) {
+ this.logger
+ .child({
+ description: 'Failed to parse outgoing event for proof request',
+ publishableEventRecord,
+ })
+ .error(error);
+
+ throw error;
+ }
+ }
+
buildEvent(
publishableEventRecord: PublishableEventRecord
): Event | undefined {
@@ -175,6 +221,9 @@ export class EventBuilder {
case this.routingConfigTableName: {
return this.buildRoutingConfigDatabaseEvent(publishableEventRecord);
}
+ case this.proofRequestTableName: {
+ return this.buildProofRequestedEvent(publishableEventRecord);
+ }
default: {
this.logger.error({
description: 'Unrecognised event type',
diff --git a/lambdas/event-publisher/src/domain/input-schemas.ts b/lambdas/event-publisher/src/domain/input-schemas.ts
index b6b4bed70..51870ad46 100644
--- a/lambdas/event-publisher/src/domain/input-schemas.ts
+++ b/lambdas/event-publisher/src/domain/input-schemas.ts
@@ -53,6 +53,9 @@ export const $DynamoDBRoutingConfig = schemaFor>()(
);
export type DynamoDBRoutingConfig = z.infer;
+export const $DynamoDBProofRequest = z.object({ id: z.string() });
+export type DynamoDBProofRequest = z.infer;
+
// the lambda doesn't necessarily have to only accept inputs from a dynamodb stream via an
// eventbridge pipe, but that's all it is doing at the moment
export const $PublishableEventRecord = $DynamoDBStreamRecord;
diff --git a/lambdas/event-publisher/src/domain/output-schemas.ts b/lambdas/event-publisher/src/domain/output-schemas.ts
index e43f8b267..17c257c09 100644
--- a/lambdas/event-publisher/src/domain/output-schemas.ts
+++ b/lambdas/event-publisher/src/domain/output-schemas.ts
@@ -1,4 +1,5 @@
import {
+ $ProofRequestedEventV1,
$RoutingConfigCompletedEventV1,
$RoutingConfigDeletedEventV1,
$RoutingConfigDraftedEventV1,
@@ -9,11 +10,12 @@ import {
import { z } from 'zod';
export const $Event = z.discriminatedUnion('type', [
- $TemplateCompletedEventV1,
- $TemplateDraftedEventV1,
- $TemplateDeletedEventV1,
+ $ProofRequestedEventV1,
$RoutingConfigCompletedEventV1,
- $RoutingConfigDraftedEventV1,
$RoutingConfigDeletedEventV1,
+ $RoutingConfigDraftedEventV1,
+ $TemplateCompletedEventV1,
+ $TemplateDeletedEventV1,
+ $TemplateDraftedEventV1,
]);
export type Event = z.infer;
diff --git a/package-lock.json b/package-lock.json
index 41c037ffa..64b5dd942 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7719,13 +7719,13 @@
}
},
"node_modules/@aws-sdk/credential-provider-login": {
- "version": "3.972.18",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.18.tgz",
- "integrity": "sha512-kINzc5BBxdYBkPZ0/i1AMPMOk5b5QaFNbYMElVw5QTX13AKj6jcxnv/YNl9oW9mg+Y08ti19hh01HhyEAxsSJQ==",
+ "version": "3.972.19",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.19.tgz",
+ "integrity": "sha512-jOXdZ1o+CywQKr6gyxgxuUmnGwTTnY2Kxs1PM7fI6AYtDWDnmW/yKXayNqkF8KjP1unflqMWKVbVt5VgmE3L0g==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.973.19",
- "@aws-sdk/nested-clients": "^3.996.8",
+ "@aws-sdk/nested-clients": "^3.996.9",
"@aws-sdk/types": "^3.973.5",
"@smithy/property-provider": "^4.2.11",
"@smithy/protocol-http": "^5.3.11",
@@ -7826,9 +7826,9 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": {
- "version": "3.996.8",
- "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.8.tgz",
- "integrity": "sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA==",
+ "version": "3.996.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.9.tgz",
+ "integrity": "sha512-+RpVtpmQbbtzFOKhMlsRcXM/3f1Z49qTOHaA8gEpHOYruERmog6f2AUtf/oTRLCWjR9H2b3roqryV/hI7QMW8w==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
@@ -7842,7 +7842,7 @@
"@aws-sdk/types": "^3.973.5",
"@aws-sdk/util-endpoints": "^3.996.4",
"@aws-sdk/util-user-agent-browser": "^3.972.7",
- "@aws-sdk/util-user-agent-node": "^3.973.5",
+ "@aws-sdk/util-user-agent-node": "^3.973.6",
"@smithy/config-resolver": "^4.4.10",
"@smithy/core": "^3.23.9",
"@smithy/fetch-http-handler": "^5.3.13",
@@ -7932,15 +7932,16 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": {
- "version": "3.973.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.5.tgz",
- "integrity": "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw==",
+ "version": "3.973.6",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.6.tgz",
+ "integrity": "sha512-iF7G0prk7AvmOK64FcLvc/fW+Ty1H+vttajL7PvJFReU8urMxfYmynTTuFKDTA76Wgpq3FzTPKwabMQIXQHiXQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/middleware-user-agent": "^3.972.20",
"@aws-sdk/types": "^3.973.5",
"@smithy/node-config-provider": "^4.3.11",
"@smithy/types": "^4.13.0",
+ "@smithy/util-config-provider": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -7970,9 +7971,9 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws/lambda-invoke-store": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz",
- "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==",
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz",
+ "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
@@ -27883,7 +27884,7 @@
},
"packages/event-schemas": {
"name": "@nhsdigital/nhs-notify-event-schemas-template-management",
- "version": "1.4.4",
+ "version": "1.4.5",
"license": "MIT",
"dependencies": {
"zod": "^4.0.17"
diff --git a/packages/event-schemas/README.md b/packages/event-schemas/README.md
index 11e021206..b64b044f9 100644
--- a/packages/event-schemas/README.md
+++ b/packages/event-schemas/README.md
@@ -19,6 +19,10 @@ Then run `npm install @nhsdigital/nhs-notify-event-schemas-template-management`
## Events
+- `ProofRequested`
+- `RoutingConfigCompleted`
+- `RoutingConfigDeleted`
+- `RoutingConfigDrafted`
- `TemplateCompleted`
- `TemplateDeleted`
- `TemplateDrafted`
diff --git a/packages/event-schemas/__tests__/events/proof-requested.test.ts b/packages/event-schemas/__tests__/events/proof-requested.test.ts
new file mode 100644
index 000000000..ef72fb968
--- /dev/null
+++ b/packages/event-schemas/__tests__/events/proof-requested.test.ts
@@ -0,0 +1,26 @@
+/* eslint-disable security/detect-non-literal-fs-filename */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { $ProofRequestedEventV1 } from '../../src/events/proof-requested';
+
+const examplesDir = path.resolve(__dirname, '../../examples/ProofRequested/v1');
+
+describe('ProofRequestedEventV1 Zod schema', () => {
+ it.each(fs.readdirSync(examplesDir))(
+ 'parses sample event %s without errors',
+ (filename) => {
+ const event = JSON.parse(
+ fs.readFileSync(path.join(examplesDir, filename), 'utf8')
+ );
+
+ const result = $ProofRequestedEventV1.safeParse(event);
+
+ if (!result.success) {
+ console.log(result.error);
+ }
+
+ expect(result.success).toBe(true);
+ }
+ );
+});
diff --git a/packages/event-schemas/__tests__/json-schemas/proof-requested.test.ts b/packages/event-schemas/__tests__/json-schemas/proof-requested.test.ts
new file mode 100644
index 000000000..aac9ab0bb
--- /dev/null
+++ b/packages/event-schemas/__tests__/json-schemas/proof-requested.test.ts
@@ -0,0 +1,33 @@
+/* eslint-disable security/detect-non-literal-fs-filename */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { Ajv2020 } from 'ajv/dist/2020';
+import addFormats from 'ajv-formats';
+
+import ProofRequestedEventV1Schema from '../../schemas/ProofRequested/v1.json';
+
+const examplesDir = path.resolve(__dirname, '../../examples/ProofRequested/v1');
+
+describe('ProofRequestedEventV1 JSON schema', () => {
+ it.each(fs.readdirSync(examplesDir))(
+ 'parses sample event %s without errors',
+ (filename) => {
+ const event = JSON.parse(
+ fs.readFileSync(path.join(examplesDir, filename), 'utf8')
+ );
+
+ const ajv = new Ajv2020();
+ addFormats(ajv);
+ const validate = ajv.compile(ProofRequestedEventV1Schema);
+
+ const valid = validate(event);
+
+ if (!valid) {
+ console.log(validate.errors);
+ }
+
+ expect(valid).toBe(true);
+ }
+ );
+});
diff --git a/packages/event-schemas/examples/ProofRequested/v1/email.json b/packages/event-schemas/examples/ProofRequested/v1/email.json
new file mode 100644
index 000000000..63fa0783b
--- /dev/null
+++ b/packages/event-schemas/examples/ProofRequested/v1/email.json
@@ -0,0 +1,25 @@
+{
+ "data": {
+ "id": "c3d4e5f6-a7b8-4012-8def-123456789012",
+ "templateId": "8c7ae592-97cd-4900-897e-ef4794c8a745",
+ "templateType": "EMAIL",
+ "testPatientNhsNumber": "9000000009",
+ "contactDetails": {
+ "email": "test.patient@example.com"
+ },
+ "personalisation": {
+ "firstName": "Jane",
+ "surgeryName": "Test Surgery"
+ }
+ },
+ "datacontenttype": "application/json",
+ "dataschema": "https://notify.nhs.uk/events/schemas/ProofRequested/v1.json",
+ "dataschemaversion": "1.0.0",
+ "id": "b3c4d5e6-f7a8-9012-bcde-f12345678902",
+ "plane": "data",
+ "source": "//notify.nhs.uk/app/nhs-notify-template-management-prod/main",
+ "specversion": "1.0",
+ "subject": "c3d4e5f6-a7b8-4012-8def-123456789012",
+ "time": "2025-07-29T10:05:45.145Z",
+ "type": "uk.nhs.notify.template-management.ProofRequested.v1"
+}
diff --git a/packages/event-schemas/examples/ProofRequested/v1/nhsapp.json b/packages/event-schemas/examples/ProofRequested/v1/nhsapp.json
new file mode 100644
index 000000000..063600877
--- /dev/null
+++ b/packages/event-schemas/examples/ProofRequested/v1/nhsapp.json
@@ -0,0 +1,24 @@
+{
+ "data": {
+ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "templateId": "3a58a370-75ab-4788-a75e-cd2572a68523",
+ "templateType": "NHS_APP",
+ "testPatientNhsNumber": "9000000009",
+ "personalisation": {
+ "firstName": "Jane",
+ "appointmentTime": "10:00",
+ "appointmentDate": "2025-08-01",
+ "surgeryName": "Test Surgery"
+ }
+ },
+ "datacontenttype": "application/json",
+ "dataschema": "https://notify.nhs.uk/events/schemas/ProofRequested/v1.json",
+ "dataschemaversion": "1.0.0",
+ "id": "f1e2d3c4-b5a6-7890-fedc-ba0987654321",
+ "plane": "data",
+ "source": "//notify.nhs.uk/app/nhs-notify-template-management-prod/main",
+ "specversion": "1.0",
+ "subject": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "time": "2025-07-29T10:05:45.145Z",
+ "type": "uk.nhs.notify.template-management.ProofRequested.v1"
+}
diff --git a/packages/event-schemas/examples/ProofRequested/v1/sms.json b/packages/event-schemas/examples/ProofRequested/v1/sms.json
new file mode 100644
index 000000000..a80d05afc
--- /dev/null
+++ b/packages/event-schemas/examples/ProofRequested/v1/sms.json
@@ -0,0 +1,24 @@
+{
+ "data": {
+ "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
+ "templateId": "7b69c481-86bc-5899-b86f-de3683b79634",
+ "templateType": "SMS",
+ "testPatientNhsNumber": "9000000009",
+ "contactDetails": {
+ "sms": "07700900000"
+ },
+ "personalisation": {
+ "firstName": "Jane"
+ }
+ },
+ "datacontenttype": "application/json",
+ "dataschema": "https://notify.nhs.uk/events/schemas/ProofRequested/v1.json",
+ "dataschemaversion": "1.0.0",
+ "id": "a2b3c4d5-e6f7-8901-abcd-ef1234567891",
+ "plane": "data",
+ "source": "//notify.nhs.uk/app/nhs-notify-template-management-prod/main",
+ "specversion": "1.0",
+ "subject": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
+ "time": "2025-07-29T10:05:45.145Z",
+ "type": "uk.nhs.notify.template-management.ProofRequested.v1"
+}
diff --git a/packages/event-schemas/package.json b/packages/event-schemas/package.json
index 32a6a1f3c..60080f05b 100644
--- a/packages/event-schemas/package.json
+++ b/packages/event-schemas/package.json
@@ -56,5 +56,5 @@
},
"type": "commonjs",
"types": "./dist/index.d.ts",
- "version": "1.4.4"
+ "version": "1.4.5"
}
diff --git a/packages/event-schemas/schemas/ProofRequested/v1.json b/packages/event-schemas/schemas/ProofRequested/v1.json
new file mode 100644
index 000000000..2b34a0437
--- /dev/null
+++ b/packages/event-schemas/schemas/ProofRequested/v1.json
@@ -0,0 +1,127 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "maxLength": 1000,
+ "description": "Unique ID for this event"
+ },
+ "time": {
+ "type": "string",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$",
+ "description": "Time the event was generated"
+ },
+ "type": {
+ "type": "string",
+ "const": "uk.nhs.notify.template-management.ProofRequested.v1"
+ },
+ "source": {
+ "type": "string",
+ "description": "Source of the event"
+ },
+ "specversion": {
+ "type": "string",
+ "const": "1.0",
+ "description": "Version of the envelope event schema"
+ },
+ "datacontenttype": {
+ "type": "string",
+ "const": "application/json",
+ "description": "Always application/json"
+ },
+ "subject": {
+ "type": "string",
+ "format": "uuid",
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
+ "description": "Unique identifier for the item in the event data"
+ },
+ "dataschema": {
+ "type": "string",
+ "const": "https://notify.nhs.uk/events/schemas/ProofRequested/v1.json"
+ },
+ "dataschemaversion": {
+ "type": "string",
+ "pattern": "^1\\..*"
+ },
+ "plane": {
+ "type": "string",
+ "const": "data"
+ },
+ "data": {
+ "$ref": "#/$defs/ProofRequestEventData"
+ }
+ },
+ "required": [
+ "id",
+ "time",
+ "type",
+ "source",
+ "specversion",
+ "datacontenttype",
+ "subject",
+ "dataschema",
+ "dataschemaversion",
+ "plane",
+ "data"
+ ],
+ "$defs": {
+ "ProofRequestEventData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[\\dA-Fa-f]{8}(?:-[\\dA-Fa-f]{4}){3}-[\\dA-Fa-f]{12}$",
+ "description": "Unique identifier of the proof request"
+ },
+ "templateId": {
+ "type": "string",
+ "pattern": "^[\\dA-Fa-f]{8}(?:-[\\dA-Fa-f]{4}){3}-[\\dA-Fa-f]{12}$",
+ "description": "Unique identifier for the template being proofed"
+ },
+ "templateType": {
+ "type": "string",
+ "enum": [
+ "NHS_APP",
+ "SMS",
+ "EMAIL"
+ ],
+ "description": "Template type for the template being proofed"
+ },
+ "testPatientNhsNumber": {
+ "type": "string",
+ "description": "NHS number of test patient to use in the proofing request"
+ },
+ "contactDetails": {
+ "description": "Contact details to send the proof to",
+ "type": "object",
+ "properties": {
+ "sms": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ }
+ },
+ "personalisation": {
+ "description": "Personalisation fields to use in the proof",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "id",
+ "templateId",
+ "templateType",
+ "testPatientNhsNumber"
+ ]
+ }
+ },
+ "$id": "https://notify.nhs.uk/events/schemas/ProofRequested/v1.json"
+}
diff --git a/packages/event-schemas/scripts/generate-json-schemas.ts b/packages/event-schemas/scripts/generate-json-schemas.ts
index 72ef759b5..ef5f6eab3 100644
--- a/packages/event-schemas/scripts/generate-json-schemas.ts
+++ b/packages/event-schemas/scripts/generate-json-schemas.ts
@@ -3,6 +3,7 @@
import fs from 'node:fs';
import path from 'node:path';
import {
+ $ProofRequestedEventV1,
$TemplateCompletedEventV1,
$TemplateDeletedEventV1,
$TemplateDraftedEventV1,
@@ -62,6 +63,13 @@ function writeSchema(
);
}
+writeSchema(
+ 'ProofRequested',
+ $ProofRequestedEventV1,
+ '1',
+ 'https://notify.nhs.uk/events/schemas/ProofRequested/v1.json'
+);
+
writeSchema(
'TemplateCompleted',
$TemplateCompletedEventV1,
diff --git a/packages/event-schemas/src/events/index.ts b/packages/event-schemas/src/events/index.ts
index fe4096163..eb1cc3332 100644
--- a/packages/event-schemas/src/events/index.ts
+++ b/packages/event-schemas/src/events/index.ts
@@ -1,6 +1,7 @@
-export * from './template-completed';
-export * from './template-deleted';
-export * from './template-drafted';
+export * from './proof-requested';
export * from './routing-config-completed';
export * from './routing-config-deleted';
export * from './routing-config-drafted';
+export * from './template-completed';
+export * from './template-deleted';
+export * from './template-drafted';
diff --git a/packages/event-schemas/src/events/proof-requested.ts b/packages/event-schemas/src/events/proof-requested.ts
new file mode 100644
index 000000000..a31fc0280
--- /dev/null
+++ b/packages/event-schemas/src/events/proof-requested.ts
@@ -0,0 +1,15 @@
+import { z } from 'zod';
+import { $NHSNotifyEventEnvelope } from '../event-envelope';
+import { $ProofRequestEventData } from '../proof-request';
+
+export const $ProofRequestedEventV1 = $NHSNotifyEventEnvelope.extend({
+ type: z.literal('uk.nhs.notify.template-management.ProofRequested.v1'),
+ dataschema: z.literal(
+ 'https://notify.nhs.uk/events/schemas/ProofRequested/v1.json'
+ ),
+ dataschemaversion: z.string().startsWith('1.'),
+ plane: z.literal('data'),
+ data: $ProofRequestEventData,
+});
+
+export type ProofRequestedEventV1 = z.infer;
diff --git a/packages/event-schemas/src/proof-request.ts b/packages/event-schemas/src/proof-request.ts
new file mode 100644
index 000000000..6cfbf2168
--- /dev/null
+++ b/packages/event-schemas/src/proof-request.ts
@@ -0,0 +1,33 @@
+import { z } from 'zod';
+
+//eslint-disable-next-line security/detect-unsafe-regex
+const UUID_REGEX = /^[\dA-Fa-f]{8}(?:-[\dA-Fa-f]{4}){3}-[\dA-Fa-f]{12}$/;
+
+export const $ProofRequestEventData = z
+ .object({
+ id: z.string().regex(UUID_REGEX).meta({
+ description: 'Unique identifier of the proof request',
+ }),
+ templateId: z.string().regex(UUID_REGEX).meta({
+ description: 'Unique identifier for the template being proofed',
+ }),
+ templateType: z.enum(['NHS_APP', 'SMS', 'EMAIL']).meta({
+ description: 'Template type for the template being proofed',
+ }),
+ testPatientNhsNumber: z.string().meta({
+ description: 'NHS number of test patient to use in the proofing request',
+ }),
+ contactDetails: z
+ .object({
+ sms: z.string().optional(),
+ email: z.string().optional(),
+ })
+ .optional()
+ .meta({ description: 'Contact details to send the proof to' }),
+ personalisation: z.record(z.string(), z.string()).optional().meta({
+ description: 'Personalisation fields to use in the proof',
+ }),
+ })
+ .meta({
+ id: 'ProofRequestEventData',
+ });
diff --git a/scripts/config/vale/styles/config/vocabularies/words/accept.txt b/scripts/config/vale/styles/config/vocabularies/words/accept.txt
index 8d5dc5918..4824dbba9 100644
--- a/scripts/config/vale/styles/config/vocabularies/words/accept.txt
+++ b/scripts/config/vale/styles/config/vocabularies/words/accept.txt
@@ -27,3 +27,4 @@ Terraform
toolchain
Trufflehog
Zod
+[Vv]alidators
diff --git a/tests/test-team/global.d.ts b/tests/test-team/global.d.ts
index 6654bfe88..c8a6e126a 100644
--- a/tests/test-team/global.d.ts
+++ b/tests/test-team/global.d.ts
@@ -9,6 +9,7 @@ declare global {
ENVIRONMENT: string;
EVENTS_SNS_TOPIC_ARN: string;
PLAYWRIGHT_RUN_ID: string;
+ PROOF_REQUESTS_TABLE_NAME: string;
REQUEST_PROOF_QUEUE_URL: string;
ROUTING_CONFIG_TABLE_NAME: string;
SFTP_ENVIRONMENT: string;
diff --git a/tests/test-team/helpers/db/proof-requests-storage-helper.ts b/tests/test-team/helpers/db/proof-requests-storage-helper.ts
new file mode 100644
index 000000000..7d70a7fbb
--- /dev/null
+++ b/tests/test-team/helpers/db/proof-requests-storage-helper.ts
@@ -0,0 +1,106 @@
+import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
+import {
+ BatchWriteCommand,
+ DynamoDBDocumentClient,
+} from '@aws-sdk/lib-dynamodb';
+import type { ProofRequest } from '../types';
+
+type ProofRequestKey = { id: string; owner: string };
+
+export class ProofRequestsStorageHelper {
+ private readonly dynamo: DynamoDBDocumentClient = DynamoDBDocumentClient.from(
+ new DynamoDBClient({ region: 'eu-west-2' })
+ );
+
+ private seedData: ProofRequest[] = [];
+
+ private adHocKeys: ProofRequestKey[] = [];
+
+ /**
+ * Seed a load of proof requests into the database
+ */
+ async seed(data: ProofRequest[]) {
+ this.seedData.push(...data);
+
+ const chunks = ProofRequestsStorageHelper.chunk(data);
+
+ await Promise.all(
+ chunks.map(async (chunk) => {
+ await this.dynamo.send(
+ new BatchWriteCommand({
+ RequestItems: {
+ [process.env.PROOF_REQUESTS_TABLE_NAME]: chunk.map(
+ (proofRequest) => ({
+ PutRequest: {
+ Item: proofRequest,
+ },
+ })
+ ),
+ },
+ })
+ );
+ })
+ );
+ }
+
+ /**
+ * Delete proof requests seeded by calls to seed
+ */
+ public async deleteSeeded() {
+ await this.delete(
+ this.seedData.map(({ id, owner }) => ({
+ id,
+ owner,
+ }))
+ );
+ this.seedData = [];
+ }
+
+ private async delete(keys: ProofRequestKey[]) {
+ const dbChunks = ProofRequestsStorageHelper.chunk(keys);
+
+ await Promise.all(
+ dbChunks.map((chunk) =>
+ this.dynamo.send(
+ new BatchWriteCommand({
+ RequestItems: {
+ [process.env.PROOF_REQUESTS_TABLE_NAME]: chunk.map((key) => ({
+ DeleteRequest: {
+ Key: key,
+ },
+ })),
+ },
+ })
+ )
+ )
+ );
+ }
+
+ /**
+ * Stores references to proof requests created in tests (not via seeding)
+ */
+ public addAdHocKey(key: ProofRequestKey) {
+ this.adHocKeys.push(key);
+ }
+
+ /**
+ * Delete proof requests referenced by calls to addAdHocKey from database
+ */
+ async deleteAdHoc() {
+ await this.delete(this.adHocKeys);
+ this.adHocKeys = [];
+ }
+
+ /**
+ * Breaks a list into chunks of upto 25 items
+ */
+ private static chunk(list: T[], size = 25): T[][] {
+ const chunks: T[][] = [];
+
+ for (let i = 0; i < list.length; i += size) {
+ chunks.push(list.slice(i, i + size));
+ }
+
+ return chunks;
+ }
+}
diff --git a/tests/test-team/helpers/types.ts b/tests/test-team/helpers/types.ts
index 2fdf5ebfd..e2844598b 100644
--- a/tests/test-team/helpers/types.ts
+++ b/tests/test-team/helpers/types.ts
@@ -4,6 +4,7 @@ import type {
RoutingConfig,
Language,
LetterType,
+ TemplateType,
} from 'nhs-notify-web-template-management-types';
export const templateTypeDisplayMappings: Record = {
@@ -133,3 +134,19 @@ export type FactoryRoutingConfigWithModifiers = FactoryRoutingConfig & {
templateId?: string
) => FactoryRoutingConfigWithModifiers;
};
+
+type DigitalTemplateType = Extract;
+
+export type ProofRequest = {
+ id: string;
+ owner: string;
+ createdAt: string;
+ personalisation: Record;
+ contactDetails?: {
+ sms?: string;
+ email?: string;
+ };
+ templateId: string;
+ templateType: DigitalTemplateType;
+ testPatientNhsNumber: string;
+};
diff --git a/tests/test-team/template-mgmt-event-tests/proof-requests.event.spec.ts b/tests/test-team/template-mgmt-event-tests/proof-requests.event.spec.ts
new file mode 100644
index 000000000..45a229aab
--- /dev/null
+++ b/tests/test-team/template-mgmt-event-tests/proof-requests.event.spec.ts
@@ -0,0 +1,81 @@
+import { randomUUID } from 'node:crypto';
+import {
+ templateManagementEventSubscriber as test,
+ expect,
+} from '../fixtures/template-management-event-subscriber';
+import {
+ createAuthHelper,
+ type TestUser,
+ testUsers,
+} from '../helpers/auth/cognito-auth-helper';
+import { eventWithId } from '../helpers/events/matchers';
+import { ProofRequestsStorageHelper } from 'helpers/db/proof-requests-storage-helper';
+
+test.describe('ProofRequestedEvent', () => {
+ const authHelper = createAuthHelper();
+ const proofRequestsStorageHelper = new ProofRequestsStorageHelper();
+
+ let user: TestUser;
+
+ test.beforeAll(async () => {
+ user = await authHelper.getTestUser(testUsers.User1.userId);
+ });
+
+ test.afterAll(async () => {
+ await proofRequestsStorageHelper.deleteSeeded();
+ });
+
+ test('Expect a ProofRequestedEventv1 to be published when a proof request is created', async ({
+ eventSubscriber,
+ }) => {
+ const start = new Date();
+
+ const proofRequestId = randomUUID();
+
+ // TODO: CCM-7941 - use API rather than directly into DB.
+ await proofRequestsStorageHelper.seed([
+ {
+ id: proofRequestId,
+ owner: `CLIENT#${user.clientId}`,
+ createdAt: new Date().toISOString(),
+ personalisation: {
+ gpSurgery: 'Test GP Surgery',
+ },
+ contactDetails: {
+ sms: '07999999999',
+ },
+ templateId: randomUUID(),
+ templateType: 'SMS',
+ testPatientNhsNumber: '9999999999',
+ },
+ ]);
+
+ await expect(async () => {
+ const events = await eventSubscriber.receive({
+ since: start,
+ match: eventWithId(proofRequestId),
+ });
+
+ expect(events).toHaveLength(1);
+
+ expect(events).toContainEqual(
+ expect.objectContaining({
+ record: expect.objectContaining({
+ type: 'uk.nhs.notify.template-management.ProofRequested.v1',
+ data: expect.objectContaining({
+ id: proofRequestId,
+ testPatientNhsNumber: '9999999999',
+ templateType: 'SMS',
+ personalisation: {
+ gpSurgery: 'Test GP Surgery',
+ },
+ contactDetails: {
+ sms: '07999999999',
+ },
+ }),
+ }),
+ })
+ );
+ }).toPass({ timeout: 60_000, intervals: [1000, 3000, 5000] });
+ });
+});
diff --git a/utils/backend-config/src/backend-config.ts b/utils/backend-config/src/backend-config.ts
index 6dace6a49..edbd1cab1 100644
--- a/utils/backend-config/src/backend-config.ts
+++ b/utils/backend-config/src/backend-config.ts
@@ -21,6 +21,7 @@ export type BackendConfig = {
testEmailBucketPrefix: string;
userPoolId: string;
userPoolClientId: string;
+ proofRequestsTableName: string;
};
export const BackendConfigHelper = {
@@ -31,6 +32,7 @@ export const BackendConfigHelper = {
clientSsmPathPrefix: process.env.CLIENT_SSM_PATH_PREFIX ?? '',
environment: process.env.ENVIRONMENT ?? '',
eventsSnsTopicArn: process.env.EVENTS_SNS_TOPIC_ARN ?? '',
+ proofRequestsTableName: process.env.PROOF_REQUESTS_TABLE_NAME ?? '',
requestProofQueueUrl: process.env.REQUEST_PROOF_QUEUE_URL ?? '',
routingConfigTableName: process.env.ROUTING_CONFIG_TABLE_NAME ?? '',
sftpEnvironment: process.env.SFTP_ENVIRONMENT ?? '',
@@ -72,6 +74,7 @@ export const BackendConfigHelper = {
process.env.SFTP_POLL_LAMBDA_NAME = config.sftpPollLambdaName;
process.env.TEST_EMAIL_BUCKET_NAME = config.testEmailBucketName;
process.env.TEST_EMAIL_BUCKET_PREFIX = config.testEmailBucketPrefix;
+ process.env.PROOF_REQUESTS_TABLE_NAME = config.proofRequestsTableName;
},
fromTerraformOutputsFile(filepath: string): BackendConfig {
@@ -85,6 +88,8 @@ export const BackendConfigHelper = {
outputsFileContent.client_ssm_path_prefix?.value ?? '',
environment: deployment.environment ?? '',
eventsSnsTopicArn: outputsFileContent.events_sns_topic_arn?.value ?? '',
+ proofRequestsTableName:
+ outputsFileContent.proof_requests_table_name?.value ?? '',
requestProofQueueUrl:
outputsFileContent.request_proof_queue_url?.value ?? '',
routingConfigTableName: