diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md
index 01fe4c4be..d91c28279 100644
--- a/infrastructure/terraform/components/api/README.md
+++ b/infrastructure/terraform/components/api/README.md
@@ -18,7 +18,7 @@ No requirements.
| [default\_tags](#input\_default\_tags) | A map of default tags to apply to all taggable resources within the component | `map(string)` | `{}` | no |
| [disable\_gateway\_execute\_endpoint](#input\_disable\_gateway\_execute\_endpoint) | Disable the execution endpoint for the API Gateway | `bool` | `true` | no |
| [enable\_api\_data\_trace](#input\_enable\_api\_data\_trace) | Enable API Gateway data trace logging | `bool` | `false` | no |
-| [enable\_event\_cache](#input\_enable\_event\_cache) | Enable caching of events to an S3 bucket | `bool` | `false` | no |
+| [enable\_event\_cache](#input\_enable\_event\_cache) | Enable caching of events to an S3 bucket | `bool` | `true` | no |
| [enable\_sns\_delivery\_logging](#input\_enable\_sns\_delivery\_logging) | Enable SNS Delivery Failure Notifications | `bool` | `false` | no |
| [environment](#input\_environment) | The name of the tfscaffold environment | `string` | n/a | yes |
| [eventpub\_control\_plane\_bus\_arn](#input\_eventpub\_control\_plane\_bus\_arn) | ARN of the EventBridge control plane bus for eventpub | `string` | `""` | no |
diff --git a/infrastructure/terraform/components/api/event_source_mapping_mi_updates.tf b/infrastructure/terraform/components/api/event_source_mapping_mi_updates.tf
index 73e7d394f..56a4c7b33 100644
--- a/infrastructure/terraform/components/api/event_source_mapping_mi_updates.tf
+++ b/infrastructure/terraform/components/api/event_source_mapping_mi_updates.tf
@@ -1,11 +1,11 @@
resource "aws_lambda_event_source_mapping" "mi_updates_transformer_kinesis" {
- event_source_arn = aws_kinesis_stream.mi_change_stream.arn
- function_name = module.mi_updates_transformer.function_arn
- starting_position = "LATEST"
- batch_size = 10
- maximum_batching_window_in_seconds = 1
+ event_source_arn = aws_kinesis_stream.mi_change_stream.arn
+ function_name = module.mi_updates_transformer.function_arn
+ starting_position = "LATEST"
+ batch_size = 10
+ maximum_batching_window_in_seconds = 1
depends_on = [
- module.mi_updates_transformer # ensures updates transformer exists
+ module.mi_updates_transformer # ensures updates transformer exists
]
}
diff --git a/infrastructure/terraform/components/api/lambda_event_source_mapping_letter_status_update.tf b/infrastructure/terraform/components/api/lambda_event_source_mapping_letter_status_update.tf
new file mode 100644
index 000000000..82e1c54df
--- /dev/null
+++ b/infrastructure/terraform/components/api/lambda_event_source_mapping_letter_status_update.tf
@@ -0,0 +1,9 @@
+resource "aws_lambda_event_source_mapping" "letter_status_update" {
+ event_source_arn = module.letter_status_updates_queue.sqs_queue_arn
+ function_name = module.letter_status_update.function_name
+ batch_size = 10
+ maximum_batching_window_in_seconds = 5
+ function_response_types = [
+ "ReportBatchItemFailures"
+ ]
+}
diff --git a/infrastructure/terraform/components/api/locals.tf b/infrastructure/terraform/components/api/locals.tf
index 683156a05..dfefd9dda 100644
--- a/infrastructure/terraform/components/api/locals.tf
+++ b/infrastructure/terraform/components/api/locals.tf
@@ -27,6 +27,8 @@ locals {
SUPPLIER_ID_HEADER = "nhsd-supplier-id",
APIM_CORRELATION_HEADER = "nhsd-correlation-id",
DOWNLOAD_URL_TTL_SECONDS = 60
+ AMENDMENTS_TOPIC_ARN = "${module.eventsub.amendments_topic.arn}",
+ EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters"
}
core_pdf_bucket_arn = "arn:aws:s3:::comms-${var.core_account_id}-eu-west-2-${var.core_environment}-api-stg-pdf-pipeline"
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 59393bd29..124e8fe89 100644
--- a/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf
+++ b/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf
@@ -59,7 +59,6 @@ data "aws_iam_policy_document" "letter_status_update" {
actions = [
"dynamodb:GetItem",
"dynamodb:Query",
- "dynamodb:UpdateItem",
]
resources = [
@@ -82,4 +81,17 @@ data "aws_iam_policy_document" "letter_status_update" {
module.letter_status_updates_queue.sqs_queue_arn
]
}
+
+ statement {
+ sid = "AllowSNSPublish"
+ effect = "Allow"
+
+ actions = [
+ "sns:Publish"
+ ]
+
+ resources = [
+ module.eventsub.amendments_topic.arn
+ ]
+ }
}
diff --git a/infrastructure/terraform/components/api/module_lambda_letter_updates_transformer.tf b/infrastructure/terraform/components/api/module_lambda_letter_updates_transformer.tf
index 52a444ec4..0f263b281 100644
--- a/infrastructure/terraform/components/api/module_lambda_letter_updates_transformer.tf
+++ b/infrastructure/terraform/components/api/module_lambda_letter_updates_transformer.tf
@@ -36,7 +36,7 @@ module "letter_updates_transformer" {
lambda_env_vars = merge(local.common_lambda_env_vars, {
EVENTPUB_SNS_TOPIC_ARN = "${module.eventpub.sns_topic.arn}",
- EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters"
+ EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters"
})
}
diff --git a/infrastructure/terraform/components/api/module_sqs_letter_updates.tf b/infrastructure/terraform/components/api/module_sqs_letter_updates.tf
index 472afb81f..61213791a 100644
--- a/infrastructure/terraform/components/api/module_sqs_letter_updates.tf
+++ b/infrastructure/terraform/components/api/module_sqs_letter_updates.tf
@@ -18,29 +18,6 @@ module "sqs_letter_updates" {
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"
@@ -65,7 +42,7 @@ data "aws_iam_policy_document" "letter_updates_queue_policy" {
condition {
test = "ArnEquals"
variable = "aws:SourceArn"
- values = [module.eventsub.sns_topic.arn]
+ values = [module.eventsub.eventsub_topic.arn, module.eventsub.amendments_topic.arn]
}
}
}
diff --git a/infrastructure/terraform/components/api/modules_eventsub.tf b/infrastructure/terraform/components/api/modules_eventsub.tf
index c55be96c0..661ff9999 100644
--- a/infrastructure/terraform/components/api/modules_eventsub.tf
+++ b/infrastructure/terraform/components/api/modules_eventsub.tf
@@ -22,7 +22,7 @@ module "eventsub" {
sns_success_logging_sample_percent = var.sns_success_logging_sample_percent
event_cache_expiry_days = 30
- enable_event_cache = var.enable_event_cache
+ enable_event_cache = var.enable_event_cache
shared_infra_account_id = var.shared_infra_account_id
}
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
index 9c232c149..424ebf0da 100644
--- 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
@@ -1,5 +1,11 @@
resource "aws_sns_topic_subscription" "eventsub_sqs_letter_updates" {
- topic_arn = module.eventsub.sns_topic.arn
+ topic_arn = module.eventsub.eventsub_topic.arn
+ protocol = "sqs"
+ endpoint = module.sqs_letter_updates.sqs_queue_arn
+}
+
+resource "aws_sns_topic_subscription" "amendments_sqs_letter_updates" {
+ topic_arn = module.eventsub.amendments_topic.arn
protocol = "sqs"
endpoint = module.sqs_letter_updates.sqs_queue_arn
}
diff --git a/infrastructure/terraform/components/api/variables.tf b/infrastructure/terraform/components/api/variables.tf
index 47928a960..2439d69f2 100644
--- a/infrastructure/terraform/components/api/variables.tf
+++ b/infrastructure/terraform/components/api/variables.tf
@@ -167,7 +167,7 @@ variable "core_environment" {
variable "enable_event_cache" {
type = bool
description = "Enable caching of events to an S3 bucket"
- default = false
+ default = true
}
variable "enable_sns_delivery_logging" {
diff --git a/infrastructure/terraform/modules/eventsub/README.md b/infrastructure/terraform/modules/eventsub/README.md
index 859c5fd5b..55c9e876a 100644
--- a/infrastructure/terraform/modules/eventsub/README.md
+++ b/infrastructure/terraform/modules/eventsub/README.md
@@ -39,8 +39,9 @@
| Name | Description |
|------|-------------|
+| [amendments\_topic](#output\_amendments\_topic) | Amendments SNS Topic ARN and Name |
+| [eventsub\_topic](#output\_eventsub\_topic) | SNS Topic 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 |
diff --git a/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_failure.tf b/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_failure.tf
index 28a7ecfb5..da12c0204 100644
--- a/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_failure.tf
+++ b/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_failure.tf
@@ -7,3 +7,13 @@ resource "aws_cloudwatch_log_group" "sns_delivery_logging_failure" {
kms_key_id = var.kms_key_arn
retention_in_days = var.log_retention_in_days
}
+
+resource "aws_cloudwatch_log_group" "amendments_sns_delivery_logging_failure" {
+ count = var.enable_sns_delivery_logging ? 1 : 0
+
+ # SNS doesn't allow specifying a log group and is derived as: sns/${region}/${account_id}/${name_of_sns_topic}/Failure
+ # (for failure logs)
+ name = "sns/${var.region}/${var.aws_account_id}/${local.csi}-amendments/Failure"
+ kms_key_id = var.kms_key_arn
+ retention_in_days = var.log_retention_in_days
+}
diff --git a/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_success.tf b/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_success.tf
index f760e8561..bac190f3f 100644
--- a/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_success.tf
+++ b/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_success.tf
@@ -7,3 +7,13 @@ resource "aws_cloudwatch_log_group" "sns_delivery_logging_success" {
kms_key_id = var.kms_key_arn
retention_in_days = var.log_retention_in_days
}
+
+resource "aws_cloudwatch_log_group" "amendments_sns_delivery_logging_success" {
+ count = var.enable_sns_delivery_logging ? 1 : 0
+
+ # SNS doesn't allow specifying a log group and is derived as: sns/${region}/${account_id}/${name_of_sns_topic}
+ # (for success logs)
+ name = "sns/${var.region}/${var.aws_account_id}/${local.csi}-amendments"
+ kms_key_id = var.kms_key_arn
+ retention_in_days = var.log_retention_in_days
+}
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 e8ef1249d..f04812dd7 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,23 @@ resource "aws_cloudwatch_metric_alarm" "sns_delivery_failures" {
treat_missing_data = "notBreaching"
dimensions = {
- TopicName = aws_sns_topic.main.name
+ TopicName = aws_sns_topic.eventsub_topic.name
+ }
+}
+
+resource "aws_cloudwatch_metric_alarm" "amendments_delivery_failures" {
+ alarm_name = "${local.csi}-amendments-sns-delivery-failures"
+ alarm_description = "RELIABILITY: Alarm for amendments SNS topic delivery failures"
+ comparison_operator = "GreaterThanThreshold"
+ evaluation_periods = 1
+ metric_name = "NumberOfNotificationsFailed"
+ namespace = "AWS/SNS"
+ period = 300
+ statistic = "Sum"
+ threshold = 0
+ treat_missing_data = "notBreaching"
+
+ dimensions = {
+ TopicName = aws_sns_topic.amendments_topic.name
}
}
diff --git a/infrastructure/terraform/modules/eventsub/iam_policy_sns_delivery_logging_cloudwatch.tf b/infrastructure/terraform/modules/eventsub/iam_policy_sns_delivery_logging_cloudwatch.tf
index d296da2dd..c18b30fac 100644
--- a/infrastructure/terraform/modules/eventsub/iam_policy_sns_delivery_logging_cloudwatch.tf
+++ b/infrastructure/terraform/modules/eventsub/iam_policy_sns_delivery_logging_cloudwatch.tf
@@ -39,6 +39,10 @@ data "aws_iam_policy_document" "sns_delivery_logging_cloudwatch" {
"${aws_cloudwatch_log_group.sns_delivery_logging_success[0].arn}:log-stream:*",
aws_cloudwatch_log_group.sns_delivery_logging_failure[0].arn,
"${aws_cloudwatch_log_group.sns_delivery_logging_failure[0].arn}:log-stream:*",
+ aws_cloudwatch_log_group.amendments_sns_delivery_logging_success[0].arn,
+ "${aws_cloudwatch_log_group.amendments_sns_delivery_logging_success[0].arn}:log-stream:*",
+ aws_cloudwatch_log_group.amendments_sns_delivery_logging_failure[0].arn,
+ "${aws_cloudwatch_log_group.amendments_sns_delivery_logging_failure[0].arn}:log-stream:*",
]
}
}
diff --git a/infrastructure/terraform/modules/eventsub/iam_role_firehose_role.tf b/infrastructure/terraform/modules/eventsub/iam_role_firehose_role.tf
index 69bf7618c..20414cc9a 100644
--- a/infrastructure/terraform/modules/eventsub/iam_role_firehose_role.tf
+++ b/infrastructure/terraform/modules/eventsub/iam_role_firehose_role.tf
@@ -1,8 +1,8 @@
resource "aws_iam_role" "firehose_role" {
count = var.enable_event_cache ? 1 : 0
- name = "${local.csi}-firehose-role"
- assume_role_policy = data.aws_iam_policy_document.firehose_assume_role[0].json
+ name = "${local.csi}-firehose-role"
+ assume_role_policy = data.aws_iam_policy_document.firehose_assume_role[0].json
}
data "aws_iam_policy_document" "firehose_assume_role" {
diff --git a/infrastructure/terraform/modules/eventsub/iam_role_sns.tf b/infrastructure/terraform/modules/eventsub/iam_role_sns.tf
index d88bea7a1..97bdc99af 100644
--- a/infrastructure/terraform/modules/eventsub/iam_role_sns.tf
+++ b/infrastructure/terraform/modules/eventsub/iam_role_sns.tf
@@ -1,6 +1,6 @@
resource "aws_iam_role" "sns_role" {
- name = "${local.csi}-sns-role"
- assume_role_policy = data.aws_iam_policy_document.sns_assume_role.json
+ name = "${local.csi}-sns-role"
+ assume_role_policy = data.aws_iam_policy_document.sns_assume_role.json
}
resource "aws_iam_policy" "firehose_delivery" {
diff --git a/infrastructure/terraform/modules/eventsub/iam_role_sns_delivery_logging.tf b/infrastructure/terraform/modules/eventsub/iam_role_sns_delivery_logging.tf
index 3bd25e06f..a952bfeee 100644
--- a/infrastructure/terraform/modules/eventsub/iam_role_sns_delivery_logging.tf
+++ b/infrastructure/terraform/modules/eventsub/iam_role_sns_delivery_logging.tf
@@ -1,8 +1,8 @@
resource "aws_iam_role" "sns_delivery_logging_role" {
count = var.enable_sns_delivery_logging ? 1 : 0
- name = "${local.csi}-sns-delivery-logging"
- assume_role_policy = data.aws_iam_policy_document.sns_delivery_logging_assume_role[0].json
+ name = "${local.csi}-sns-delivery-logging"
+ assume_role_policy = data.aws_iam_policy_document.sns_delivery_logging_assume_role[0].json
}
data "aws_iam_policy_document" "sns_delivery_logging_assume_role" {
diff --git a/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf b/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf
index f042c2d22..d51f34267 100644
--- a/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf
+++ b/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf
@@ -48,7 +48,7 @@ module "s3bucket_event_cache" {
}
default_tags = {
- Name = "Event Cache Storage"
+ Name = "Event Cache Storage"
NHSE-Enable-S3-Backup-Acct = "True"
}
}
diff --git a/infrastructure/terraform/modules/eventsub/moved.tf b/infrastructure/terraform/modules/eventsub/moved.tf
new file mode 100644
index 000000000..433685ffa
--- /dev/null
+++ b/infrastructure/terraform/modules/eventsub/moved.tf
@@ -0,0 +1,11 @@
+# Moved blocks to handle resource renames without destroy/recreate
+
+moved {
+ from = aws_sns_topic.main
+ to = aws_sns_topic.eventsub_topic
+}
+
+moved {
+ from = aws_sns_topic_policy.main
+ to = aws_sns_topic_policy.eventsub_topic
+}
diff --git a/infrastructure/terraform/modules/eventsub/outputs.tf b/infrastructure/terraform/modules/eventsub/outputs.tf
index e2ff3b38e..891049a37 100644
--- a/infrastructure/terraform/modules/eventsub/outputs.tf
+++ b/infrastructure/terraform/modules/eventsub/outputs.tf
@@ -1,8 +1,16 @@
-output "sns_topic" {
+output "eventsub_topic" {
description = "SNS Topic ARN and Name"
value = {
- arn = aws_sns_topic.main.arn
- name = aws_sns_topic.main.name
+ arn = aws_sns_topic.eventsub_topic.arn
+ name = aws_sns_topic.eventsub_topic.name
+ }
+}
+
+output "amendments_topic" {
+ description = "Amendments SNS Topic ARN and Name"
+ value = {
+ arn = aws_sns_topic.amendments_topic.arn
+ name = aws_sns_topic.amendments_topic.name
}
}
diff --git a/infrastructure/terraform/modules/eventsub/sns_topic.tf b/infrastructure/terraform/modules/eventsub/sns_topic.tf
deleted file mode 100644
index cc30db153..000000000
--- a/infrastructure/terraform/modules/eventsub/sns_topic.tf
+++ /dev/null
@@ -1,24 +0,0 @@
-resource "aws_sns_topic" "main" {
- name = local.csi
- 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
- 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/infrastructure/terraform/modules/eventsub/sns_topic_eventsub.tf b/infrastructure/terraform/modules/eventsub/sns_topic_eventsub.tf
new file mode 100644
index 000000000..d4631657e
--- /dev/null
+++ b/infrastructure/terraform/modules/eventsub/sns_topic_eventsub.tf
@@ -0,0 +1,49 @@
+resource "aws_sns_topic" "eventsub_topic" {
+ name = local.csi
+ 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
+ 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
+}
+
+resource "aws_sns_topic" "amendments_topic" {
+ name = "${local.csi}-amendments"
+ 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
+ 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/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf b/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf
deleted file mode 100644
index a772e9e72..000000000
--- a/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf
+++ /dev/null
@@ -1,63 +0,0 @@
-resource "aws_sns_topic_policy" "main" {
- arn = aws_sns_topic.main.arn
-
- policy = data.aws_iam_policy_document.sns_topic_policy.json
-}
-
-data "aws_iam_policy_document" "sns_topic_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.main.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.main.arn,
- ]
- }
-}
diff --git a/infrastructure/terraform/modules/eventsub/sns_topic_policy_eventsub.tf b/infrastructure/terraform/modules/eventsub/sns_topic_policy_eventsub.tf
new file mode 100644
index 000000000..d2fb2fce1
--- /dev/null
+++ b/infrastructure/terraform/modules/eventsub/sns_topic_policy_eventsub.tf
@@ -0,0 +1,127 @@
+resource "aws_sns_topic_policy" "eventsub_topic" {
+ arn = aws_sns_topic.eventsub_topic.arn
+
+ policy = data.aws_iam_policy_document.sns_topic_policy.json
+}
+
+resource "aws_sns_topic_policy" "amendments_topic" {
+ arn = aws_sns_topic.amendments_topic.arn
+
+ policy = data.aws_iam_policy_document.amendments_topic_policy.json
+}
+
+data "aws_iam_policy_document" "sns_topic_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.eventsub_topic.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.eventsub_topic.arn,
+ ]
+ }
+}
+
+data "aws_iam_policy_document" "amendments_topic_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.eventsub_topic.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.amendments_topic.arn,
+ ]
+ }
+}
diff --git a/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf b/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf
index 42457f6de..32ad06f01 100644
--- a/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf
+++ b/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf
@@ -1,7 +1,17 @@
-resource "aws_sns_topic_subscription" "firehose" {
+resource "aws_sns_topic_subscription" "firehose_eventsub" {
count = var.enable_event_cache ? 1 : 0
- topic_arn = aws_sns_topic.main.arn
+ topic_arn = aws_sns_topic.eventsub_topic.arn
+ protocol = "firehose"
+ subscription_role_arn = aws_iam_role.sns_role.arn
+ endpoint = aws_kinesis_firehose_delivery_stream.main[0].arn
+ raw_message_delivery = var.enable_firehose_raw_message_delivery
+}
+
+resource "aws_sns_topic_subscription" "firehose_amendments" {
+ count = var.enable_event_cache ? 1 : 0
+
+ topic_arn = aws_sns_topic.amendments_topic.arn
protocol = "firehose"
subscription_role_arn = aws_iam_role.sns_role.arn
endpoint = aws_kinesis_firehose_delivery_stream.main[0].arn
diff --git a/internal/datastore/src/__test__/letter-repository.test.ts b/internal/datastore/src/__test__/letter-repository.test.ts
index 4c44ddbe6..193c1c077 100644
--- a/internal/datastore/src/__test__/letter-repository.test.ts
+++ b/internal/datastore/src/__test__/letter-repository.test.ts
@@ -14,10 +14,12 @@ function createLetter(
supplierId: string,
letterId: string,
status: Letter["status"] = "PENDING",
+ eventId?: string,
): InsertLetter {
const now = new Date().toISOString();
return {
id: letterId,
+ eventId,
supplierId,
specificationId: "specification1",
groupId: "group1",
@@ -168,6 +170,7 @@ describe("LetterRepository", () => {
const updateLetter: UpdateLetter = {
id: "letter1",
+ eventId: "event1",
supplierId: "supplier1",
status: "REJECTED",
reasonCode: "R01",
@@ -180,6 +183,7 @@ describe("LetterRepository", () => {
"letter1",
);
expect(updatedLetter.status).toBe("REJECTED");
+ expect(updatedLetter.previousStatus).toBe("PENDING");
expect(updatedLetter.reasonCode).toBe("R01");
expect(updatedLetter.reasonText).toBe("Reason text");
});
@@ -199,6 +203,7 @@ describe("LetterRepository", () => {
jest.setSystemTime(new Date(2020, 1, 2));
const letterDto: UpdateLetter = {
id: "letter1",
+ eventId: "event1",
supplierId: "supplier1",
status: "DELIVERED",
};
@@ -215,6 +220,7 @@ describe("LetterRepository", () => {
test("can't update a letter that does not exist", async () => {
const updateLetter: UpdateLetter = {
id: "letter1",
+ eventId: "event1",
supplierId: "supplier1",
status: "DELIVERED",
};
@@ -233,6 +239,7 @@ describe("LetterRepository", () => {
const updateLetter: UpdateLetter = {
id: "letter1",
+ eventId: "event1",
supplierId: "supplier1",
status: "DELIVERED",
};
@@ -241,6 +248,52 @@ describe("LetterRepository", () => {
).rejects.toThrow("Cannot do operations on a non-existent table");
});
+ test("does not update a letter if the same eventId is used", async () => {
+ const letter = createLetter("supplier1", "letter1", "DELIVERED", "event1");
+ await letterRepository.putLetter(letter);
+
+ const duplicateUpdate: UpdateLetter = {
+ id: "letter1",
+ eventId: "event1",
+ supplierId: "supplier1",
+ status: "REJECTED",
+ reasonCode: "R01",
+ };
+ const result = await letterRepository.updateLetterStatus(duplicateUpdate);
+
+ expect(result).toBeUndefined();
+ const unchangedLetter = await letterRepository.getLetterById(
+ "supplier1",
+ "letter1",
+ );
+ expect(unchangedLetter.status).toBe("DELIVERED");
+ expect(unchangedLetter.eventId).toBe("event1");
+ expect(unchangedLetter.reasonCode).toBeUndefined();
+ });
+
+ test("updates a letter if a different eventId is used", async () => {
+ const letter = createLetter("supplier1", "letter1", "DELIVERED", "event1");
+ await letterRepository.putLetter(letter);
+
+ const duplicateUpdate: UpdateLetter = {
+ id: "letter1",
+ eventId: "event2",
+ supplierId: "supplier1",
+ status: "REJECTED",
+ reasonCode: "R01",
+ };
+ const result = await letterRepository.updateLetterStatus(duplicateUpdate);
+
+ expect(result).toBeDefined();
+ const changedLetter = await letterRepository.getLetterById(
+ "supplier1",
+ "letter1",
+ );
+ expect(changedLetter.status).toBe("REJECTED");
+ expect(changedLetter.eventId).toBe("event2");
+ expect(changedLetter.reasonCode).toBe("R01");
+ });
+
test("should return a list of letters matching status", async () => {
await letterRepository.putLetter(createLetter("supplier1", "letter1"));
await letterRepository.putLetter(createLetter("supplier1", "letter2"));
@@ -278,6 +331,7 @@ describe("LetterRepository", () => {
const updateLetter: UpdateLetter = {
id: "letter1",
+ eventId: "event1",
supplierId: "supplier1",
status: "DELIVERED",
};
diff --git a/internal/datastore/src/letter-repository.ts b/internal/datastore/src/letter-repository.ts
index f22868789..def7c1b36 100644
--- a/internal/datastore/src/letter-repository.ts
+++ b/internal/datastore/src/letter-repository.ts
@@ -7,6 +7,7 @@ import {
UpdateCommand,
UpdateCommandOutput,
} from "@aws-sdk/lib-dynamodb";
+import { ConditionalCheckFailedException } from "@aws-sdk/client-dynamodb";
import { Logger } from "pino";
import { z } from "zod";
import {
@@ -163,32 +164,16 @@ export class LetterRepository {
};
}
- async updateLetterStatus(letterToUpdate: UpdateLetter): Promise {
+ async updateLetterStatus(
+ letterToUpdate: UpdateLetter,
+ ): Promise {
this.log.debug(
`Updating letter ${letterToUpdate.id} to status ${letterToUpdate.status}`,
);
let result: UpdateCommandOutput;
try {
- let updateExpression =
- "set #status = :status, updatedAt = :updatedAt, supplierStatus = :supplierStatus, #ttl = :ttl";
- const expressionAttributeValues: Record = {
- ":status": letterToUpdate.status,
- ":updatedAt": new Date().toISOString(),
- ":supplierStatus": `${letterToUpdate.supplierId}#${letterToUpdate.status}`,
- ":ttl": Math.floor(
- Date.now() / 1000 + 60 * 60 * this.config.lettersTtlHours,
- ),
- };
-
- if (letterToUpdate.reasonCode) {
- updateExpression += ", reasonCode = :reasonCode";
- expressionAttributeValues[":reasonCode"] = letterToUpdate.reasonCode;
- }
-
- if (letterToUpdate.reasonText) {
- updateExpression += ", reasonText = :reasonText";
- expressionAttributeValues[":reasonText"] = letterToUpdate.reasonText;
- }
+ const { expressionAttributeValues, updateExpression } =
+ this.buildUpdateExpression(letterToUpdate);
result = await this.ddbClient.send(
new UpdateCommand({
@@ -198,31 +183,61 @@ export class LetterRepository {
supplierId: letterToUpdate.supplierId,
},
UpdateExpression: updateExpression,
- ConditionExpression: "attribute_exists(id)", // Ensure letter exists
+ ConditionExpression:
+ "attribute_exists(id) AND (attribute_not_exists(eventId) OR eventId <> :eventId)",
ExpressionAttributeNames: {
"#status": "status",
"#ttl": "ttl",
},
ExpressionAttributeValues: expressionAttributeValues,
ReturnValues: "ALL_NEW",
+ ReturnValuesOnConditionCheckFailure: "ALL_OLD",
}),
);
+
+ this.log.debug(
+ `Updated letter ${letterToUpdate.id} to status ${letterToUpdate.status}`,
+ );
+ return LetterSchema.parse(result.Attributes);
} catch (error) {
- if (
- error instanceof Error &&
- error.name === "ConditionalCheckFailedException"
- ) {
+ if (error instanceof ConditionalCheckFailedException) {
+ if (error.Item?.eventId.S === letterToUpdate.eventId) {
+ this.log.warn(
+ `Skipping update for letter ${letterToUpdate.id}: eventId ${letterToUpdate.eventId} already processed`,
+ );
+ return undefined;
+ }
throw new Error(
`Letter with id ${letterToUpdate.id} not found for supplier ${letterToUpdate.supplierId}`,
);
}
throw error;
}
+ }
- this.log.debug(
- `Updated letter ${letterToUpdate.id} to status ${letterToUpdate.status}`,
- );
- return LetterSchema.parse(result.Attributes);
+ private buildUpdateExpression(letterToUpdate: UpdateLetter) {
+ let updateExpression = `set #status = :status, previousStatus = #status, updatedAt = :updatedAt, supplierStatus = :supplierStatus,
+ #ttl = :ttl, eventId = :eventId`;
+ const expressionAttributeValues: Record = {
+ ":status": letterToUpdate.status,
+ ":updatedAt": new Date().toISOString(),
+ ":supplierStatus": `${letterToUpdate.supplierId}#${letterToUpdate.status}`,
+ ":ttl": Math.floor(
+ Date.now() / 1000 + 60 * 60 * this.config.lettersTtlHours,
+ ),
+ ":eventId": letterToUpdate.eventId,
+ };
+
+ if (letterToUpdate.reasonCode) {
+ updateExpression += ", reasonCode = :reasonCode";
+ expressionAttributeValues[":reasonCode"] = letterToUpdate.reasonCode;
+ }
+
+ if (letterToUpdate.reasonText) {
+ updateExpression += ", reasonText = :reasonText";
+ expressionAttributeValues[":reasonText"] = letterToUpdate.reasonText;
+ }
+ return { updateExpression, expressionAttributeValues };
}
async getLettersBySupplier(
diff --git a/internal/datastore/src/types.ts b/internal/datastore/src/types.ts
index a0b9f719c..fd83e0403 100644
--- a/internal/datastore/src/types.ts
+++ b/internal/datastore/src/types.ts
@@ -42,9 +42,11 @@ export const LetterSchemaBase = z.object({
export const LetterSchema = LetterSchemaBase.extend({
supplierId: idRef(SupplierSchema, "id"),
+ eventId: z.string().optional(),
url: z.url(),
createdAt: z.string(),
updatedAt: z.string(),
+ previousStatus: LetterStatus.optional(),
supplierStatus: z.string().describe("Secondary index PK"),
supplierStatusSk: z.string().describe("Secondary index SK"),
ttl: z.int(),
@@ -67,6 +69,7 @@ export type InsertLetter = Omit<
>;
export type UpdateLetter = {
id: string;
+ eventId: string;
supplierId: string;
status: Letter["status"];
reasonCode?: string;
diff --git a/internal/events/jest.config.ts b/internal/events/jest.config.ts
index 84251001b..926706a37 100644
--- a/internal/events/jest.config.ts
+++ b/internal/events/jest.config.ts
@@ -24,7 +24,7 @@ export const baseJestConfig: Config = {
},
},
- coveragePathIgnorePatterns: ["/__tests__/"],
+ coveragePathIgnorePatterns: ["/src/index.ts$", "/__tests__/"],
transform: { "^.+\\.ts$": "ts-jest" },
testPathIgnorePatterns: [".build"],
testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],
diff --git a/internal/events/package.json b/internal/events/package.json
index 135961c97..51fee7656 100644
--- a/internal/events/package.json
+++ b/internal/events/package.json
@@ -1,6 +1,7 @@
{
"dependencies": {
"@asyncapi/bundler": "^0.6.4",
+ "@internal/datastore": "*",
"zod": "^4.1.11"
},
"description": "Schemas for NHS Notify Supplier API events",
@@ -50,5 +51,5 @@
"typecheck": "tsc --noEmit"
},
"types": "dist/index.d.ts",
- "version": "1.0.10"
+ "version": "1.0.11"
}
diff --git a/internal/events/src/__tests__/version.test.ts b/internal/events/src/__tests__/version.test.ts
new file mode 100644
index 000000000..412eef4ce
--- /dev/null
+++ b/internal/events/src/__tests__/version.test.ts
@@ -0,0 +1,11 @@
+import { MAJOR_VERSION, VERSION } from "../version";
+
+describe("version exports", () => {
+ it("should export MAJOR_VERSION as the first segment of the version", () => {
+ expect(VERSION.startsWith(`${MAJOR_VERSION}.`)).toBeTruthy();
+ });
+
+ it("should have VERSION in semver format", () => {
+ expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/);
+ });
+});
diff --git a/lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts b/internal/events/src/events/__tests__/letter-mapper.test.ts
similarity index 94%
rename from lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts
rename to internal/events/src/events/__tests__/letter-mapper.test.ts
index 077d73792..c870dc91b 100644
--- a/lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts
+++ b/internal/events/src/events/__tests__/letter-mapper.test.ts
@@ -1,6 +1,6 @@
import { $LetterEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src";
import { Letter } from "@internal/datastore";
-import mapLetterToCloudEvent from "../letter-mapper";
+import { mapLetterToCloudEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-mapper";
describe("letter-mapper", () => {
it("maps a letter to a letter event", async () => {
diff --git a/lambdas/letter-updates-transformer/src/mappers/letter-mapper.ts b/internal/events/src/events/letter-mapper.ts
similarity index 86%
rename from lambdas/letter-updates-transformer/src/mappers/letter-mapper.ts
rename to internal/events/src/events/letter-mapper.ts
index f2f25a827..91f72988a 100644
--- a/lambdas/letter-updates-transformer/src/mappers/letter-mapper.ts
+++ b/internal/events/src/events/letter-mapper.ts
@@ -1,10 +1,11 @@
-import { LetterEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src";
import { randomBytes, randomUUID } from "node:crypto";
import eventSchemaPackage from "@nhsdigital/nhs-notify-event-schemas-supplier-api/package.json";
-import { LetterForEventPub } from "../types";
+import { Letter } from "@internal/datastore";
+import { LetterEvent } from "./letter-events";
-export default function mapLetterToCloudEvent(
- letter: LetterForEventPub,
+// eslint-disable-next-line import-x/prefer-default-export
+export function mapLetterToCloudEvent(
+ letter: Letter,
source: string,
): LetterEvent {
const eventId = randomUUID();
diff --git a/internal/events/src/index.ts b/internal/events/src/index.ts
index 339ddcd64..6aecfa516 100644
--- a/internal/events/src/index.ts
+++ b/internal/events/src/index.ts
@@ -4,4 +4,5 @@ export { default as DomainBase } from "./domain/domain-base";
export * from "./events/event-envelope";
export * from "./events/letter-events";
export * from "./events/mi-events";
+export * from "./events/letter-mapper";
export * from "./version";
diff --git a/internal/events/tsconfig.json b/internal/events/tsconfig.json
index 167e805ad..ad730e287 100644
--- a/internal/events/tsconfig.json
+++ b/internal/events/tsconfig.json
@@ -3,8 +3,7 @@
"declaration": true,
"isolatedModules": true,
"module": "commonjs",
- "outDir": "dist",
- "resolveJsonModule": true
+ "outDir": "dist"
},
"exclude": [
"node_modules",
diff --git a/lambdas/api-handler/package.json b/lambdas/api-handler/package.json
index 1306f33d3..5ebfed49f 100644
--- a/lambdas/api-handler/package.json
+++ b/lambdas/api-handler/package.json
@@ -2,11 +2,13 @@
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.925.0",
"@aws-sdk/client-s3": "^3.925.0",
+ "@aws-sdk/client-sns": "^3.925.0",
"@aws-sdk/client-sqs": "^3.925.0",
"@aws-sdk/lib-dynamodb": "^3.925.0",
"@aws-sdk/s3-request-presigner": "^3.925.0",
"@internal/datastore": "*",
"@internal/helpers": "*",
+ "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*",
"aws-lambda": "^1.0.7",
"esbuild": "^0.25.11",
"pino": "^9.7.0",
diff --git a/lambdas/api-handler/src/config/__tests__/env.test.ts b/lambdas/api-handler/src/config/__tests__/env.test.ts
index afbca1d82..bb7fc9613 100644
--- a/lambdas/api-handler/src/config/__tests__/env.test.ts
+++ b/lambdas/api-handler/src/config/__tests__/env.test.ts
@@ -25,6 +25,8 @@ describe("lambdaEnv", () => {
process.env.DOWNLOAD_URL_TTL_SECONDS = "60";
process.env.MAX_LIMIT = "2500";
process.env.QUEUE_URL = "url";
+ process.env.EVENT_SOURCE = "supplier-api";
+ process.env.AMENDMENTS_TOPIC_ARN = "sns-topic.arn";
const { envVars } = require("../env");
@@ -38,6 +40,8 @@ describe("lambdaEnv", () => {
DOWNLOAD_URL_TTL_SECONDS: 60,
MAX_LIMIT: 2500,
QUEUE_URL: "url",
+ EVENT_SOURCE: "supplier-api",
+ AMENDMENTS_TOPIC_ARN: "sns-topic.arn",
});
});
@@ -61,6 +65,8 @@ describe("lambdaEnv", () => {
process.env.LETTER_TTL_HOURS = "12960";
process.env.MI_TTL_HOURS = "2160";
process.env.DOWNLOAD_URL_TTL_SECONDS = "60";
+ process.env.EVENT_SOURCE = "supplier-api";
+ process.env.AMENDMENTS_TOPIC_ARN = "sns-topic.arn";
const { envVars } = require("../env");
@@ -73,6 +79,8 @@ describe("lambdaEnv", () => {
MI_TTL_HOURS: 2160,
DOWNLOAD_URL_TTL_SECONDS: 60,
MAX_LIMIT: undefined,
+ EVENT_SOURCE: "supplier-api",
+ AMENDMENTS_TOPIC_ARN: "sns-topic.arn",
});
});
});
diff --git a/lambdas/api-handler/src/config/deps.ts b/lambdas/api-handler/src/config/deps.ts
index c8d187bf4..0c5a6fa27 100644
--- a/lambdas/api-handler/src/config/deps.ts
+++ b/lambdas/api-handler/src/config/deps.ts
@@ -2,6 +2,7 @@ import { S3Client } from "@aws-sdk/client-s3";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { SQSClient } from "@aws-sdk/client-sqs";
+import { SNSClient } from "@aws-sdk/client-sns";
import pino from "pino";
import {
DBHealthcheck,
@@ -13,6 +14,7 @@ import { EnvVars, envVars } from "./env";
export type Deps = {
s3Client: S3Client;
sqsClient: SQSClient;
+ snsClient: SNSClient;
letterRepo: LetterRepository;
miRepo: MIRepository;
dbHealthcheck: DBHealthcheck;
@@ -64,6 +66,7 @@ export function createDependenciesContainer(): Deps {
return {
s3Client: new S3Client(),
sqsClient: new SQSClient(),
+ snsClient: new SNSClient(),
letterRepo: createLetterRepository(log, envVars),
miRepo: createMIRepository(log, envVars),
dbHealthcheck: createDBHealthcheck(envVars),
diff --git a/lambdas/api-handler/src/config/env.ts b/lambdas/api-handler/src/config/env.ts
index fbdc0924d..170926a7c 100644
--- a/lambdas/api-handler/src/config/env.ts
+++ b/lambdas/api-handler/src/config/env.ts
@@ -10,6 +10,8 @@ const EnvVarsSchema = z.object({
DOWNLOAD_URL_TTL_SECONDS: z.coerce.number().int(),
MAX_LIMIT: z.coerce.number().int().optional(),
QUEUE_URL: z.coerce.string().optional(),
+ EVENT_SOURCE: z.string(),
+ AMENDMENTS_TOPIC_ARN: z.string(),
});
export type EnvVars = z.infer;
diff --git a/lambdas/api-handler/src/handlers/__tests__/letter-status-update.test.ts b/lambdas/api-handler/src/handlers/__tests__/letter-status-update.test.ts
index 908ba1a3d..808372e95 100644
--- a/lambdas/api-handler/src/handlers/__tests__/letter-status-update.test.ts
+++ b/lambdas/api-handler/src/handlers/__tests__/letter-status-update.test.ts
@@ -1,13 +1,25 @@
import { Context, SQSEvent, SQSRecord } from "aws-lambda";
import { mockDeep } from "jest-mock-extended";
-import { S3Client } from "@aws-sdk/client-s3";
import pino from "pino";
-import { LetterRepository } from "@internal/datastore/src";
+import { SNSClient } from "@aws-sdk/client-sns";
+import { mapLetterToCloudEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-mapper";
+import { Letter, LetterRepository } from "@internal/datastore/src";
import { UpdateLetterCommand } from "../../contracts/letters";
import { EnvVars } from "../../config/env";
import { Deps } from "../../config/deps";
import createLetterStatusUpdateHandler from "../letter-status-update";
+// Make crypto return consistent values, since we"re calling it in both prod and test code and comparing the values
+const realCrypto = jest.requireActual("crypto");
+const randomBytes: Record = {
+ "8": realCrypto.randomBytes(8),
+ "16": realCrypto.randomBytes(16),
+};
+jest.mock("crypto", () => ({
+ randomUUID: () => "4616b2d9-b7a5-45aa-8523-fa7419626b69",
+ randomBytes: (size: number) => randomBytes[String(size)],
+}));
+
const buildEvent = (updateLetterCommand: UpdateLetterCommand[]): SQSEvent => {
const records: Partial[] = updateLetterCommand.map((letter) => {
return {
@@ -30,51 +42,64 @@ const buildEvent = (updateLetterCommand: UpdateLetterCommand[]): SQSEvent => {
};
describe("createLetterStatusUpdateHandler", () => {
- beforeEach(() => {
+ beforeEach(async () => {
jest.clearAllMocks();
});
+ const mockedDeps: jest.Mocked = {
+ snsClient: { send: jest.fn() } as unknown as SNSClient,
+ letterRepo: {
+ getLetterById: jest.fn(),
+ } as unknown as LetterRepository,
+ logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
+ env: {
+ EVENT_SOURCE: "supplier-api",
+ SNS_TOPIC_ARN: "sns_topic.arn",
+ } as unknown as EnvVars,
+ } as Deps;
+
+ const letters: Letter[] = [
+ {
+ id: "id1",
+ supplierId: "s1",
+ status: "PENDING",
+ } as Letter,
+ {
+ id: "id2",
+ supplierId: "s2",
+ status: "PENDING",
+ } as Letter,
+ {
+ id: "id3",
+ supplierId: "s3",
+ status: "PENDING",
+ } as Letter,
+ ];
+
+ const updateLetterCommands: UpdateLetterCommand[] = [
+ {
+ ...letters[0],
+ status: "REJECTED",
+ reasonCode: "123",
+ reasonText: "Reason text",
+ },
+ { ...letters[1], status: "ACCEPTED" },
+ { ...letters[2], status: "DELIVERED" },
+ ];
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
it("processes letters successfully", async () => {
- const updateLetterCommands: UpdateLetterCommand[] = [
- {
- id: "id1",
- status: "REJECTED",
- supplierId: "s1",
- reasonCode: "123",
- reasonText: "Reason text",
- },
- {
- id: "id2",
- supplierId: "s2",
- status: "ACCEPTED",
- },
- {
- id: "id3",
- supplierId: "s3",
- status: "DELIVERED",
- },
- ];
-
- const mockedDeps: jest.Mocked = {
- s3Client: {} as unknown as S3Client,
- letterRepo: {
- updateLetterStatus: jest
- .fn()
- .mockResolvedValueOnce(updateLetterCommands[0])
- .mockResolvedValueOnce(updateLetterCommands[1])
- .mockResolvedValueOnce(updateLetterCommands[2]),
- } as unknown as LetterRepository,
- logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
- env: {
- SUPPLIER_ID_HEADER: "nhsd-supplier-id",
- APIM_CORRELATION_HEADER: "nhsd-correlation-id",
- LETTERS_TABLE_NAME: "LETTERS_TABLE_NAME",
- LETTER_TTL_HOURS: 12_960,
- DOWNLOAD_URL_TTL_SECONDS: 60,
- MAX_LIMIT: 2500,
- QUEUE_URL: "SQS_URL",
- } as unknown as EnvVars,
- } as Deps;
+ (mockedDeps.letterRepo.getLetterById as jest.Mock)
+ .mockResolvedValueOnce(letters[0])
+ .mockResolvedValueOnce(letters[1])
+ .mockResolvedValueOnce(letters[2]);
const context = mockDeep();
const callback = jest.fn();
@@ -87,70 +112,74 @@ describe("createLetterStatusUpdateHandler", () => {
callback,
);
- expect(mockedDeps.letterRepo.updateLetterStatus).toHaveBeenNthCalledWith(
- 1,
- updateLetterCommands[0],
- );
- expect(mockedDeps.letterRepo.updateLetterStatus).toHaveBeenNthCalledWith(
- 2,
- updateLetterCommands[1],
- );
- expect(mockedDeps.letterRepo.updateLetterStatus).toHaveBeenNthCalledWith(
- 3,
- updateLetterCommands[2],
- );
+ for (let i = 0; i < 3; i++) {
+ expect(mockedDeps.snsClient.send).toHaveBeenNthCalledWith(
+ i + 1,
+ expect.objectContaining({
+ input: expect.objectContaining({
+ TopicArn: mockedDeps.env.AMENDMENTS_TOPIC_ARN,
+ Message: JSON.stringify(
+ mapLetterToCloudEvent(
+ updateLetterCommands[i] as Letter,
+ mockedDeps.env.EVENT_SOURCE,
+ ),
+ ),
+ }),
+ }),
+ );
+ }
});
it("logs error if error thrown when updating", async () => {
const mockError = new Error("Update error");
-
- const mockedDeps: jest.Mocked = {
- s3Client: {} as unknown as S3Client,
- letterRepo: {
- updateLetterStatus: jest.fn().mockRejectedValue(mockError),
- } as unknown as LetterRepository,
- logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
- env: {
- SUPPLIER_ID_HEADER: "nhsd-supplier-id",
- APIM_CORRELATION_HEADER: "nhsd-correlation-id",
- LETTERS_TABLE_NAME: "LETTERS_TABLE_NAME",
- LETTER_TTL_HOURS: 12_960,
- DOWNLOAD_URL_TTL_SECONDS: 60,
- MAX_LIMIT: 2500,
- QUEUE_URL: "SQS_URL",
- } as unknown as EnvVars,
- } as Deps;
+ (mockedDeps.snsClient.send as jest.Mock).mockRejectedValue(mockError);
+ (mockedDeps.letterRepo.getLetterById as jest.Mock).mockResolvedValueOnce(
+ letters[1],
+ );
const context = mockDeep();
const callback = jest.fn();
- const updateLetterCommands: UpdateLetterCommand[] = [
- {
- id: "id1",
- status: "ACCEPTED",
- supplierId: "s1",
- },
- ];
-
const letterStatusUpdateHandler =
createLetterStatusUpdateHandler(mockedDeps);
await letterStatusUpdateHandler(
- buildEvent(updateLetterCommands),
+ buildEvent([updateLetterCommands[1]]),
context,
callback,
);
- expect(mockedDeps.letterRepo.updateLetterStatus).toHaveBeenCalledWith(
- updateLetterCommands[0],
- );
expect(mockedDeps.logger.error).toHaveBeenCalledWith(
{
err: mockError,
- messageId: "mid-id1",
- correlationId: "correlationId-id1",
- messageBody: '{"id":"id1","status":"ACCEPTED","supplierId":"s1"}',
+ messageId: "mid-id2",
+ correlationId: "correlationId-id2",
+ messageBody: '{"id":"id2","supplierId":"s2","status":"ACCEPTED"}',
},
"Error processing letter status update",
);
});
+
+ it("returns batch update failures in the response", async () => {
+ (mockedDeps.letterRepo.getLetterById as jest.Mock)
+ .mockResolvedValueOnce(letters[0])
+ .mockResolvedValueOnce(letters[1])
+ .mockResolvedValueOnce(letters[2]);
+ (mockedDeps.snsClient.send as jest.Mock).mockResolvedValueOnce({});
+ (mockedDeps.snsClient.send as jest.Mock).mockRejectedValueOnce(
+ new Error("Update error"),
+ );
+ (mockedDeps.snsClient.send as jest.Mock).mockResolvedValueOnce({});
+
+ const letterStatusUpdateHandler =
+ createLetterStatusUpdateHandler(mockedDeps);
+ const sqsBatchResponse = await letterStatusUpdateHandler(
+ buildEvent(updateLetterCommands),
+ mockDeep(),
+ jest.fn(),
+ );
+
+ expect(sqsBatchResponse?.batchItemFailures).toEqual([
+ { itemIdentifier: "mid-id2" },
+ ]);
+ });
});
diff --git a/lambdas/api-handler/src/handlers/letter-status-update.ts b/lambdas/api-handler/src/handlers/letter-status-update.ts
index fa14e672d..500d1e685 100644
--- a/lambdas/api-handler/src/handlers/letter-status-update.ts
+++ b/lambdas/api-handler/src/handlers/letter-status-update.ts
@@ -1,21 +1,37 @@
-import { SQSEvent, SQSHandler } from "aws-lambda";
+import { SQSBatchItemFailure, SQSEvent, SQSHandler } from "aws-lambda";
+import { PublishCommand } from "@aws-sdk/client-sns";
+import { LetterEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events";
+import { mapLetterToCloudEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-mapper";
import {
UpdateLetterCommand,
UpdateLetterCommandSchema,
} from "../contracts/letters";
import { Deps } from "../config/deps";
-import { mapToUpdateLetter } from "../mappers/letter-mapper";
export default function createLetterStatusUpdateHandler(
deps: Deps,
): SQSHandler {
return async (event: SQSEvent) => {
+ const batchItemFailures: SQSBatchItemFailure[] = [];
+
const tasks = event.Records.map(async (message) => {
try {
- const letterToUpdate: UpdateLetterCommand =
+ const updateLetterCommand: UpdateLetterCommand =
UpdateLetterCommandSchema.parse(JSON.parse(message.body));
- await deps.letterRepo.updateLetterStatus(
- mapToUpdateLetter(letterToUpdate),
+ const letter = await deps.letterRepo.getLetterById(
+ updateLetterCommand.supplierId,
+ updateLetterCommand.id,
+ );
+ letter.status = updateLetterCommand.status;
+ letter.reasonCode = updateLetterCommand.reasonCode;
+ letter.reasonText = updateLetterCommand.reasonText;
+
+ const letterEvent = mapLetterToCloudEvent(
+ letter,
+ deps.env.EVENT_SOURCE,
+ );
+ await deps.snsClient.send(
+ buildSnsCommand(letterEvent, deps.env.AMENDMENTS_TOPIC_ARN),
);
} catch (error) {
deps.logger.error(
@@ -27,9 +43,22 @@ export default function createLetterStatusUpdateHandler(
},
"Error processing letter status update",
);
+ batchItemFailures.push({ itemIdentifier: message.messageId });
}
});
await Promise.all(tasks);
+
+ return { batchItemFailures };
};
}
+
+function buildSnsCommand(
+ letterEvent: LetterEvent,
+ topicArn: string,
+): PublishCommand {
+ return new PublishCommand({
+ TopicArn: topicArn,
+ Message: JSON.stringify(letterEvent),
+ });
+}
diff --git a/lambdas/api-handler/src/mappers/letter-mapper.ts b/lambdas/api-handler/src/mappers/letter-mapper.ts
index c11d6d8c0..c31d61b34 100644
--- a/lambdas/api-handler/src/mappers/letter-mapper.ts
+++ b/lambdas/api-handler/src/mappers/letter-mapper.ts
@@ -1,4 +1,4 @@
-import { LetterBase, LetterStatus, UpdateLetter } from "@internal/datastore";
+import { LetterBase, LetterStatus } from "@internal/datastore";
import {
GetLetterResponse,
GetLetterResponseSchema,
@@ -68,22 +68,6 @@ export function mapToUpdateCommands(
}));
}
-// ---------------------------------------------
-// Map letter command to repository type
-// ---------------------------------------------
-
-export function mapToUpdateLetter(
- updateLetter: UpdateLetterCommand,
-): UpdateLetter {
- return {
- id: updateLetter.id,
- supplierId: updateLetter.supplierId,
- status: updateLetter.status,
- reasonCode: updateLetter.reasonCode,
- reasonText: updateLetter.reasonText,
- };
-}
-
// ---------------------------------------------
// Map internal datastore letter to response
// ---------------------------------------------
diff --git a/lambdas/letter-updates-transformer/src/__tests__/letter-updates-transformer.test.ts b/lambdas/letter-updates-transformer/src/__tests__/letter-updates-transformer.test.ts
index 065a9a1e3..583ea0143 100644
--- a/lambdas/letter-updates-transformer/src/__tests__/letter-updates-transformer.test.ts
+++ b/lambdas/letter-updates-transformer/src/__tests__/letter-updates-transformer.test.ts
@@ -7,13 +7,12 @@ import {
KinesisStreamRecordPayload,
} from "aws-lambda";
import { mockDeep } from "jest-mock-extended";
-import { LetterBase } from "@internal/datastore";
+import { Letter } from "@internal/datastore";
+import { mapLetterToCloudEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-mapper";
import createHandler from "../letter-updates-transformer";
import { Deps } from "../deps";
import { EnvVars } from "../env";
-import mapLetterToCloudEvent from "../mappers/letter-mapper";
import { LetterStatus } from "../../../api-handler/src/contracts/letters";
-import { LetterForEventPub } from "../types";
// Make crypto return consistent values, since we"re calling it in both prod and test code and comparing the values
const realCrypto = jest.requireActual("crypto");
@@ -171,7 +170,7 @@ describe("letter-updates-transformer Lambda", () => {
it("does not publish invalid letter data", async () => {
const handler = createHandler(mockedDeps);
const oldLetter = generateLetter("ACCEPTED");
- const newLetter = { id: oldLetter.id } as LetterForEventPub;
+ const newLetter = { id: oldLetter.id } as Letter;
const testData = generateKinesisEvent([
generateModifyRecord(oldLetter, newLetter),
@@ -324,7 +323,7 @@ describe("letter-updates-transformer Lambda", () => {
});
});
-function generateLetter(status: LetterStatus, id?: string): LetterForEventPub {
+function generateLetter(status: LetterStatus, id?: string): Letter {
return {
id: id || "1",
status,
@@ -337,14 +336,14 @@ function generateLetter(status: LetterStatus, id?: string): LetterForEventPub {
url: "https://example.com/letter.pdf",
source: "test-source",
subject: "test-source/subject-id",
+ supplierStatus: `supplier1#${status}`,
+ supplierStatusSk: "2025-12-10T11:12:54Z#1",
+ ttl: 1_234_567_890,
};
}
-function generateLetters(
- numLetters: number,
- status: LetterStatus,
-): LetterForEventPub[] {
- const letters: LetterForEventPub[] = Array.from({ length: numLetters });
+function generateLetters(numLetters: number, status: LetterStatus): Letter[] {
+ const letters: Letter[] = Array.from({ length: numLetters });
for (let i = 0; i < numLetters; i++) {
letters[i] = generateLetter(status, String(i + 1));
}
@@ -352,31 +351,34 @@ function generateLetters(
}
function generateModifyRecord(
- oldLetter: LetterForEventPub,
- newLetter: LetterForEventPub,
+ oldLetter: Letter,
+ newLetter: Letter,
): DynamoDBRecord {
- const oldImage = Object.fromEntries(
- Object.entries(oldLetter).map(([key, value]) => [key, { S: value }]),
- );
- const newImage = Object.fromEntries(
- Object.entries(newLetter).map(([key, value]) => [key, { S: value }]),
- );
+ const oldImage = buildStreamImage(oldLetter);
+ const newImage = buildStreamImage(newLetter);
return {
eventName: "MODIFY",
dynamodb: { OldImage: oldImage, NewImage: newImage },
};
}
-function generateInsertRecord(newLetter: LetterBase): DynamoDBRecord {
- const newImage = Object.fromEntries(
- Object.entries(newLetter).map(([key, value]) => [key, { S: value }]),
- );
+function generateInsertRecord(newLetter: Letter): DynamoDBRecord {
+ const newImage = buildStreamImage(newLetter);
return {
eventName: "INSERT",
dynamodb: { NewImage: newImage },
};
}
+function buildStreamImage(letter: Letter) {
+ return Object.fromEntries(
+ Object.entries(letter).map(([key, value]) => [
+ key,
+ typeof value === "number" ? { N: String(value) } : { S: value },
+ ]),
+ );
+}
+
function generateKinesisEvent(letterEvents: object[]): KinesisStreamEvent {
const records = letterEvents
.map((letter) => Buffer.from(JSON.stringify(letter)).toString("base64"))
diff --git a/lambdas/letter-updates-transformer/src/letter-updates-transformer.ts b/lambdas/letter-updates-transformer/src/letter-updates-transformer.ts
index c53151c07..c0de285d9 100644
--- a/lambdas/letter-updates-transformer/src/letter-updates-transformer.ts
+++ b/lambdas/letter-updates-transformer/src/letter-updates-transformer.ts
@@ -10,9 +10,9 @@ import {
PublishBatchRequestEntry,
} from "@aws-sdk/client-sns";
import { LetterEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src";
-import mapLetterToCloudEvent from "./mappers/letter-mapper";
+import { mapLetterToCloudEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-mapper";
+import { Letter, LetterSchema } from "@internal/datastore";
import { Deps } from "./deps";
-import { LetterForEventPub, LetterSchemaForEventPub } from "./types";
// SNS PublishBatchCommand supports up to 10 messages per batch
const BATCH_SIZE = 10;
@@ -109,9 +109,9 @@ function isChanged(record: DynamoDBRecord, property: string): boolean {
return oldValue?.S !== newValue?.S;
}
-function extractNewLetter(record: DynamoDBRecord): LetterForEventPub {
+function extractNewLetter(record: DynamoDBRecord): Letter {
const newImage = record.dynamodb?.NewImage!;
- return LetterSchemaForEventPub.parse(unmarshall(newImage as any));
+ return LetterSchema.parse(unmarshall(newImage as any));
}
function* generateBatches(events: LetterEvent[]) {
diff --git a/lambdas/letter-updates-transformer/src/types.ts b/lambdas/letter-updates-transformer/src/types.ts
deleted file mode 100644
index 34920991b..000000000
--- a/lambdas/letter-updates-transformer/src/types.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { LetterSchema } from "@internal/datastore";
-import { z } from "zod";
-
-export const LetterSchemaForEventPub = LetterSchema.omit({
- supplierStatus: true,
- supplierStatusSk: true,
- ttl: true,
-});
-
-export type LetterForEventPub = z.infer;
diff --git a/lambdas/letter-updates-transformer/tsconfig.json b/lambdas/letter-updates-transformer/tsconfig.json
index f3fa0970e..bb8177b74 100644
--- a/lambdas/letter-updates-transformer/tsconfig.json
+++ b/lambdas/letter-updates-transformer/tsconfig.json
@@ -1,7 +1,6 @@
{
"compilerOptions": {
- "esModuleInterop": true,
- "resolveJsonModule": true
+ "esModuleInterop": true
},
"extends": "../../tsconfig.base.json",
"include": [
diff --git a/lambdas/mi-updates-transformer/tsconfig.json b/lambdas/mi-updates-transformer/tsconfig.json
index f3fa0970e..bb8177b74 100644
--- a/lambdas/mi-updates-transformer/tsconfig.json
+++ b/lambdas/mi-updates-transformer/tsconfig.json
@@ -1,7 +1,6 @@
{
"compilerOptions": {
- "esModuleInterop": true,
- "resolveJsonModule": true
+ "esModuleInterop": true
},
"extends": "../../tsconfig.base.json",
"include": [
diff --git a/lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts b/lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts
index 3f4341340..5415b2f09 100644
--- a/lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts
+++ b/lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts
@@ -237,6 +237,7 @@ describe("createUpsertLetterHandler", () => {
const firstArg = (mockedDeps.letterRepo.putLetter as jest.Mock).mock
.calls[0][0];
expect(firstArg.id).toBe("letter1");
+ expect(firstArg.eventId).toBe("7b9a03ca-342a-4150-b56b-989109c45613");
expect(firstArg.supplierId).toBe("supplier1");
expect(firstArg.specificationId).toBe("spec1");
expect(firstArg.url).toBe("s3://letterDataBucket/letter1.pdf");
diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts
index a1b2ea08b..4d39fedd5 100644
--- a/lambdas/upsert-letter/src/handler/upsert-handler.ts
+++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts
@@ -74,6 +74,7 @@ function mapToInsertLetter(
const now = new Date().toISOString();
return {
id: upsertRequest.data.domainId,
+ eventId: upsertRequest.id,
supplierId: supplier,
status: "PENDING",
specificationId: spec,
@@ -93,6 +94,7 @@ function mapToInsertLetter(
function mapToUpdateLetter(upsertRequest: LetterEvent): UpdateLetter {
return {
id: upsertRequest.data.domainId,
+ eventId: upsertRequest.id,
supplierId: upsertRequest.data.supplierId,
status: upsertRequest.data.status,
reasonCode: upsertRequest.data.reasonCode,
@@ -107,8 +109,9 @@ function resolveSupplierForVariant(
return deps.env.VARIANT_MAP[variantId];
}
-function parseSNSNotification(record: SQSRecord) {
+function parseSNSNotification(record: SQSRecord, deps: Deps) {
const notification = JSON.parse(record.body) as Partial;
+ deps.logger.info({ topicArn: notification.TopicArn });
if (
notification.Type !== "Notification" ||
typeof notification.Message !== "string"
@@ -158,12 +161,16 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler {
const tasks = event.Records.map(async (record) => {
try {
- const message: string = parseSNSNotification(record);
+ const message: string = parseSNSNotification(record, deps);
const snsEvent = JSON.parse(message);
+ deps.logger.info({ snsEvent });
+
const letterEvent: unknown = removeEventBridgeWrapper(snsEvent);
+ deps.logger.info({ letterEvent });
+
const type = getType(letterEvent);
const operation = getOperationFromType(type);
diff --git a/package-lock.json b/package-lock.json
index b52939b16..2150861f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4212,9 +4212,9 @@
}
},
"node_modules/@isaacs/brace-expansion": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
- "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
+ "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 833dc5d46..5277328d6 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"name": "nhs-notify-supplier-api",
"overrides": {
"fast-xml-parser": "^5.3.4",
+ "@isaacs/brace-expansion": "^5.0.1",
"pretty-format": {
"react-is": "19.0.0"
},
diff --git a/tsconfig.base.json b/tsconfig.base.json
index b340a8fa2..a0f424750 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -7,6 +7,7 @@
"module": "ES2020",
"moduleResolution": "node",
"noEmit": true,
+ "resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022"