diff --git a/.gitleaksignore b/.gitleaksignore
index f6971326..bb8000c0 100644
--- a/.gitleaksignore
+++ b/.gitleaksignore
@@ -21,3 +21,5 @@ e12407e09151898bfd8d049d57eee9db9977d56b:.github/copilot-instructions.md:generic
82cf3b2e89ea24b97c4ffc09e618700fb1b0aff3:pact-contracts/pacts/letter-rendering/supplier-api-letter-request-prepared.json:generic-api-key:10
82f6be3e657b46d8447e77cdc1894fba0b855c26:tests/component-tests/testCases/create-letter-request.spec.ts:generic-api-key:10
debc75a97cfe551a69fd1e8694be483213322a9d:pact-contracts/pacts/letter-rendering/supplier-api-letter-request-prepared.json:generic-api-key:10
+777eb4047ad06b9e939a292ee18664a0ffee4f29:tests/resources/prepared-letter.json:generic-api-key:4
+4fa1923947bbff2387218d698d766cbb7c121a0f:pact-contracts/pacts/letter-rendering/supplier-api-letter-request-prepared.json:generic-api-key:10
diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md
index 202237b5..28d83bcc 100644
--- a/infrastructure/terraform/components/api/README.md
+++ b/infrastructure/terraform/components/api/README.md
@@ -38,6 +38,8 @@ No requirements.
| Name | Source | Version |
|------|--------|---------|
+| [allocation\_lambda](#module\_allocation\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip | n/a |
+| [amendments\_queue](#module\_amendments\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a |
| [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| [domain\_truststore](#module\_domain\_truststore) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-s3bucket.zip | n/a |
| [eventpub](#module\_eventpub) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-eventpub.zip | n/a |
@@ -48,14 +50,15 @@ No requirements.
| [get\_status](#module\_get\_status) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-kms.zip | n/a |
| [letter\_status\_update](#module\_letter\_status\_update) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
-| [letter\_status\_updates\_queue](#module\_letter\_status\_updates\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a |
| [letter\_updates\_transformer](#module\_letter\_updates\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| [logging\_bucket](#module\_logging\_bucket) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-s3bucket.zip | n/a |
| [patch\_letter](#module\_patch\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| [post\_letters](#module\_post\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| [post\_mi](#module\_post\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| [s3bucket\_test\_letters](#module\_s3bucket\_test\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-s3bucket.zip | n/a |
-| [sqs\_letter\_updates](#module\_sqs\_letter\_updates) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a |
+| [supplier\_events\_forwarder\_lambda](#module\_supplier\_events\_forwarder\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip | n/a |
+| [supplier\_events\_queue](#module\_supplier\_events\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a |
+| [supplier\_requests\_queue](#module\_supplier\_requests\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a |
| [supplier\_ssl](#module\_supplier\_ssl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-ssl.zip | n/a |
| [upsert\_letter](#module\_upsert\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
## Outputs
diff --git a/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf b/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf
index ab3634c4..e9463793 100644
--- a/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf
+++ b/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf
@@ -1,12 +1,11 @@
resource "aws_lambda_event_source_mapping" "status_updates_sqs_to_status_update_handler" {
- event_source_arn = module.letter_status_updates_queue.sqs_queue_arn
+ event_source_arn = module.supplier_requests_queue.sqs_queue_arn
function_name = module.letter_status_update.function_arn
batch_size = 10
- maximum_batching_window_in_seconds = 1
scaling_config { maximum_concurrency = 10 }
depends_on = [
- module.letter_status_updates_queue, # ensures queue exists
- module.letter_status_update # ensures update handler exists
+ module.supplier_requests_queue, # ensures queue exists
+ module.letter_status_update # ensures update handler exists
]
}
diff --git a/infrastructure/terraform/components/api/lambda_event_source_mapping_upsert_letter.tf b/infrastructure/terraform/components/api/lambda_event_source_mapping_upsert_letter.tf
index a592ea9e..f4d6ad7f 100644
--- a/infrastructure/terraform/components/api/lambda_event_source_mapping_upsert_letter.tf
+++ b/infrastructure/terraform/components/api/lambda_event_source_mapping_upsert_letter.tf
@@ -1,8 +1,7 @@
resource "aws_lambda_event_source_mapping" "upsert_letter" {
- event_source_arn = module.sqs_letter_updates.sqs_queue_arn
+ event_source_arn = module.amendments_queue.sqs_queue_arn
function_name = module.upsert_letter.function_name
batch_size = 10
- maximum_batching_window_in_seconds = 5
function_response_types = [
"ReportBatchItemFailures"
]
diff --git a/infrastructure/terraform/components/api/module_lambda_allocation.tf b/infrastructure/terraform/components/api/module_lambda_allocation.tf
new file mode 100644
index 00000000..4db8d4d6
--- /dev/null
+++ b/infrastructure/terraform/components/api/module_lambda_allocation.tf
@@ -0,0 +1,72 @@
+module "allocation_lambda" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip"
+
+ function_name = "allocate_supplier"
+ description = "Lambda function for allocating supplier"
+
+ aws_account_id = var.aws_account_id
+ component = var.component
+ environment = var.environment
+ project = var.project
+ region = var.region
+ group = var.group
+
+ log_retention_in_days = var.log_retention_in_days
+ kms_key_arn = module.kms.key_arn
+
+ iam_policy_document = {
+ body = data.aws_iam_policy_document.allocation_lambda.json
+ }
+
+ function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
+ function_code_base_path = local.aws_lambda_functions_dir_path
+ function_code_dir = "allocation/dist"
+ function_include_common = true
+ handler_function_name = "handler"
+ runtime = "nodejs22.x"
+ memory = 128
+ timeout = 29
+ log_level = var.log_level
+
+ force_lambda_code_deploy = var.force_lambda_code_deploy
+ enable_lambda_insights = false
+
+ send_to_firehose = true
+ log_destination_arn = local.destination_arn
+ log_subscription_role_arn = local.acct.log_subscription_role_arn
+
+ lambda_env_vars = {
+ QUEUE_URL = module.amendments_queue.sqs_queue_url
+ }
+}
+
+
+data "aws_iam_policy_document" "allocation_lambda" {
+ statement {
+ sid = "KMSPermissions"
+ effect = "Allow"
+
+ actions = [
+ "kms:Decrypt",
+ "kms:GenerateDataKey",
+ ]
+
+ resources = [
+ module.kms.key_arn,
+ ]
+ }
+
+ statement {
+ sid = "AllowQueueAccess"
+ effect = "Allow"
+
+ actions = [
+ "sqs:SendMessage",
+ "sqs:GetQueueAttributes",
+ ]
+
+ resources = [
+ module.amendments_queue.sqs_queue_arn
+ ]
+ }
+}
diff --git a/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf b/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf
index 59393bd2..d01b0c58 100644
--- a/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf
+++ b/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf
@@ -79,7 +79,7 @@ data "aws_iam_policy_document" "letter_status_update" {
]
resources = [
- module.letter_status_updates_queue.sqs_queue_arn
+ module.supplier_requests_queue.sqs_queue_arn
]
}
}
diff --git a/infrastructure/terraform/components/api/module_lambda_patch_letter.tf b/infrastructure/terraform/components/api/module_lambda_patch_letter.tf
index b09c303f..41148490 100644
--- a/infrastructure/terraform/components/api/module_lambda_patch_letter.tf
+++ b/infrastructure/terraform/components/api/module_lambda_patch_letter.tf
@@ -35,7 +35,7 @@ module "patch_letter" {
log_subscription_role_arn = local.acct.log_subscription_role_arn
lambda_env_vars = merge(local.common_lambda_env_vars, {
- QUEUE_URL = module.letter_status_updates_queue.sqs_queue_url
+ QUEUE_URL = module.supplier_requests_queue.sqs_queue_url
})
}
@@ -64,7 +64,7 @@ data "aws_iam_policy_document" "patch_letter_lambda" {
]
resources = [
- module.letter_status_updates_queue.sqs_queue_arn
+ module.supplier_requests_queue.sqs_queue_arn
]
}
}
diff --git a/infrastructure/terraform/components/api/module_lambda_post_letters.tf b/infrastructure/terraform/components/api/module_lambda_post_letters.tf
index 8eb8ad52..5199cb34 100644
--- a/infrastructure/terraform/components/api/module_lambda_post_letters.tf
+++ b/infrastructure/terraform/components/api/module_lambda_post_letters.tf
@@ -35,7 +35,7 @@ module "post_letters" {
log_subscription_role_arn = local.acct.log_subscription_role_arn
lambda_env_vars = merge(local.common_lambda_env_vars, {
- QUEUE_URL = module.letter_status_updates_queue.sqs_queue_url,
+ QUEUE_URL = module.supplier_requests_queue.sqs_queue_url,
MAX_LIMIT = var.max_get_limit
})
}
@@ -65,7 +65,7 @@ data "aws_iam_policy_document" "post_letters" {
]
resources = [
- module.letter_status_updates_queue.sqs_queue_arn
+ module.supplier_requests_queue.sqs_queue_arn
]
}
}
diff --git a/infrastructure/terraform/components/api/module_lambda_supplier_events_forwarder.tf b/infrastructure/terraform/components/api/module_lambda_supplier_events_forwarder.tf
new file mode 100644
index 00000000..524d67be
--- /dev/null
+++ b/infrastructure/terraform/components/api/module_lambda_supplier_events_forwarder.tf
@@ -0,0 +1,86 @@
+module "supplier_events_forwarder_lambda" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip"
+
+ function_name = "supplier_events_forwarder"
+ description = "Lambda function for forwarding supplier events to Firehose"
+
+ aws_account_id = var.aws_account_id
+ component = var.component
+ environment = var.environment
+ project = var.project
+ region = var.region
+ group = var.group
+
+ log_retention_in_days = var.log_retention_in_days
+ kms_key_arn = module.kms.key_arn
+
+ iam_policy_document = {
+ body = data.aws_iam_policy_document.supplier_events_forwarder_lambda.json
+ }
+
+ function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
+ function_code_base_path = local.aws_lambda_functions_dir_path
+ function_code_dir = "supplier-events-forwarder/dist"
+ function_include_common = true
+ handler_function_name = "handler"
+ runtime = "nodejs22.x"
+ memory = 128
+ timeout = 29
+ log_level = var.log_level
+
+ force_lambda_code_deploy = var.force_lambda_code_deploy
+ enable_lambda_insights = false
+
+ send_to_firehose = true
+ log_destination_arn = local.destination_arn
+ log_subscription_role_arn = local.acct.log_subscription_role_arn
+
+ lambda_env_vars = {
+ FIREHOSE_DELIVERY_STREAM_NAME = module.eventsub.firehose_delivery_stream.name
+ }
+}
+
+data "aws_iam_policy_document" "supplier_events_forwarder_lambda" {
+ statement {
+ sid = "KMSPermissions"
+ effect = "Allow"
+
+ actions = [
+ "kms:Decrypt",
+ "kms:GenerateDataKey",
+ ]
+
+ resources = [
+ module.kms.key_arn,
+ ]
+ }
+
+ statement {
+ sid = "FirehosePermissions"
+ effect = "Allow"
+
+ actions = [
+ "firehose:PutRecord",
+ "firehose:PutRecordBatch",
+ ]
+
+ resources = [
+ module.eventsub.firehose_delivery_stream.arn,
+ ]
+ }
+
+ statement {
+ sid = "SQSPermissions"
+ effect = "Allow"
+
+ actions = [
+ "sqs:ReceiveMessage",
+ "sqs:DeleteMessage",
+ "sqs:GetQueueAttributes",
+ ]
+
+ resources = [
+ module.supplier_events_queue.sqs_queue_arn,
+ ]
+ }
+}
diff --git a/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf b/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf
index b4a4278b..15723ac3 100644
--- a/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf
+++ b/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf
@@ -77,7 +77,7 @@ data "aws_iam_policy_document" "upsert_letter_lambda" {
]
resources = [
- module.sqs_letter_updates.sqs_queue_arn
+ module.amendments_queue.sqs_queue_arn
]
}
}
diff --git a/infrastructure/terraform/components/api/module_sqs_amendments_queue.tf b/infrastructure/terraform/components/api/module_sqs_amendments_queue.tf
new file mode 100644
index 00000000..ce9b6977
--- /dev/null
+++ b/infrastructure/terraform/components/api/module_sqs_amendments_queue.tf
@@ -0,0 +1,47 @@
+module "amendments_queue" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip"
+
+ aws_account_id = var.aws_account_id
+ component = var.component
+ environment = var.environment
+ project = var.project
+ region = var.region
+ name = "${local.csi}-amendments-queue"
+
+ fifo_queue = true
+ content_based_deduplication = true
+
+ sqs_kms_key_arn = module.kms.key_arn
+
+ visibility_timeout_seconds = 60
+
+ create_dlq = true
+ sqs_policy_overload = data.aws_iam_policy_document.amendments_queue_policy.json
+}
+
+data "aws_iam_policy_document" "amendments_queue_policy" {
+ version = "2012-10-17"
+ statement {
+ sid = "AllowSNSToSendMessage"
+ effect = "Allow"
+
+ principals {
+ type = "Service"
+ identifiers = ["sns.amazonaws.com"]
+ }
+
+ actions = [
+ "sqs:SendMessage"
+ ]
+
+ resources = [
+ "arn:aws:sqs:${var.region}:${var.aws_account_id}:${local.csi}-amendments-queue.fifo"
+ ]
+
+ condition {
+ test = "ArnEquals"
+ variable = "aws:SourceArn"
+ values = [module.eventsub.sns_topic_supplier.arn]
+ }
+ }
+}
diff --git a/infrastructure/terraform/components/api/module_sqs_letter_updates.tf b/infrastructure/terraform/components/api/module_sqs_letter_updates.tf
deleted file mode 100644
index 472afb81..00000000
--- a/infrastructure/terraform/components/api/module_sqs_letter_updates.tf
+++ /dev/null
@@ -1,71 +0,0 @@
-module "sqs_letter_updates" {
- source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip"
-
- aws_account_id = var.aws_account_id
- component = var.component
- environment = var.environment
- project = var.project
- region = var.region
- name = "letter-updates"
-
- sqs_kms_key_arn = module.kms.key_arn
-
- visibility_timeout_seconds = 60
-
- create_dlq = true
- sqs_policy_overload = data.aws_iam_policy_document.letter_updates_queue_policy.json
-}
-
-data "aws_iam_policy_document" "letter_updates_queue_policy" {
- version = "2012-10-17"
- statement {
- sid = "AllowSNSToSendMessage"
- effect = "Allow"
-
- principals {
- type = "Service"
- identifiers = ["sns.amazonaws.com"]
- }
-
- actions = [
- "sqs:SendMessage"
- ]
-
- resources = [
- "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-letter-updates-queue"
- ]
-
- condition {
- test = "ArnEquals"
- variable = "aws:SourceArn"
- values = [module.eventsub.sns_topic.arn]
- }
- }
-
- statement {
- sid = "AllowSNSPermissions"
- effect = "Allow"
-
- principals {
- type = "Service"
- identifiers = ["sns.amazonaws.com"]
- }
-
- actions = [
- "sqs:SendMessage",
- "sqs:ListQueueTags",
- "sqs:GetQueueUrl",
- "sqs:GetQueueAttributes",
- ]
-
- resources = [
- "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-letter-updates-queue"
- ]
-
- condition {
- test = "ArnEquals"
- variable = "aws:SourceArn"
- values = [module.eventsub.sns_topic.arn]
- }
- }
-}
diff --git a/infrastructure/terraform/components/api/module_sqs_supplier_events_queue.tf b/infrastructure/terraform/components/api/module_sqs_supplier_events_queue.tf
new file mode 100644
index 00000000..c0255d46
--- /dev/null
+++ b/infrastructure/terraform/components/api/module_sqs_supplier_events_queue.tf
@@ -0,0 +1,47 @@
+module "supplier_events_queue" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip"
+
+ aws_account_id = var.aws_account_id
+ component = var.component
+ environment = var.environment
+ project = var.project
+ region = var.region
+ name = "${local.csi}-supplier-events-queue"
+
+ fifo_queue = true
+ content_based_deduplication = true
+
+ sqs_kms_key_arn = module.kms.key_arn
+
+ visibility_timeout_seconds = 60
+
+ create_dlq = true
+ sqs_policy_overload = data.aws_iam_policy_document.supplier_events_queue_policy.json
+}
+
+data "aws_iam_policy_document" "supplier_events_queue_policy" {
+ version = "2012-10-17"
+ statement {
+ sid = "AllowSNSToSendMessage"
+ effect = "Allow"
+
+ principals {
+ type = "Service"
+ identifiers = ["sns.amazonaws.com"]
+ }
+
+ actions = [
+ "sqs:SendMessage"
+ ]
+
+ resources = [
+ "arn:aws:sqs:${var.region}:${var.aws_account_id}:${local.csi}-supplier-events-queue.fifo"
+ ]
+
+ condition {
+ test = "ArnEquals"
+ variable = "aws:SourceArn"
+ values = [module.eventsub.sns_topic_supplier.arn]
+ }
+ }
+}
diff --git a/infrastructure/terraform/components/api/module_sqs_letter_status_updates.tf b/infrastructure/terraform/components/api/module_sqs_supplier_requests_queue.tf
similarity index 71%
rename from infrastructure/terraform/components/api/module_sqs_letter_status_updates.tf
rename to infrastructure/terraform/components/api/module_sqs_supplier_requests_queue.tf
index a604faaf..0fbdc1bc 100644
--- a/infrastructure/terraform/components/api/module_sqs_letter_status_updates.tf
+++ b/infrastructure/terraform/components/api/module_sqs_supplier_requests_queue.tf
@@ -1,8 +1,8 @@
# Queue to transport update letter status messages
-module "letter_status_updates_queue" {
+module "supplier_requests_queue" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip"
- name = "letter_status_updates_queue"
+ name = "${local.csi}-supplier-requests-queue"
aws_account_id = var.aws_account_id
component = var.component
@@ -10,6 +10,9 @@ module "letter_status_updates_queue" {
project = var.project
region = var.region
+ fifo_queue = true
+ content_based_deduplication = true
+
sqs_kms_key_arn = module.kms.key_arn
create_dlq = true
diff --git a/infrastructure/terraform/components/api/sns_topic_subscription_allocation_lambda.tf b/infrastructure/terraform/components/api/sns_topic_subscription_allocation_lambda.tf
new file mode 100644
index 00000000..91fc7ae4
--- /dev/null
+++ b/infrastructure/terraform/components/api/sns_topic_subscription_allocation_lambda.tf
@@ -0,0 +1,13 @@
+resource "aws_sns_topic_subscription" "allocation_lambda" {
+ topic_arn = module.eventsub.sns_topic_event_bus.arn
+ protocol = "lambda"
+ endpoint = module.allocation_lambda.function_arn
+}
+
+resource "aws_lambda_permission" "allocation_lambda_sns" {
+ statement_id = "AllowExecutionFromSNS"
+ action = "lambda:InvokeFunction"
+ function_name = module.allocation_lambda.function_name
+ principal = "sns.amazonaws.com"
+ source_arn = module.eventsub.sns_topic_event_bus.arn
+}
diff --git a/infrastructure/terraform/components/api/sns_topic_subscription_amendments_queue.tf b/infrastructure/terraform/components/api/sns_topic_subscription_amendments_queue.tf
new file mode 100644
index 00000000..e609684d
--- /dev/null
+++ b/infrastructure/terraform/components/api/sns_topic_subscription_amendments_queue.tf
@@ -0,0 +1,6 @@
+resource "aws_sns_topic_subscription" "amendments_queue" {
+ topic_arn = module.eventsub.sns_topic_supplier.arn
+ protocol = "sqs"
+ endpoint = module.amendments_queue.sqs_queue_arn
+ raw_message_delivery = false
+}
diff --git a/infrastructure/terraform/components/api/sns_topic_subscription_eventsub_sqs_letter_updates.tf b/infrastructure/terraform/components/api/sns_topic_subscription_eventsub_sqs_letter_updates.tf
deleted file mode 100644
index 9c232c14..00000000
--- a/infrastructure/terraform/components/api/sns_topic_subscription_eventsub_sqs_letter_updates.tf
+++ /dev/null
@@ -1,5 +0,0 @@
-resource "aws_sns_topic_subscription" "eventsub_sqs_letter_updates" {
- topic_arn = module.eventsub.sns_topic.arn
- protocol = "sqs"
- endpoint = module.sqs_letter_updates.sqs_queue_arn
-}
diff --git a/infrastructure/terraform/components/api/sns_topic_subscription_supplier_events_forwarder_lambda.tf b/infrastructure/terraform/components/api/sns_topic_subscription_supplier_events_forwarder_lambda.tf
new file mode 100644
index 00000000..e8e3c01d
--- /dev/null
+++ b/infrastructure/terraform/components/api/sns_topic_subscription_supplier_events_forwarder_lambda.tf
@@ -0,0 +1,18 @@
+resource "aws_sns_topic_subscription" "supplier_events_queue" {
+ topic_arn = module.eventsub.sns_topic_supplier.arn
+ protocol = "sqs"
+ endpoint = module.supplier_events_queue.sqs_queue_arn
+ raw_message_delivery = false
+}
+
+resource "aws_lambda_event_source_mapping" "supplier_events_forwarder" {
+ event_source_arn = module.supplier_events_queue.sqs_queue_arn
+ function_name = module.supplier_events_forwarder_lambda.function_arn
+ batch_size = 10
+ scaling_config { maximum_concurrency = 10 }
+
+ depends_on = [
+ module.supplier_events_queue,
+ module.supplier_events_forwarder_lambda
+ ]
+}
diff --git a/infrastructure/terraform/modules/eventsub/README.md b/infrastructure/terraform/modules/eventsub/README.md
index a5653fda..ea09ed78 100644
--- a/infrastructure/terraform/modules/eventsub/README.md
+++ b/infrastructure/terraform/modules/eventsub/README.md
@@ -39,8 +39,10 @@
| Name | Description |
|------|-------------|
+| [firehose\_delivery\_stream](#output\_firehose\_delivery\_stream) | Kinesis Firehose Delivery Stream ARN and Name |
| [s3\_bucket\_event\_cache](#output\_s3\_bucket\_event\_cache) | S3 Bucket ARN and Name for event cache |
-| [sns\_topic](#output\_sns\_topic) | SNS Topic ARN and Name |
+| [sns\_topic\_event\_bus](#output\_sns\_topic\_event\_bus) | SNS Topic ARN and Name |
+| [sns\_topic\_supplier](#output\_sns\_topic\_supplier) | SNS Topic ARN and Name |
diff --git a/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf b/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf
index e8ef1249..f174026f 100644
--- a/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf
+++ b/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf
@@ -11,6 +11,6 @@ resource "aws_cloudwatch_metric_alarm" "sns_delivery_failures" {
treat_missing_data = "notBreaching"
dimensions = {
- TopicName = aws_sns_topic.main.name
+ TopicName = aws_sns_topic.sns_topic_event_bus.name
}
}
diff --git a/infrastructure/terraform/modules/eventsub/outputs.tf b/infrastructure/terraform/modules/eventsub/outputs.tf
index e2ff3b38..6ddc8ef0 100644
--- a/infrastructure/terraform/modules/eventsub/outputs.tf
+++ b/infrastructure/terraform/modules/eventsub/outputs.tf
@@ -1,11 +1,27 @@
-output "sns_topic" {
+output "sns_topic_event_bus" {
description = "SNS Topic ARN and Name"
value = {
- arn = aws_sns_topic.main.arn
- name = aws_sns_topic.main.name
+ arn = aws_sns_topic.sns_topic_event_bus.arn
+ name = aws_sns_topic.sns_topic_event_bus.name
}
}
+output "sns_topic_supplier" {
+ description = "SNS Topic ARN and Name"
+ value = {
+ arn = aws_sns_topic.sns_topic_supplier.arn
+ name = aws_sns_topic.sns_topic_supplier.name
+ }
+}
+
+output "firehose_delivery_stream" {
+ description = "Kinesis Firehose Delivery Stream ARN and Name"
+ value = var.enable_event_cache ? {
+ arn = aws_kinesis_firehose_delivery_stream.main[0].arn
+ name = aws_kinesis_firehose_delivery_stream.main[0].name
+ } : {}
+}
+
output "s3_bucket_event_cache" {
description = "S3 Bucket ARN and Name for event cache"
value = var.enable_event_cache ? {
diff --git a/infrastructure/terraform/modules/eventsub/sns_topic.tf b/infrastructure/terraform/modules/eventsub/sns_topic_event_bus.tf
similarity index 95%
rename from infrastructure/terraform/modules/eventsub/sns_topic.tf
rename to infrastructure/terraform/modules/eventsub/sns_topic_event_bus.tf
index cc30db15..28299fe7 100644
--- a/infrastructure/terraform/modules/eventsub/sns_topic.tf
+++ b/infrastructure/terraform/modules/eventsub/sns_topic_event_bus.tf
@@ -1,5 +1,5 @@
-resource "aws_sns_topic" "main" {
- name = local.csi
+resource "aws_sns_topic" "sns_topic_event_bus" {
+ name = "${local.csi}-event-bus-events"
kms_master_key_id = var.kms_key_arn
application_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
diff --git a/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf b/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf
index a772e9e7..0a396baf 100644
--- a/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf
+++ b/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf
@@ -1,14 +1,78 @@
-resource "aws_sns_topic_policy" "main" {
- arn = aws_sns_topic.main.arn
+resource "aws_sns_topic_policy" "sns_topic_event_bus" {
+ arn = aws_sns_topic.sns_topic_event_bus.arn
- policy = data.aws_iam_policy_document.sns_topic_policy.json
+ policy = data.aws_iam_policy_document.sns_topic_event_bus_policy.json
}
-data "aws_iam_policy_document" "sns_topic_policy" {
+resource "aws_sns_topic_policy" "sns_topic_supplier" {
+ arn = aws_sns_topic.sns_topic_supplier.arn
+
+ policy = data.aws_iam_policy_document.sns_topic_supplier_policy.json
+}
+
+data "aws_iam_policy_document" "sns_topic_event_bus_policy" {
+ policy_id = "__default_policy_ID"
+
+ statement {
+ sid = "AllowAllSNSActionsFromAccount"
+ effect = "Allow"
+
+ principals {
+ type = "AWS"
+ identifiers = ["*"]
+ }
+
+ actions = [
+ "SNS:Subscribe",
+ "SNS:SetTopicAttributes",
+ "SNS:RemovePermission",
+ "SNS:Receive",
+ "SNS:Publish",
+ "SNS:ListSubscriptionsByTopic",
+ "SNS:GetTopicAttributes",
+ "SNS:DeleteTopic",
+ "SNS:AddPermission",
+ ]
+
+ resources = [
+ aws_sns_topic.sns_topic_event_bus.arn,
+ ]
+
+ condition {
+ test = "StringEquals"
+ variable = "AWS:SourceOwner"
+
+ values = [
+ var.aws_account_id,
+ ]
+ }
+ }
+
+ statement {
+ sid = "AllowAllSNSActionsFromSharedAccount"
+ effect = "Allow"
+ actions = [
+ "SNS:Publish",
+ ]
+
+ principals {
+ type = "AWS"
+ identifiers = [
+ "arn:aws:iam::${var.shared_infra_account_id}:root"
+ ]
+ }
+
+ resources = [
+ aws_sns_topic.sns_topic_event_bus.arn,
+ ]
+ }
+}
+
+data "aws_iam_policy_document" "sns_topic_supplier_policy" {
policy_id = "__default_policy_ID"
statement {
- sid = "AllowAllSNSActionsFromAccount"
+ sid = "AllowAllSNSActionsFromAccount"
effect = "Allow"
principals {
@@ -29,7 +93,7 @@ data "aws_iam_policy_document" "sns_topic_policy" {
]
resources = [
- aws_sns_topic.main.arn,
+ aws_sns_topic.sns_topic_supplier.arn,
]
condition {
@@ -43,7 +107,7 @@ data "aws_iam_policy_document" "sns_topic_policy" {
}
statement {
- sid = "AllowAllSNSActionsFromSharedAccount"
+ sid = "AllowAllSNSActionsFromSharedAccount"
effect = "Allow"
actions = [
"SNS:Publish",
@@ -57,7 +121,7 @@ data "aws_iam_policy_document" "sns_topic_policy" {
}
resources = [
- aws_sns_topic.main.arn,
+ aws_sns_topic.sns_topic_supplier.arn,
]
}
}
diff --git a/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf b/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf
index 42457f6d..120f5582 100644
--- a/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf
+++ b/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf
@@ -1,7 +1,7 @@
-resource "aws_sns_topic_subscription" "firehose" {
+resource "aws_sns_topic_subscription" "sns_topic_event_bus_firehose" {
count = var.enable_event_cache ? 1 : 0
- topic_arn = aws_sns_topic.main.arn
+ topic_arn = aws_sns_topic.sns_topic_event_bus.arn
protocol = "firehose"
subscription_role_arn = aws_iam_role.sns_role.arn
endpoint = aws_kinesis_firehose_delivery_stream.main[0].arn
diff --git a/infrastructure/terraform/modules/eventsub/sns_topic_supplier.tf b/infrastructure/terraform/modules/eventsub/sns_topic_supplier.tf
new file mode 100644
index 00000000..31bb9d77
--- /dev/null
+++ b/infrastructure/terraform/modules/eventsub/sns_topic_supplier.tf
@@ -0,0 +1,27 @@
+resource "aws_sns_topic" "sns_topic_supplier" {
+ name = "${local.csi}-supplier-events.fifo"
+ kms_master_key_id = var.kms_key_arn
+
+ fifo_topic = true
+ content_based_deduplication = true
+
+ application_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
+ application_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
+ application_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null
+
+ firehose_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
+ firehose_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
+ firehose_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null
+
+ http_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
+ http_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
+ http_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null
+
+ lambda_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
+ lambda_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
+ lambda_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null
+
+ sqs_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
+ sqs_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null
+ sqs_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null
+}
diff --git a/internal/datastore/src/types.md b/internal/datastore/src/types.md
index 89056843..70504c55 100644
--- a/internal/datastore/src/types.md
+++ b/internal/datastore/src/types.md
@@ -22,6 +22,9 @@ erDiagram
string supplierStatus
string supplierStatusSk
number ttl "min: -9007199254740991, max: 9007199254740991"
+ string source
+ string subject
+ string billingRef
}
```
diff --git a/internal/events/package.json b/internal/events/package.json
index 09e72bde..395859af 100644
--- a/internal/events/package.json
+++ b/internal/events/package.json
@@ -50,5 +50,5 @@
"typecheck": "tsc --noEmit"
},
"types": "dist/index.d.ts",
- "version": "1.0.6"
+ "version": "1.0.7"
}
diff --git a/lambdas/allocation/.eslintignore b/lambdas/allocation/.eslintignore
new file mode 100644
index 00000000..1521c8b7
--- /dev/null
+++ b/lambdas/allocation/.eslintignore
@@ -0,0 +1 @@
+dist
diff --git a/lambdas/allocation/.gitignore b/lambdas/allocation/.gitignore
new file mode 100644
index 00000000..9b19292a
--- /dev/null
+++ b/lambdas/allocation/.gitignore
@@ -0,0 +1,4 @@
+.build
+coverage
+node_modules
+dist
diff --git a/lambdas/allocation/jest.config.ts b/lambdas/allocation/jest.config.ts
new file mode 100644
index 00000000..f88e7277
--- /dev/null
+++ b/lambdas/allocation/jest.config.ts
@@ -0,0 +1,60 @@
+import type { Config } from "jest";
+
+export const baseJestConfig: Config = {
+ preset: "ts-jest",
+
+ // Automatically clear mock calls, instances, contexts and results before every test
+ clearMocks: true,
+
+ // Indicates whether the coverage information should be collected while executing the test
+ collectCoverage: true,
+
+ // The directory where Jest should output its coverage files
+ coverageDirectory: "./.reports/unit/coverage",
+
+ // Indicates which provider should be used to instrument code for coverage
+ coverageProvider: "babel",
+
+ coverageThreshold: {
+ global: {
+ branches: 100,
+ functions: 100,
+ lines: 100,
+ statements: -10,
+ },
+ },
+
+ coveragePathIgnorePatterns: ["/__tests__/"],
+ transform: { "^.+\\.ts$": "ts-jest" },
+ testPathIgnorePatterns: [".build"],
+ testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],
+
+ // Use this configuration option to add custom reporters to Jest
+ reporters: [
+ "default",
+ [
+ "jest-html-reporter",
+ {
+ pageTitle: "Test Report",
+ outputPath: "./.reports/unit/test-report.html",
+ includeFailureMsg: true,
+ },
+ ],
+ ],
+
+ // The test environment that will be used for testing
+ testEnvironment: "jsdom",
+};
+
+const utilsJestConfig = {
+ ...baseJestConfig,
+
+ testEnvironment: "node",
+
+ coveragePathIgnorePatterns: [
+ ...(baseJestConfig.coveragePathIgnorePatterns ?? []),
+ "zod-validators.ts",
+ ],
+};
+
+export default utilsJestConfig;
diff --git a/lambdas/allocation/package.json b/lambdas/allocation/package.json
new file mode 100644
index 00000000..4c9b09e8
--- /dev/null
+++ b/lambdas/allocation/package.json
@@ -0,0 +1,27 @@
+{
+ "dependencies": {
+ "@aws-sdk/client-sqs": "^3.925.0",
+ "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*",
+ "aws-lambda": "^1.0.7",
+ "esbuild": "^0.25.11",
+ "pino": "^9.7.0",
+ "zod": "^4.1.11"
+ },
+ "devDependencies": {
+ "@tsconfig/node22": "^22.0.2",
+ "@types/jest": "^30.0.0",
+ "jest": "^30.2.0",
+ "jest-mock-extended": "^4.0.0",
+ "typescript": "^5.9.3"
+ },
+ "name": "nhs-notify-supplier-allocation",
+ "private": true,
+ "scripts": {
+ "lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:unit": "jest",
+ "typecheck": "tsc --noEmit"
+ },
+ "version": "0.0.1"
+}
diff --git a/lambdas/allocation/src/__tests__/allocator.test.ts b/lambdas/allocation/src/__tests__/allocator.test.ts
new file mode 100644
index 00000000..619e7f02
--- /dev/null
+++ b/lambdas/allocation/src/__tests__/allocator.test.ts
@@ -0,0 +1,159 @@
+import { Context, SNSEvent, SNSEventRecord } from "aws-lambda";
+import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
+import { mockDeep } from "jest-mock-extended";
+import pino from "pino";
+import {
+ $LetterEvent,
+ LetterEvent,
+} from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src";
+import createAllocator from "../allocator";
+import { Deps } from "../deps";
+
+function createSNSEvent(records: SNSEventRecord[]): SNSEvent {
+ return {
+ Records: records,
+ };
+}
+
+function createSNSEventRecord(message: string): SNSEventRecord {
+ return {
+ Sns: {
+ Message: message,
+ } as SNSEventRecord["Sns"],
+ } as SNSEventRecord;
+}
+
+function createLetterEvent(domainId: string): LetterEvent {
+ const now = new Date().toISOString();
+
+ return $LetterEvent.parse({
+ data: {
+ domainId,
+ groupId: "client_template",
+ origin: {
+ domain: "letter-rendering",
+ event: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
+ source: "/data-plane/letter-rendering/prod/render-pdf",
+ subject:
+ "client/00f3b388-bbe9-41c9-9e76-052d37ee8988/letter-request/test",
+ },
+
+ specificationId: "spec-001",
+ billingRef: "billing-001",
+ status: "PENDING",
+ supplierId: "supplier-001",
+ },
+ datacontenttype: "application/json",
+ dataschema:
+ "https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.PENDING.1.0.0.schema.json",
+ dataschemaversion: "1.0.0",
+ id: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
+ plane: "data",
+ recordedtime: now,
+ severitynumber: 2,
+ severitytext: "INFO",
+ source: "/data-plane/supplier-api/prod/update-status",
+ specversion: "1.0",
+ subject:
+ "letter-origin/letter-rendering/letter/f47ac10b-58cc-4372-a567-0e02b2c3d479",
+ time: now,
+ traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
+ type: "uk.nhs.notify.supplier-api.letter.PENDING.v1",
+ });
+}
+
+describe("allocator", () => {
+ const mockQueueUrl =
+ "https://sqs.eu-west-2.amazonaws.com/123456789012/test-queue.fifo";
+
+ let mockDeps: Deps;
+
+ beforeEach(() => {
+ mockDeps = {
+ sqsClient: { send: jest.fn() } as unknown as SQSClient,
+ queueUrl: mockQueueUrl,
+ logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
+ };
+
+ jest.clearAllMocks();
+ });
+
+ describe("createAllocator", () => {
+ it("should process a single SNS record and send message to SQS", async () => {
+ const letterEvent = createLetterEvent("id1");
+ const snsEvent = createSNSEvent([
+ createSNSEventRecord(JSON.stringify(letterEvent)),
+ ]);
+
+ const handler = createAllocator(mockDeps);
+ await handler(snsEvent, mockDeep(), jest.fn());
+
+ expect(mockDeps.sqsClient.send).toHaveBeenCalledWith(
+ expect.objectContaining({
+ input: {
+ QueueUrl: mockQueueUrl,
+ MessageBody: JSON.stringify(letterEvent),
+ MessageGroupId: "id1",
+ },
+ }),
+ );
+ });
+
+ it("should process multiple SNS records and send messages to SQS", async () => {
+ const letterEvent1 = createLetterEvent("id1");
+ const letterEvent2 = createLetterEvent("id2");
+ const letterEvent3 = createLetterEvent("id3");
+
+ const snsEvent = createSNSEvent([
+ createSNSEventRecord(JSON.stringify(letterEvent1)),
+ createSNSEventRecord(JSON.stringify(letterEvent2)),
+ createSNSEventRecord(JSON.stringify(letterEvent3)),
+ ]);
+
+ const handler = createAllocator(mockDeps);
+ await handler(snsEvent, mockDeep(), jest.fn());
+
+ expect(mockDeps.sqsClient.send).toHaveBeenCalledTimes(3);
+
+ expect(mockDeps.sqsClient.send).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ input: {
+ QueueUrl: mockQueueUrl,
+ MessageBody: JSON.stringify(letterEvent1),
+ MessageGroupId: "id1",
+ },
+ }),
+ );
+ expect(mockDeps.sqsClient.send).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ input: {
+ QueueUrl: mockQueueUrl,
+ MessageBody: JSON.stringify(letterEvent2),
+ MessageGroupId: "id2",
+ },
+ }),
+ );
+ expect(mockDeps.sqsClient.send).toHaveBeenNthCalledWith(
+ 3,
+ expect.objectContaining({
+ input: {
+ QueueUrl: mockQueueUrl,
+ MessageBody: JSON.stringify(letterEvent3),
+ MessageGroupId: "id3",
+ },
+ }),
+ );
+ });
+
+ it("should handle empty SNS event with no records", async () => {
+ const snsEvent = createSNSEvent([]);
+
+ const handler = createAllocator(mockDeps);
+ await handler(snsEvent, mockDeep(), jest.fn());
+
+ expect(mockDeps.sqsClient.send).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/lambdas/allocation/src/allocator.ts b/lambdas/allocation/src/allocator.ts
new file mode 100644
index 00000000..138fe1a6
--- /dev/null
+++ b/lambdas/allocation/src/allocator.ts
@@ -0,0 +1,37 @@
+import { SNSEvent, SNSEventRecord, SNSHandler } from "aws-lambda";
+import { SendMessageCommand } from "@aws-sdk/client-sqs";
+import {
+ $LetterEvent,
+ LetterEvent,
+} from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src";
+import { Deps } from "./deps";
+
+export default function createAllocator(deps: Deps): SNSHandler {
+ return async (event: SNSEvent): Promise => {
+ // Allocation will be done under a future ticket. For now, just place events on the queue,
+ // adding a message group ID to permit the use of a FIFO queue
+ const sqsCommands: SendMessageCommand[] = event.Records.map((record) =>
+ extractLetterEvent(record),
+ ).map((letterEvent) => buildSendMessageCommand(letterEvent, deps.queueUrl));
+
+ for (const sqsCommand of sqsCommands) {
+ deps.logger.info({
+ description: "Placing message on queue",
+ MessageGroupId: sqsCommand.input.MessageGroupId,
+ });
+ await deps.sqsClient.send(sqsCommand);
+ }
+ };
+}
+
+function extractLetterEvent(record: SNSEventRecord): LetterEvent {
+ return $LetterEvent.parse(JSON.parse(record.Sns.Message));
+}
+
+function buildSendMessageCommand(letterEvent: LetterEvent, queueUrl: string) {
+ return new SendMessageCommand({
+ QueueUrl: queueUrl,
+ MessageBody: JSON.stringify(letterEvent),
+ MessageGroupId: letterEvent.data.domainId,
+ });
+}
diff --git a/lambdas/allocation/src/deps.ts b/lambdas/allocation/src/deps.ts
new file mode 100644
index 00000000..be2969a8
--- /dev/null
+++ b/lambdas/allocation/src/deps.ts
@@ -0,0 +1,19 @@
+import pino from "pino";
+import { SQSClient } from "@aws-sdk/client-sqs";
+import { envVars } from "./env";
+
+export type Deps = {
+ sqsClient: SQSClient;
+ queueUrl: string;
+ logger: pino.Logger;
+};
+
+export function createDependenciesContainer(): Deps {
+ const log = pino();
+
+ return {
+ sqsClient: new SQSClient(),
+ queueUrl: envVars.QUEUE_URL,
+ logger: log,
+ };
+}
diff --git a/lambdas/allocation/src/env.ts b/lambdas/allocation/src/env.ts
new file mode 100644
index 00000000..ad8e5901
--- /dev/null
+++ b/lambdas/allocation/src/env.ts
@@ -0,0 +1,9 @@
+import { z } from "zod";
+
+const EnvVarsSchema = z.object({
+ QUEUE_URL: z.coerce.string(),
+});
+
+export type EnvVars = z.infer;
+
+export const envVars = EnvVarsSchema.parse(process.env);
diff --git a/lambdas/allocation/src/index.ts b/lambdas/allocation/src/index.ts
new file mode 100644
index 00000000..bb1f82ee
--- /dev/null
+++ b/lambdas/allocation/src/index.ts
@@ -0,0 +1,7 @@
+import createAllocator from "./allocator";
+import { createDependenciesContainer } from "./deps";
+
+const container = createDependenciesContainer();
+
+// eslint-disable-next-line import-x/prefer-default-export
+export const handler = createAllocator(container);
diff --git a/lambdas/allocation/tsconfig.json b/lambdas/allocation/tsconfig.json
new file mode 100644
index 00000000..24902365
--- /dev/null
+++ b/lambdas/allocation/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {},
+ "extends": "../../tsconfig.base.json",
+ "include": [
+ "src/**/*",
+ "jest.config.ts"
+ ]
+}
diff --git a/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts b/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
index 550fbeca..6aca8047 100644
--- a/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
+++ b/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
@@ -247,6 +247,7 @@ describe("enqueueLetterUpdateRequests function", () => {
reasonCode: lettersToUpdate[0].reasonCode,
reasonText: lettersToUpdate[0].reasonText,
}),
+ MessageGroupId: lettersToUpdate[0].id,
},
}),
);
@@ -267,6 +268,7 @@ describe("enqueueLetterUpdateRequests function", () => {
status: lettersToUpdate[1].status,
supplierId: lettersToUpdate[1].supplierId,
}),
+ MessageGroupId: lettersToUpdate[1].id,
},
}),
);
@@ -310,6 +312,7 @@ describe("enqueueLetterUpdateRequests function", () => {
status: lettersToUpdate[1].status,
supplierId: lettersToUpdate[1].supplierId,
}),
+ MessageGroupId: lettersToUpdate[1].id,
},
}),
);
diff --git a/lambdas/api-handler/src/services/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts
index c94e76b5..2738d2ec 100644
--- a/lambdas/api-handler/src/services/letter-operations.ts
+++ b/lambdas/api-handler/src/services/letter-operations.ts
@@ -94,6 +94,7 @@ export async function enqueueLetterUpdateRequests(
CorrelationId: { DataType: "String", StringValue: correlationId },
},
MessageBody: JSON.stringify(request),
+ MessageGroupId: request.id,
});
await deps.sqsClient.send(command);
} catch (error) {
diff --git a/lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts b/lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts
index 93429c90..004ef701 100644
--- a/lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts
+++ b/lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts
@@ -22,7 +22,7 @@ describe("letter-mapper", () => {
expect(event.dataschema).toBe(
`https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.PRINTED.${event.dataschemaversion}.schema.json`
);
- expect(event.dataschemaversion).toBe("1.0.6");
+ expect(event.dataschemaversion).toMatch(/1\.\d+\.\d+/);
expect(event.subject).toBe("letter-origin/supplier-api/letter/id1");
expect(event.time).toBe("2025-11-24T15:55:18.000Z");
expect(event.recordedtime).toBe("2025-11-24T15:55:18.000Z");
diff --git a/lambdas/supplier-events-forwarder/.eslintignore b/lambdas/supplier-events-forwarder/.eslintignore
new file mode 100644
index 00000000..1521c8b7
--- /dev/null
+++ b/lambdas/supplier-events-forwarder/.eslintignore
@@ -0,0 +1 @@
+dist
diff --git a/lambdas/supplier-events-forwarder/.gitignore b/lambdas/supplier-events-forwarder/.gitignore
new file mode 100644
index 00000000..9b19292a
--- /dev/null
+++ b/lambdas/supplier-events-forwarder/.gitignore
@@ -0,0 +1,4 @@
+.build
+coverage
+node_modules
+dist
diff --git a/lambdas/supplier-events-forwarder/jest.config.ts b/lambdas/supplier-events-forwarder/jest.config.ts
new file mode 100644
index 00000000..f8e09e55
--- /dev/null
+++ b/lambdas/supplier-events-forwarder/jest.config.ts
@@ -0,0 +1,59 @@
+import type { Config } from "jest";
+
+export const baseJestConfig: Config = {
+ preset: "ts-jest",
+
+ // Automatically clear mock calls, instances, contexts and results before every test
+ clearMocks: true,
+
+ // Indicates whether the coverage information should be collected while executing the test
+ collectCoverage: true,
+
+ // The directory where Jest should output its coverage files
+ coverageDirectory: "./.reports/unit/coverage",
+
+ // Indicates which provider should be used to instrument code for coverage
+ coverageProvider: "babel",
+
+ coverageThreshold: {
+ global: {
+ branches: 100,
+ functions: 100,
+ lines: 100,
+ statements: -10,
+ },
+ },
+
+ coveragePathIgnorePatterns: ["/__tests__/"],
+ transform: { "^.+\\.ts$": "ts-jest" },
+ testPathIgnorePatterns: [".build"],
+ testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],
+
+ // Use this configuration option to add custom reporters to Jest
+ reporters: [
+ "default",
+ [
+ "jest-html-reporter",
+ {
+ pageTitle: "Test Report",
+ outputPath: "./.reports/unit/test-report.html",
+ includeFailureMsg: true,
+ },
+ ],
+ ],
+
+ // The test environment that will be used for testing
+ testEnvironment: "jsdom",
+};
+
+const utilsJestConfig = {
+ ...baseJestConfig,
+
+ testEnvironment: "node",
+
+ coveragePathIgnorePatterns: [
+ ...(baseJestConfig.coveragePathIgnorePatterns ?? []),
+ ],
+};
+
+export default utilsJestConfig;
diff --git a/lambdas/supplier-events-forwarder/package.json b/lambdas/supplier-events-forwarder/package.json
new file mode 100644
index 00000000..0ce56473
--- /dev/null
+++ b/lambdas/supplier-events-forwarder/package.json
@@ -0,0 +1,26 @@
+{
+ "dependencies": {
+ "@aws-sdk/client-firehose": "^3.925.0",
+ "aws-lambda": "^1.0.7",
+ "esbuild": "^0.25.11",
+ "pino": "^9.7.0",
+ "zod": "^4.1.11"
+ },
+ "devDependencies": {
+ "@tsconfig/node22": "^22.0.2",
+ "@types/jest": "^30.0.0",
+ "jest": "^30.2.0",
+ "jest-mock-extended": "^4.0.0",
+ "typescript": "^5.9.3"
+ },
+ "name": "nhs-notify-supplier-events-forwarder",
+ "private": true,
+ "scripts": {
+ "lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:unit": "jest",
+ "typecheck": "tsc --noEmit"
+ },
+ "version": "0.0.1"
+}
diff --git a/lambdas/supplier-events-forwarder/src/__tests__/forwarder.test.ts b/lambdas/supplier-events-forwarder/src/__tests__/forwarder.test.ts
new file mode 100644
index 00000000..76aef5a8
--- /dev/null
+++ b/lambdas/supplier-events-forwarder/src/__tests__/forwarder.test.ts
@@ -0,0 +1,224 @@
+import { Context, SQSEvent, SQSRecord } from "aws-lambda";
+import { FirehoseClient, PutRecordCommand } from "@aws-sdk/client-firehose";
+import { mockDeep } from "jest-mock-extended";
+import pino from "pino";
+import createForwarder from "../forwarder";
+import { Deps } from "../deps";
+
+function createSQSEvent(records: SQSRecord[]): SQSEvent {
+ return {
+ Records: records,
+ };
+}
+
+/**
+ * Creates an SQS record with a body containing the SNS notification wrapper.
+ * This simulates what SNS delivers to SQS when raw_message_delivery is false.
+ */
+function createSQSRecord(
+ body: string,
+ messageId = "test-sqs-msg-id",
+): SQSRecord {
+ return {
+ messageId,
+ receiptHandle: "test-receipt-handle",
+ body,
+ attributes: {
+ ApproximateReceiveCount: "1",
+ SentTimestamp: "1704801600000",
+ SenderId: "123456789012",
+ ApproximateFirstReceiveTimestamp: "1704801600000",
+ },
+ messageAttributes: {},
+ md5OfBody: "test-md5",
+ eventSource: "aws:sqs",
+ eventSourceARN: "arn:aws:sqs:eu-west-2:123456789012:test-queue.fifo",
+ awsRegion: "eu-west-2",
+ };
+}
+
+/**
+ * Creates an SNS notification wrapper as it would appear in the SQS message body
+ * when raw_message_delivery is false.
+ */
+function createSnsNotificationWrapper(
+ message: string,
+ overrides: Partial<{
+ MessageId: string;
+ TopicArn: string;
+ Subject: string;
+ }> = {},
+): string {
+ return JSON.stringify({
+ Type: "Notification",
+ MessageId: overrides.MessageId ?? "test-sns-message-id",
+ TopicArn:
+ overrides.TopicArn ??
+ "arn:aws:sns:eu-west-2:123456789012:test-topic.fifo",
+ Subject: overrides.Subject ?? "Test Subject",
+ Message: message,
+ Timestamp: "2026-01-09T12:00:00.000Z",
+ SignatureVersion: "1",
+ Signature: "test-signature",
+ SigningCertUrl: "https://sns.eu-west-2.amazonaws.com/cert.pem",
+ UnsubscribeUrl: "https://sns.eu-west-2.amazonaws.com/unsubscribe",
+ MessageAttributes: {},
+ });
+}
+
+describe("forwarder", () => {
+ const mockDeliveryStreamName = "test-delivery-stream";
+
+ let mockFirehoseClient: jest.Mocked;
+ let mockDeps: Deps;
+
+ beforeEach(() => {
+ mockFirehoseClient = {
+ send: jest.fn().mockResolvedValue({}),
+ } as unknown as jest.Mocked;
+
+ mockDeps = {
+ firehoseClient: mockFirehoseClient,
+ deliveryStreamName: mockDeliveryStreamName,
+ logger: pino({ level: "silent" }),
+ };
+
+ jest.clearAllMocks();
+ });
+
+ describe("createForwarder", () => {
+ it("should process a single SQS record and send to Firehose", async () => {
+ const message = JSON.stringify({ eventType: "test", data: "value" });
+ const snsWrapper = createSnsNotificationWrapper(message);
+ const sqsRecord = createSQSRecord(snsWrapper);
+ const sqsEvent = createSQSEvent([sqsRecord]);
+
+ const handler = createForwarder(mockDeps);
+ await handler(sqsEvent, mockDeep(), jest.fn());
+
+ expect(mockFirehoseClient.send).toHaveBeenCalledTimes(1);
+ expect(mockFirehoseClient.send).toHaveBeenCalledWith(
+ expect.any(PutRecordCommand),
+ );
+
+ const sentCommand = mockFirehoseClient.send.mock
+ .calls[0][0] as PutRecordCommand;
+ expect(sentCommand.input).toEqual({
+ DeliveryStreamName: mockDeliveryStreamName,
+ Record: {
+ Data: Buffer.from(`${snsWrapper}\n`),
+ },
+ });
+ });
+
+ it("should process multiple SQS records and send to Firehose", async () => {
+ const message1 = JSON.stringify({ eventType: "test1" });
+ const message2 = JSON.stringify({ eventType: "test2" });
+ const message3 = JSON.stringify({ eventType: "test3" });
+
+ const snsWrapper1 = createSnsNotificationWrapper(message1, {
+ MessageId: "msg-1",
+ });
+ const snsWrapper2 = createSnsNotificationWrapper(message2, {
+ MessageId: "msg-2",
+ });
+ const snsWrapper3 = createSnsNotificationWrapper(message3, {
+ MessageId: "msg-3",
+ });
+
+ const sqsEvent = createSQSEvent([
+ createSQSRecord(snsWrapper1, "sqs-1"),
+ createSQSRecord(snsWrapper2, "sqs-2"),
+ createSQSRecord(snsWrapper3, "sqs-3"),
+ ]);
+
+ const handler = createForwarder(mockDeps);
+ await handler(sqsEvent, mockDeep(), jest.fn());
+
+ expect(mockFirehoseClient.send).toHaveBeenCalledTimes(3);
+
+ const sentCommands = mockFirehoseClient.send.mock.calls.map(
+ (call) => call[0] as PutRecordCommand,
+ );
+
+ expect(sentCommands[0].input).toEqual({
+ DeliveryStreamName: mockDeliveryStreamName,
+ Record: {
+ Data: Buffer.from(`${snsWrapper1}\n`),
+ },
+ });
+
+ expect(sentCommands[1].input).toEqual({
+ DeliveryStreamName: mockDeliveryStreamName,
+ Record: {
+ Data: Buffer.from(`${snsWrapper2}\n`),
+ },
+ });
+
+ expect(sentCommands[2].input).toEqual({
+ DeliveryStreamName: mockDeliveryStreamName,
+ Record: {
+ Data: Buffer.from(`${snsWrapper3}\n`),
+ },
+ });
+ });
+
+ it("should handle empty SQS event with no records", async () => {
+ const sqsEvent = createSQSEvent([]);
+
+ const handler = createForwarder(mockDeps);
+ await handler(sqsEvent, mockDeep(), jest.fn());
+
+ expect(mockFirehoseClient.send).not.toHaveBeenCalled();
+ });
+
+ it("should forward the SNS notification wrapper from SQS body to Firehose", async () => {
+ const message = JSON.stringify({ key: "value" });
+ const snsWrapper = createSnsNotificationWrapper(message, {
+ MessageId: "unique-msg-id",
+ TopicArn: "arn:aws:sns:eu-west-2:123456789012:my-topic.fifo",
+ Subject: "My Subject",
+ });
+ const sqsRecord = createSQSRecord(snsWrapper);
+ const sqsEvent = createSQSEvent([sqsRecord]);
+
+ const handler = createForwarder(mockDeps);
+ await handler(sqsEvent, mockDeep(), jest.fn());
+
+ const sentCommand = mockFirehoseClient.send.mock
+ .calls[0][0] as PutRecordCommand;
+ const recordData = sentCommand.input.Record?.Data as Buffer;
+ const parsedData = JSON.parse(recordData.toString().replace(/\n$/, ""));
+
+ expect(parsedData).toEqual({
+ Type: "Notification",
+ MessageId: "unique-msg-id",
+ TopicArn: "arn:aws:sns:eu-west-2:123456789012:my-topic.fifo",
+ Subject: "My Subject",
+ Message: message,
+ Timestamp: "2026-01-09T12:00:00.000Z",
+ SignatureVersion: "1",
+ Signature: "test-signature",
+ SigningCertUrl: "https://sns.eu-west-2.amazonaws.com/cert.pem",
+ UnsubscribeUrl: "https://sns.eu-west-2.amazonaws.com/unsubscribe",
+ MessageAttributes: {},
+ });
+ });
+
+ it("should append newline to message for JSON Lines format", async () => {
+ const message = JSON.stringify({ key: "value" });
+ const snsWrapper = createSnsNotificationWrapper(message);
+ const sqsRecord = createSQSRecord(snsWrapper);
+ const sqsEvent = createSQSEvent([sqsRecord]);
+
+ const handler = createForwarder(mockDeps);
+ await handler(sqsEvent, mockDeep(), jest.fn());
+
+ const sentCommand = mockFirehoseClient.send.mock
+ .calls[0][0] as PutRecordCommand;
+ const recordData = sentCommand.input.Record?.Data as Buffer;
+
+ expect(recordData.toString().endsWith("\n")).toBe(true);
+ });
+ });
+});
diff --git a/lambdas/supplier-events-forwarder/src/deps.ts b/lambdas/supplier-events-forwarder/src/deps.ts
new file mode 100644
index 00000000..3194a216
--- /dev/null
+++ b/lambdas/supplier-events-forwarder/src/deps.ts
@@ -0,0 +1,19 @@
+import pino from "pino";
+import { FirehoseClient } from "@aws-sdk/client-firehose";
+import { envVars } from "./env";
+
+export type Deps = {
+ firehoseClient: FirehoseClient;
+ deliveryStreamName: string;
+ logger: pino.Logger;
+};
+
+export function createDependenciesContainer(): Deps {
+ const log = pino();
+
+ return {
+ firehoseClient: new FirehoseClient(),
+ deliveryStreamName: envVars.FIREHOSE_DELIVERY_STREAM_NAME,
+ logger: log,
+ };
+}
diff --git a/lambdas/supplier-events-forwarder/src/env.ts b/lambdas/supplier-events-forwarder/src/env.ts
new file mode 100644
index 00000000..f96c0446
--- /dev/null
+++ b/lambdas/supplier-events-forwarder/src/env.ts
@@ -0,0 +1,9 @@
+import { z } from "zod";
+
+const EnvVarsSchema = z.object({
+ FIREHOSE_DELIVERY_STREAM_NAME: z.coerce.string(),
+});
+
+export type EnvVars = z.infer;
+
+export const envVars = EnvVarsSchema.parse(process.env);
diff --git a/lambdas/supplier-events-forwarder/src/forwarder.ts b/lambdas/supplier-events-forwarder/src/forwarder.ts
new file mode 100644
index 00000000..b1686caa
--- /dev/null
+++ b/lambdas/supplier-events-forwarder/src/forwarder.ts
@@ -0,0 +1,36 @@
+import { SQSEvent, SQSHandler, SQSRecord } from "aws-lambda";
+import { PutRecordCommand } from "@aws-sdk/client-firehose";
+import { Deps } from "./deps";
+
+export default function createForwarder(deps: Deps): SQSHandler {
+ return async (event: SQSEvent): Promise => {
+ const firehoseCommands: PutRecordCommand[] = event.Records.map((record) =>
+ buildPutRecordCommand(record, deps.deliveryStreamName),
+ );
+
+ for (const firehoseCommand of firehoseCommands) {
+ await deps.firehoseClient.send(firehoseCommand);
+ }
+ };
+}
+
+/**
+ * Builds a PutRecordCommand for Firehose.
+ * The SQS message body already contains the SNS notification wrapper
+ * (since raw_message_delivery is false on the SNS->SQS subscription),
+ * so we forward it directly to Firehose.
+ */
+function buildPutRecordCommand(
+ record: SQSRecord,
+ deliveryStreamName: string,
+): PutRecordCommand {
+ // Add a newline to each record for proper JSON Lines format in S3
+ const data = `${record.body}\n`;
+
+ return new PutRecordCommand({
+ DeliveryStreamName: deliveryStreamName,
+ Record: {
+ Data: Buffer.from(data),
+ },
+ });
+}
diff --git a/lambdas/supplier-events-forwarder/src/index.ts b/lambdas/supplier-events-forwarder/src/index.ts
new file mode 100644
index 00000000..b3fbaa59
--- /dev/null
+++ b/lambdas/supplier-events-forwarder/src/index.ts
@@ -0,0 +1,7 @@
+import createForwarder from "./forwarder";
+import { createDependenciesContainer } from "./deps";
+
+const container = createDependenciesContainer();
+
+// eslint-disable-next-line import-x/prefer-default-export
+export const handler = createForwarder(container);
diff --git a/lambdas/supplier-events-forwarder/tsconfig.json b/lambdas/supplier-events-forwarder/tsconfig.json
new file mode 100644
index 00000000..24902365
--- /dev/null
+++ b/lambdas/supplier-events-forwarder/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {},
+ "extends": "../../tsconfig.base.json",
+ "include": [
+ "src/**/*",
+ "jest.config.ts"
+ ]
+}
diff --git a/lambdas/upsert-letter/package.json b/lambdas/upsert-letter/package.json
index 9444e714..0b014d2a 100644
--- a/lambdas/upsert-letter/package.json
+++ b/lambdas/upsert-letter/package.json
@@ -1,10 +1,10 @@
{
"dependencies": {
+ "@types/aws-lambda": "^8.10.148",
"esbuild": "^0.24.0"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
- "@types/aws-lambda": "^8.10.148",
"@types/jest": "^30.0.0",
"jest": "^30.2.0",
"jest-mock-extended": "^4.0.0",
diff --git a/lambdas/upsert-letter/src/__tests__/index.test.ts b/lambdas/upsert-letter/src/__tests__/index.test.ts
index d29b864b..5d2c910e 100644
--- a/lambdas/upsert-letter/src/__tests__/index.test.ts
+++ b/lambdas/upsert-letter/src/__tests__/index.test.ts
@@ -1,17 +1,12 @@
-import type { Context } from "aws-lambda";
+import type { Context, SQSEvent } from "aws-lambda";
import { mockDeep } from "jest-mock-extended";
import handler from "..";
describe("event-logging Lambda", () => {
- it("logs the input event and returns 200", async () => {
- const event = { foo: "bar" };
+ it("completes successfully", async () => {
+ const event = { Records: [{ body: "{}" }] } as SQSEvent;
const context = mockDeep();
const callback = jest.fn();
- const result = await handler(event, context, callback);
-
- expect(result).toEqual({
- statusCode: 200,
- body: "Event logged",
- });
+ await handler(event, context, callback);
});
});
diff --git a/lambdas/upsert-letter/src/index.ts b/lambdas/upsert-letter/src/index.ts
index a165560c..a50f8323 100644
--- a/lambdas/upsert-letter/src/index.ts
+++ b/lambdas/upsert-letter/src/index.ts
@@ -1,12 +1,13 @@
// Replace me with the actual code for your Lambda function
-import { Handler } from "aws-lambda";
+import { SQSEvent, SQSHandler } from "aws-lambda";
+import pino from "pino";
-const handler: Handler = async (event) => {
- console.log("Received event:", event);
- return {
- statusCode: 200,
- body: "Event logged",
- };
+const log = pino();
+
+export const handler: SQSHandler = async (event: SQSEvent) => {
+ for (const record of event.Records) {
+ log.info({ description: "Received event", message: record.body });
+ }
};
export default handler;
diff --git a/package-lock.json b/package-lock.json
index 7a29e4d7..ee166458 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"@aws-sdk/client-sns": "^3.936.0",
"@playwright/test": "^1.57.0",
"ajv": "^8.17.1",
+ "aws-lambda": "^1.0.7",
"get-east-asian-width": "^1.4.0",
"js-yaml": "^4.1.0",
"openapi-response-validator": "^12.1.3",
@@ -125,7 +126,7 @@
},
"internal/events": {
"name": "@nhsdigital/nhs-notify-event-schemas-supplier-api",
- "version": "1.0.6",
+ "version": "1.0.7",
"license": "MIT",
"dependencies": {
"@asyncapi/bundler": "^0.6.4",
@@ -167,6 +168,47 @@
"typescript": "^5.9.3"
}
},
+ "lambdas/allocation": {
+ "name": "nhs-notify-supplier-allocation",
+ "version": "0.0.1",
+ "dependencies": {
+ "@aws-sdk/client-sqs": "^3.925.0",
+ "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*",
+ "aws-lambda": "^1.0.7",
+ "esbuild": "^0.25.11",
+ "pino": "^9.7.0",
+ "zod": "^4.1.11"
+ },
+ "devDependencies": {
+ "@tsconfig/node22": "^22.0.2",
+ "@types/jest": "^30.0.0",
+ "jest": "^30.2.0",
+ "jest-mock-extended": "^4.0.0",
+ "typescript": "^5.9.3"
+ }
+ },
+ "lambdas/allocation/node_modules/pino": {
+ "version": "9.14.0",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
+ "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==",
+ "license": "MIT",
+ "dependencies": {
+ "@pinojs/redact": "^0.4.0",
+ "atomic-sleep": "^1.0.0",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^2.0.0",
+ "pino-std-serializers": "^7.0.0",
+ "process-warning": "^5.0.0",
+ "quick-format-unescaped": "^4.0.3",
+ "real-require": "^0.2.0",
+ "safe-stable-stringify": "^2.3.1",
+ "sonic-boom": "^4.0.1",
+ "thread-stream": "^3.0.0"
+ },
+ "bin": {
+ "pino": "bin.js"
+ }
+ },
"lambdas/api-handler": {
"name": "nhs-notify-supplier-api-handler",
"version": "0.0.1",
@@ -1177,6 +1219,46 @@
"@esbuild/win32-x64": "0.24.2"
}
},
+ "lambdas/supplier-events-forwarder": {
+ "name": "nhs-notify-supplier-events-forwarder",
+ "version": "0.0.1",
+ "dependencies": {
+ "@aws-sdk/client-firehose": "^3.925.0",
+ "aws-lambda": "^1.0.7",
+ "esbuild": "^0.25.11",
+ "pino": "^9.7.0",
+ "zod": "^4.1.11"
+ },
+ "devDependencies": {
+ "@tsconfig/node22": "^22.0.2",
+ "@types/jest": "^30.0.0",
+ "jest": "^30.2.0",
+ "jest-mock-extended": "^4.0.0",
+ "typescript": "^5.9.3"
+ }
+ },
+ "lambdas/supplier-events-forwarder/node_modules/pino": {
+ "version": "9.14.0",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
+ "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==",
+ "license": "MIT",
+ "dependencies": {
+ "@pinojs/redact": "^0.4.0",
+ "atomic-sleep": "^1.0.0",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^2.0.0",
+ "pino-std-serializers": "^7.0.0",
+ "process-warning": "^5.0.0",
+ "quick-format-unescaped": "^4.0.3",
+ "real-require": "^0.2.0",
+ "safe-stable-stringify": "^2.3.1",
+ "sonic-boom": "^4.0.1",
+ "thread-stream": "^3.0.0"
+ },
+ "bin": {
+ "pino": "bin.js"
+ }
+ },
"lambdas/upsert-letter": {
"name": "nhs-notify-supplier-api-upsert-letter",
"version": "0.0.1",
@@ -1997,6 +2079,512 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@aws-sdk/client-firehose": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-firehose/-/client-firehose-3.965.0.tgz",
+ "integrity": "sha512-kMMta0tkyTfJ7H5+RmtRHrUZXjz1UqOqJwWegS6+R0bp3v47n/BRuSn3udTNIWHsE2N87cRre07XTeCQ3RFyPw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/credential-provider-node": "3.965.0",
+ "@aws-sdk/middleware-host-header": "3.965.0",
+ "@aws-sdk/middleware-logger": "3.965.0",
+ "@aws-sdk/middleware-recursion-detection": "3.965.0",
+ "@aws-sdk/middleware-user-agent": "3.965.0",
+ "@aws-sdk/region-config-resolver": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@aws-sdk/util-endpoints": "3.965.0",
+ "@aws-sdk/util-user-agent-browser": "3.965.0",
+ "@aws-sdk/util-user-agent-node": "3.965.0",
+ "@smithy/config-resolver": "^4.4.5",
+ "@smithy/core": "^3.20.0",
+ "@smithy/fetch-http-handler": "^5.3.8",
+ "@smithy/hash-node": "^4.2.7",
+ "@smithy/invalid-dependency": "^4.2.7",
+ "@smithy/middleware-content-length": "^4.2.7",
+ "@smithy/middleware-endpoint": "^4.4.1",
+ "@smithy/middleware-retry": "^4.4.17",
+ "@smithy/middleware-serde": "^4.2.8",
+ "@smithy/middleware-stack": "^4.2.7",
+ "@smithy/node-config-provider": "^4.3.7",
+ "@smithy/node-http-handler": "^4.4.7",
+ "@smithy/protocol-http": "^5.3.7",
+ "@smithy/smithy-client": "^4.10.2",
+ "@smithy/types": "^4.11.0",
+ "@smithy/url-parser": "^4.2.7",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.16",
+ "@smithy/util-defaults-mode-node": "^4.2.19",
+ "@smithy/util-endpoints": "^3.2.7",
+ "@smithy/util-middleware": "^4.2.7",
+ "@smithy/util-retry": "^4.2.7",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/client-sso": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.965.0.tgz",
+ "integrity": "sha512-iv2tr+n4aZ+nPUFFvG00hISPuEd4DU+1/Q8rPAYKXsM+vEPJ2nAnP5duUOa2fbOLIUCRxX3dcQaQaghVHDHzQw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/middleware-host-header": "3.965.0",
+ "@aws-sdk/middleware-logger": "3.965.0",
+ "@aws-sdk/middleware-recursion-detection": "3.965.0",
+ "@aws-sdk/middleware-user-agent": "3.965.0",
+ "@aws-sdk/region-config-resolver": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@aws-sdk/util-endpoints": "3.965.0",
+ "@aws-sdk/util-user-agent-browser": "3.965.0",
+ "@aws-sdk/util-user-agent-node": "3.965.0",
+ "@smithy/config-resolver": "^4.4.5",
+ "@smithy/core": "^3.20.0",
+ "@smithy/fetch-http-handler": "^5.3.8",
+ "@smithy/hash-node": "^4.2.7",
+ "@smithy/invalid-dependency": "^4.2.7",
+ "@smithy/middleware-content-length": "^4.2.7",
+ "@smithy/middleware-endpoint": "^4.4.1",
+ "@smithy/middleware-retry": "^4.4.17",
+ "@smithy/middleware-serde": "^4.2.8",
+ "@smithy/middleware-stack": "^4.2.7",
+ "@smithy/node-config-provider": "^4.3.7",
+ "@smithy/node-http-handler": "^4.4.7",
+ "@smithy/protocol-http": "^5.3.7",
+ "@smithy/smithy-client": "^4.10.2",
+ "@smithy/types": "^4.11.0",
+ "@smithy/url-parser": "^4.2.7",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.16",
+ "@smithy/util-defaults-mode-node": "^4.2.19",
+ "@smithy/util-endpoints": "^3.2.7",
+ "@smithy/util-middleware": "^4.2.7",
+ "@smithy/util-retry": "^4.2.7",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/core": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.965.0.tgz",
+ "integrity": "sha512-aq9BhQxdHit8UUJ9C0im9TtuKeK0pT6NXmNJxMTCFeStI7GG7ImIsSislg3BZTIifVg1P6VLdzMyz9de85iutQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.965.0",
+ "@aws-sdk/xml-builder": "3.965.0",
+ "@smithy/core": "^3.20.0",
+ "@smithy/node-config-provider": "^4.3.7",
+ "@smithy/property-provider": "^4.2.7",
+ "@smithy/protocol-http": "^5.3.7",
+ "@smithy/signature-v4": "^5.3.7",
+ "@smithy/smithy-client": "^4.10.2",
+ "@smithy/types": "^4.11.0",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-middleware": "^4.2.7",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.965.0.tgz",
+ "integrity": "sha512-mdGnaIjMxTIjsb70dEj3VsWPWpoq1V5MWzBSfJq2H8zgMBXjn6d5/qHP8HMf53l9PrsgqzMpXGv3Av549A2x1g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/property-provider": "^4.2.7",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.965.0.tgz",
+ "integrity": "sha512-YuGQel9EgA/z25oeLM+GYYQS750+8AESvr7ZEmVnRPL0sg+K3DmGqdv+9gFjFd0UkLjTlC/jtbP2cuY6UcPiHQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/fetch-http-handler": "^5.3.8",
+ "@smithy/node-http-handler": "^4.4.7",
+ "@smithy/property-provider": "^4.2.7",
+ "@smithy/protocol-http": "^5.3.7",
+ "@smithy/smithy-client": "^4.10.2",
+ "@smithy/types": "^4.11.0",
+ "@smithy/util-stream": "^4.5.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.965.0.tgz",
+ "integrity": "sha512-xRo72Prer5s0xYVSCxCymVIRSqrVlevK5cmU0GWq9yJtaBNpnx02jwdJg80t/Ni7pgbkQyFWRMcq38c1tc6M/w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/credential-provider-env": "3.965.0",
+ "@aws-sdk/credential-provider-http": "3.965.0",
+ "@aws-sdk/credential-provider-login": "3.965.0",
+ "@aws-sdk/credential-provider-process": "3.965.0",
+ "@aws-sdk/credential-provider-sso": "3.965.0",
+ "@aws-sdk/credential-provider-web-identity": "3.965.0",
+ "@aws-sdk/nested-clients": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/credential-provider-imds": "^4.2.7",
+ "@smithy/property-provider": "^4.2.7",
+ "@smithy/shared-ini-file-loader": "^4.4.2",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-login": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.965.0.tgz",
+ "integrity": "sha512-43/H8Qku8LHyugbhLo8kjD+eauhybCeVkmrnvWl8bXNHJP7xi1jCdtBQJKKJqiIHZws4MOEwkji8kFdAVRCe6g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/nested-clients": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/property-provider": "^4.2.7",
+ "@smithy/protocol-http": "^5.3.7",
+ "@smithy/shared-ini-file-loader": "^4.4.2",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.965.0.tgz",
+ "integrity": "sha512-cRxmMHF+Zh2lkkkEVduKl+8OQdtg/DhYA69+/7SPSQURlgyjFQGlRQ58B7q8abuNlrGT3sV+UzeOylZpJbV61Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.965.0",
+ "@aws-sdk/credential-provider-http": "3.965.0",
+ "@aws-sdk/credential-provider-ini": "3.965.0",
+ "@aws-sdk/credential-provider-process": "3.965.0",
+ "@aws-sdk/credential-provider-sso": "3.965.0",
+ "@aws-sdk/credential-provider-web-identity": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/credential-provider-imds": "^4.2.7",
+ "@smithy/property-provider": "^4.2.7",
+ "@smithy/shared-ini-file-loader": "^4.4.2",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.965.0.tgz",
+ "integrity": "sha512-gmkPmdiR0yxnTzLPDb7rwrDhGuCUjtgnj8qWP+m0gSz/W43rR4jRPVEf6DUX2iC+ImQhxo3NFhuB3V42Kzo3TQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/property-provider": "^4.2.7",
+ "@smithy/shared-ini-file-loader": "^4.4.2",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.965.0.tgz",
+ "integrity": "sha512-N01AYvtCqG3Wo/s/LvYt19ity18/FqggiXT+elAs3X9Om/Wfx+hw9G+i7jaDmy+/xewmv8AdQ2SK5Q30dXw/Fw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-sso": "3.965.0",
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/token-providers": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/property-provider": "^4.2.7",
+ "@smithy/shared-ini-file-loader": "^4.4.2",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.965.0.tgz",
+ "integrity": "sha512-T4gMZ2JzXnfxe1oTD+EDGLSxFfk1+WkLZdiHXEMZp8bFI1swP/3YyDFXI+Ib9Uq1JhnAmrCXtOnkicKEhDkdhQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/nested-clients": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/property-provider": "^4.2.7",
+ "@smithy/shared-ini-file-loader": "^4.4.2",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-host-header": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.965.0.tgz",
+ "integrity": "sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/protocol-http": "^5.3.7",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-logger": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.965.0.tgz",
+ "integrity": "sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-recursion-detection": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.965.0.tgz",
+ "integrity": "sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.965.0",
+ "@aws/lambda-invoke-store": "^0.2.2",
+ "@smithy/protocol-http": "^5.3.7",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-user-agent": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.965.0.tgz",
+ "integrity": "sha512-RBEYVGgu/WeAt+H/qLrGc+t8LqAUkbyvh3wBfTiuAD+uBcWsKnvnB1iSBX75FearC0fmoxzXRUc0PMxMdqpjJQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@aws-sdk/util-endpoints": "3.965.0",
+ "@smithy/core": "^3.20.0",
+ "@smithy/protocol-http": "^5.3.7",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/nested-clients": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.965.0.tgz",
+ "integrity": "sha512-muNVUjUEU+/KLFrLzQ8PMXyw4+a/MP6t4GIvwLtyx/kH0rpSy5s0YmqacMXheuIe6F/5QT8uksXGNAQenitkGQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/middleware-host-header": "3.965.0",
+ "@aws-sdk/middleware-logger": "3.965.0",
+ "@aws-sdk/middleware-recursion-detection": "3.965.0",
+ "@aws-sdk/middleware-user-agent": "3.965.0",
+ "@aws-sdk/region-config-resolver": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@aws-sdk/util-endpoints": "3.965.0",
+ "@aws-sdk/util-user-agent-browser": "3.965.0",
+ "@aws-sdk/util-user-agent-node": "3.965.0",
+ "@smithy/config-resolver": "^4.4.5",
+ "@smithy/core": "^3.20.0",
+ "@smithy/fetch-http-handler": "^5.3.8",
+ "@smithy/hash-node": "^4.2.7",
+ "@smithy/invalid-dependency": "^4.2.7",
+ "@smithy/middleware-content-length": "^4.2.7",
+ "@smithy/middleware-endpoint": "^4.4.1",
+ "@smithy/middleware-retry": "^4.4.17",
+ "@smithy/middleware-serde": "^4.2.8",
+ "@smithy/middleware-stack": "^4.2.7",
+ "@smithy/node-config-provider": "^4.3.7",
+ "@smithy/node-http-handler": "^4.4.7",
+ "@smithy/protocol-http": "^5.3.7",
+ "@smithy/smithy-client": "^4.10.2",
+ "@smithy/types": "^4.11.0",
+ "@smithy/url-parser": "^4.2.7",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.16",
+ "@smithy/util-defaults-mode-node": "^4.2.19",
+ "@smithy/util-endpoints": "^3.2.7",
+ "@smithy/util-middleware": "^4.2.7",
+ "@smithy/util-retry": "^4.2.7",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/region-config-resolver": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.965.0.tgz",
+ "integrity": "sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/config-resolver": "^4.4.5",
+ "@smithy/node-config-provider": "^4.3.7",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/token-providers": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.965.0.tgz",
+ "integrity": "sha512-aR0qxg0b8flkXJVE+CM1gzo7uJ57md50z2eyCwofC0QIz5Y0P7/7vvb9/dmUQt6eT9XRN5iRcUqq2IVxVDvJOw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.965.0",
+ "@aws-sdk/nested-clients": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/property-provider": "^4.2.7",
+ "@smithy/shared-ini-file-loader": "^4.4.2",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/types": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.965.0.tgz",
+ "integrity": "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.965.0.tgz",
+ "integrity": "sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/types": "^4.11.0",
+ "@smithy/url-parser": "^4.2.7",
+ "@smithy/util-endpoints": "^3.2.7",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.965.0.tgz",
+ "integrity": "sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/types": "^4.11.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.965.0.tgz",
+ "integrity": "sha512-kokIHUfNT3/P55E4fUJJrFHuuA9BbjFKUIxoLrd3UaRfdafT0ScRfg2eaZie6arf60EuhlUIZH0yALxttMEjxQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-user-agent": "3.965.0",
+ "@aws-sdk/types": "3.965.0",
+ "@smithy/node-config-provider": "^4.3.7",
+ "@smithy/types": "^4.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/xml-builder": {
+ "version": "3.965.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.965.0.tgz",
+ "integrity": "sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.11.0",
+ "fast-xml-parser": "5.2.5",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@aws-sdk/client-kinesis": {
"version": "3.964.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-kinesis/-/client-kinesis-3.964.0.tgz",
@@ -6580,9 +7168,9 @@
}
},
"node_modules/@smithy/core": {
- "version": "3.20.0",
- "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.0.tgz",
- "integrity": "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==",
+ "version": "3.20.1",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.1.tgz",
+ "integrity": "sha512-wOboSEdQ85dbKAJ0zL+wQ6b0HTSBRhtGa0PYKysQXkRg+vK0tdCRRVruiFM2QMprkOQwSYOnwF4og96PAaEGag==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/middleware-serde": "^4.2.8",
@@ -6800,12 +7388,12 @@
}
},
"node_modules/@smithy/middleware-endpoint": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.1.tgz",
- "integrity": "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==",
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.2.tgz",
+ "integrity": "sha512-mqpAdux0BNmZu/SqkFhQEnod4fX23xxTvU2LUpmKp0JpSI+kPYCiHJMmzREr8yxbNxKL2/DU1UZm9i++ayU+2g==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.20.0",
+ "@smithy/core": "^3.20.1",
"@smithy/middleware-serde": "^4.2.8",
"@smithy/node-config-provider": "^4.3.7",
"@smithy/shared-ini-file-loader": "^4.4.2",
@@ -6819,15 +7407,15 @@
}
},
"node_modules/@smithy/middleware-retry": {
- "version": "4.4.17",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.17.tgz",
- "integrity": "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==",
+ "version": "4.4.18",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.18.tgz",
+ "integrity": "sha512-E5hulijA59nBk/zvcwVMaS7FG7Y4l6hWA9vrW018r+8kiZef4/ETQaPI4oY+3zsy9f6KqDv3c4VKtO4DwwgpCg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/node-config-provider": "^4.3.7",
"@smithy/protocol-http": "^5.3.7",
"@smithy/service-error-classification": "^4.2.7",
- "@smithy/smithy-client": "^4.10.2",
+ "@smithy/smithy-client": "^4.10.3",
"@smithy/types": "^4.11.0",
"@smithy/util-middleware": "^4.2.7",
"@smithy/util-retry": "^4.2.7",
@@ -6994,13 +7582,13 @@
}
},
"node_modules/@smithy/smithy-client": {
- "version": "4.10.2",
- "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.2.tgz",
- "integrity": "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==",
+ "version": "4.10.3",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.3.tgz",
+ "integrity": "sha512-EfECiO/0fAfb590LBnUe7rI5ux7XfquQ8LBzTe7gxw0j9QW/q8UT/EHWHlxV/+jhQ3+Ssga9uUYXCQgImGMbNg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.20.0",
- "@smithy/middleware-endpoint": "^4.4.1",
+ "@smithy/core": "^3.20.1",
+ "@smithy/middleware-endpoint": "^4.4.2",
"@smithy/middleware-stack": "^4.2.7",
"@smithy/protocol-http": "^5.3.7",
"@smithy/types": "^4.11.0",
@@ -7101,13 +7689,13 @@
}
},
"node_modules/@smithy/util-defaults-mode-browser": {
- "version": "4.3.16",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.16.tgz",
- "integrity": "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==",
+ "version": "4.3.17",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.17.tgz",
+ "integrity": "sha512-dwN4GmivYF1QphnP3xJESXKtHvkkvKHSZI8GrSKMVoENVSKW2cFPRYC4ZgstYjUHdR3zwaDkIaTDIp26JuY7Cw==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/property-provider": "^4.2.7",
- "@smithy/smithy-client": "^4.10.2",
+ "@smithy/smithy-client": "^4.10.3",
"@smithy/types": "^4.11.0",
"tslib": "^2.6.2"
},
@@ -7116,16 +7704,16 @@
}
},
"node_modules/@smithy/util-defaults-mode-node": {
- "version": "4.2.19",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.19.tgz",
- "integrity": "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==",
+ "version": "4.2.20",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.20.tgz",
+ "integrity": "sha512-VD/I4AEhF1lpB3B//pmOIMBNLMrtdMXwy9yCOfa2QkJGDr63vH3RqPbSAKzoGMov3iryCxTXCxSsyGmEB8PDpg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/config-resolver": "^4.4.5",
"@smithy/credential-provider-imds": "^4.2.7",
"@smithy/node-config-provider": "^4.3.7",
"@smithy/property-provider": "^4.2.7",
- "@smithy/smithy-client": "^4.10.2",
+ "@smithy/smithy-client": "^4.10.3",
"@smithy/types": "^4.11.0",
"tslib": "^2.6.2"
},
@@ -16953,6 +17541,10 @@
"resolved": "docs",
"link": true
},
+ "node_modules/nhs-notify-supplier-allocation": {
+ "resolved": "lambdas/allocation",
+ "link": true
+ },
"node_modules/nhs-notify-supplier-api-handler": {
"resolved": "lambdas/api-handler",
"link": true
@@ -16981,6 +17573,10 @@
"resolved": "lambdas/authorizer",
"link": true
},
+ "node_modules/nhs-notify-supplier-events-forwarder": {
+ "resolved": "lambdas/supplier-events-forwarder",
+ "link": true
+ },
"node_modules/nhsuk-frontend": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/nhsuk-frontend/-/nhsuk-frontend-10.2.2.tgz",
@@ -21776,13 +22372,6 @@
"node": ">=18.17"
}
},
- "node_modules/undici-types": {
- "version": "7.18.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
- "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
- "extraneous": true,
- "license": "MIT"
- },
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
diff --git a/package.json b/package.json
index 10db0476..377e3f0d 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"@aws-sdk/client-sns": "^3.936.0",
"@playwright/test": "^1.57.0",
"ajv": "^8.17.1",
+ "aws-lambda": "^1.0.7",
"js-yaml": "^4.1.0",
"get-east-asian-width": "^1.4.0",
"openapi-response-validator": "^12.1.3",