diff --git a/docs/architecture/c4/notifhir/viewer/callback/index.md b/docs/architecture/c4/notifhir/viewer/callback/index.md
index 26dfc7f69..5a1ce5645 100644
--- a/docs/architecture/c4/notifhir/viewer/callback/index.md
+++ b/docs/architecture/c4/notifhir/viewer/callback/index.md
@@ -8,7 +8,7 @@ is_not_draft: false
last_modified_date: 2026-03-26
owner: Ross Buggins
author: Tom D'Roza
-diagrams: [c4code-nhsapp-status-handler]
+diagrams: [c4code-core-status-handler]
events-raised: [viewer-digital-letter-read]
events-consumed: []
c4type: code
diff --git a/docs/collections/_diagrams/c4code-nhsapp-status-handler.md b/docs/collections/_diagrams/c4code-core-status-handler.md
similarity index 89%
rename from docs/collections/_diagrams/c4code-nhsapp-status-handler.md
rename to docs/collections/_diagrams/c4code-core-status-handler.md
index adb158b8e..7ae348413 100644
--- a/docs/collections/_diagrams/c4code-nhsapp-status-handler.md
+++ b/docs/collections/_diagrams/c4code-core-status-handler.md
@@ -1,12 +1,12 @@
---
-title: c4code-nhsapp-status-handler
+title: c4code-core-status-handler
---
```mermaid
architecture-beta
- group AppStatusHandler(cloud)[NHSAppStatusHandler]
+ group AppStatusHandler(cloud)[CoreStatusHandler]
service optedOutEvent(aws:res-amazon-eventbridge-event)[channel status PUBLISHED v1 Event]
service lambda(logos:aws-lambda)[App Status Handler] in AppStatusHandler
service sqs(logos:aws-sqs)[App Status Queue] in AppStatusHandler
diff --git a/infrastructure/terraform/components/dl/README.md b/infrastructure/terraform/components/dl/README.md
index 8e169b050..173603601 100644
--- a/infrastructure/terraform/components/dl/README.md
+++ b/infrastructure/terraform/components/dl/README.md
@@ -67,6 +67,7 @@ No requirements.
| Name | Source | Version |
|------|--------|---------|
| [core\_notifier](#module\_core\_notifier) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
+| [core\_status\_handler](#module\_core\_status\_handler) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
| [eventpub](#module\_eventpub) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-eventpub.zip | n/a |
| [file\_scanner](#module\_file\_scanner) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
| [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-kms.zip | n/a |
@@ -76,7 +77,6 @@ No requirements.
| [mesh\_download](#module\_mesh\_download) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
| [mesh\_poll](#module\_mesh\_poll) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
| [move\_scanned\_files](#module\_move\_scanned\_files) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
-| [nhsapp\_status\_handler](#module\_nhsapp\_status\_handler) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
| [pdm\_mock](#module\_pdm\_mock) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
| [pdm\_poll](#module\_pdm\_poll) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
| [pdm\_uploader](#module\_pdm\_uploader) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a |
@@ -95,11 +95,11 @@ No requirements.
| [s3bucket\_reporting](#module\_s3bucket\_reporting) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-s3bucket.zip | n/a |
| [s3bucket\_static\_assets](#module\_s3bucket\_static\_assets) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-s3bucket.zip | n/a |
| [sqs\_core\_notifier](#module\_sqs\_core\_notifier) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip | n/a |
+| [sqs\_core\_status\_handler](#module\_sqs\_core\_status\_handler) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip | n/a |
| [sqs\_event\_publisher\_errors](#module\_sqs\_event\_publisher\_errors) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip | n/a |
| [sqs\_mesh\_acknowledge](#module\_sqs\_mesh\_acknowledge) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip | n/a |
| [sqs\_mesh\_download](#module\_sqs\_mesh\_download) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip | n/a |
| [sqs\_move\_scanned\_files](#module\_sqs\_move\_scanned\_files) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip | n/a |
-| [sqs\_nhsapp\_status\_handler](#module\_sqs\_nhsapp\_status\_handler) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip | n/a |
| [sqs\_pdm\_poll](#module\_sqs\_pdm\_poll) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip | n/a |
| [sqs\_pdm\_uploader](#module\_sqs\_pdm\_uploader) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip | n/a |
| [sqs\_print\_analyser](#module\_sqs\_print\_analyser) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip | n/a |
diff --git a/infrastructure/terraform/components/dl/cloudwatch_event_rule_channel_status_published.tf b/infrastructure/terraform/components/dl/cloudwatch_event_rule_channel_status_published.tf
index 38203115f..29f67e5bd 100644
--- a/infrastructure/terraform/components/dl/cloudwatch_event_rule_channel_status_published.tf
+++ b/infrastructure/terraform/components/dl/cloudwatch_event_rule_channel_status_published.tf
@@ -12,8 +12,8 @@ resource "aws_cloudwatch_event_rule" "channel_status_published" {
})
}
-resource "aws_cloudwatch_event_target" "sqs_nhsapp_status_handler_target" {
+resource "aws_cloudwatch_event_target" "channel_status_published_core_status_handler" {
rule = aws_cloudwatch_event_rule.channel_status_published.name
- arn = module.sqs_nhsapp_status_handler.sqs_queue_arn
+ arn = module.sqs_core_status_handler.sqs_queue_arn
event_bus_name = aws_cloudwatch_event_bus.main.name
}
diff --git a/infrastructure/terraform/components/dl/cloudwatch_event_rule_message_status_published.tf b/infrastructure/terraform/components/dl/cloudwatch_event_rule_message_status_published.tf
new file mode 100644
index 000000000..80a532895
--- /dev/null
+++ b/infrastructure/terraform/components/dl/cloudwatch_event_rule_message_status_published.tf
@@ -0,0 +1,19 @@
+resource "aws_cloudwatch_event_rule" "message_status_published" {
+ name = "${local.csi}-message-status-published"
+ description = "message status PUBLISHED event rule"
+ event_bus_name = aws_cloudwatch_event_bus.main.name
+
+ event_pattern = jsonencode({
+ "detail" : {
+ "type" : [
+ "uk.nhs.notify.message.status.PUBLISHED.v1"
+ ],
+ }
+ })
+}
+
+resource "aws_cloudwatch_event_target" "message_status_published_core_status_handler" {
+ rule = aws_cloudwatch_event_rule.message_status_published.name
+ arn = module.sqs_core_status_handler.sqs_queue_arn
+ event_bus_name = aws_cloudwatch_event_bus.main.name
+}
diff --git a/infrastructure/terraform/components/dl/lambda_event_source_mapping_core_status_handler.tf b/infrastructure/terraform/components/dl/lambda_event_source_mapping_core_status_handler.tf
new file mode 100644
index 000000000..cfd90e31e
--- /dev/null
+++ b/infrastructure/terraform/components/dl/lambda_event_source_mapping_core_status_handler.tf
@@ -0,0 +1,10 @@
+resource "aws_lambda_event_source_mapping" "core_status_handler" {
+ event_source_arn = module.sqs_core_status_handler.sqs_queue_arn
+ function_name = module.core_status_handler.function_name
+ batch_size = var.queue_batch_size
+ maximum_batching_window_in_seconds = var.queue_batch_window_seconds
+
+ function_response_types = [
+ "ReportBatchItemFailures"
+ ]
+}
diff --git a/infrastructure/terraform/components/dl/lambda_event_source_mapping_nhsapp_status_handler.tf b/infrastructure/terraform/components/dl/lambda_event_source_mapping_nhsapp_status_handler.tf
deleted file mode 100644
index c001c6757..000000000
--- a/infrastructure/terraform/components/dl/lambda_event_source_mapping_nhsapp_status_handler.tf
+++ /dev/null
@@ -1,10 +0,0 @@
-resource "aws_lambda_event_source_mapping" "nhsapp_status_handler" {
- event_source_arn = module.sqs_nhsapp_status_handler.sqs_queue_arn
- function_name = module.nhsapp_status_handler.function_name
- batch_size = var.queue_batch_size
- maximum_batching_window_in_seconds = var.queue_batch_window_seconds
-
- function_response_types = [
- "ReportBatchItemFailures"
- ]
-}
diff --git a/infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf b/infrastructure/terraform/components/dl/module_lambda_core_status_handler.tf
similarity index 84%
rename from infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf
rename to infrastructure/terraform/components/dl/module_lambda_core_status_handler.tf
index e656f1b3f..428a4e276 100644
--- a/infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf
+++ b/infrastructure/terraform/components/dl/module_lambda_core_status_handler.tf
@@ -1,8 +1,8 @@
-module "nhsapp_status_handler" {
+module "core_status_handler" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip"
- function_name = "nhsapp-status-handler"
- description = "A function for handling NHS app status"
+ function_name = "core-status-handler"
+ description = "A function for handling core status"
aws_account_id = var.aws_account_id
component = local.component
@@ -15,12 +15,12 @@ module "nhsapp_status_handler" {
kms_key_arn = module.kms.key_arn
iam_policy_document = {
- body = data.aws_iam_policy_document.nhsapp_status_handler.json
+ body = data.aws_iam_policy_document.core_status_handler.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 = "nhsapp-status-handler/dist"
+ function_code_dir = "core-status-handler/dist"
function_include_common = true
handler_function_name = "handler"
runtime = "nodejs22.x"
@@ -43,13 +43,14 @@ module "nhsapp_status_handler" {
}
}
-data "aws_iam_policy_document" "nhsapp_status_handler" {
+data "aws_iam_policy_document" "core_status_handler" {
statement {
sid = "AllowTtlDynamoAccess"
effect = "Allow"
actions = [
"dynamodb:UpdateItem",
+ "dynamodb:DeleteItem",
]
resources = [
@@ -72,7 +73,7 @@ data "aws_iam_policy_document" "nhsapp_status_handler" {
}
statement {
- sid = "SQSPermissionsNhsappStatusHandlerQueue"
+ sid = "SQSPermissionsCoreStatusHandlerQueue"
effect = "Allow"
actions = [
@@ -83,7 +84,7 @@ data "aws_iam_policy_document" "nhsapp_status_handler" {
]
resources = [
- module.sqs_nhsapp_status_handler.sqs_queue_arn,
+ module.sqs_core_status_handler.sqs_queue_arn,
]
}
diff --git a/infrastructure/terraform/components/dl/module_sqs_nhsapp_status_handler.tf b/infrastructure/terraform/components/dl/module_sqs_core_status_handler.tf
similarity index 74%
rename from infrastructure/terraform/components/dl/module_sqs_nhsapp_status_handler.tf
rename to infrastructure/terraform/components/dl/module_sqs_core_status_handler.tf
index 00136cac7..c11a46640 100644
--- a/infrastructure/terraform/components/dl/module_sqs_nhsapp_status_handler.tf
+++ b/infrastructure/terraform/components/dl/module_sqs_core_status_handler.tf
@@ -1,4 +1,4 @@
-module "sqs_nhsapp_status_handler" {
+module "sqs_core_status_handler" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.4/terraform-sqs.zip"
aws_account_id = var.aws_account_id
@@ -6,15 +6,15 @@ module "sqs_nhsapp_status_handler" {
environment = var.environment
project = var.project
region = var.region
- name = "nhsapp-status-handler"
+ name = "core-status-handler"
sqs_kms_key_arn = module.kms.key_arn
visibility_timeout_seconds = var.sqs_visibility_timeout_seconds
create_dlq = true
max_receive_count = var.sqs_max_receive_count
- sqs_policy_overload = data.aws_iam_policy_document.sqs_nhsapp_status_handler.json
+ sqs_policy_overload = data.aws_iam_policy_document.sqs_core_status_handler.json
}
-data "aws_iam_policy_document" "sqs_nhsapp_status_handler" {
+data "aws_iam_policy_document" "sqs_core_status_handler" {
statement {
sid = "AllowEventBridgeToSendMessage"
effect = "Allow"
@@ -29,13 +29,16 @@ data "aws_iam_policy_document" "sqs_nhsapp_status_handler" {
]
resources = [
- "arn:aws:sqs:${var.region}:${var.aws_account_id}:${local.csi}-nhsapp-status-handler-queue"
+ "arn:aws:sqs:${var.region}:${var.aws_account_id}:${local.csi}-core-status-handler-queue"
]
condition {
test = "ArnLike"
variable = "aws:SourceArn"
- values = [aws_cloudwatch_event_rule.channel_status_published.arn]
+ values = [
+ aws_cloudwatch_event_rule.channel_status_published.arn,
+ aws_cloudwatch_event_rule.message_status_published.arn
+ ]
}
}
}
diff --git a/lambdas/nhsapp-status-handler/jest.config.ts b/lambdas/core-status-handler/jest.config.ts
similarity index 100%
rename from lambdas/nhsapp-status-handler/jest.config.ts
rename to lambdas/core-status-handler/jest.config.ts
diff --git a/lambdas/nhsapp-status-handler/package.json b/lambdas/core-status-handler/package.json
similarity index 92%
rename from lambdas/nhsapp-status-handler/package.json
rename to lambdas/core-status-handler/package.json
index a9827e47f..3ab7d6550 100644
--- a/lambdas/nhsapp-status-handler/package.json
+++ b/lambdas/core-status-handler/package.json
@@ -12,7 +12,7 @@
"jest": "^29.7.0",
"typescript": "^5.9.3"
},
- "name": "nhs-notify-digital-letters-nhsapp-status-handler",
+ "name": "nhs-notify-digital-letters-core-status-handler",
"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",
diff --git a/lambdas/nhsapp-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/core-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts
similarity index 80%
rename from lambdas/nhsapp-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts
rename to lambdas/core-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts
index f5d3584e4..73a1f08bc 100644
--- a/lambdas/nhsapp-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts
+++ b/lambdas/core-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts
@@ -1,4 +1,4 @@
-import { messageDownloadedEvent, nhsAppStatusEvent } from '__tests__/data';
+import { channelStatusEvent, messageDownloadedEvent } from '__tests__/data';
import { createHandler } from 'apis/sqs-trigger-lambda';
import type { SQSEvent } from 'aws-lambda';
import {
@@ -18,13 +18,13 @@ mockRandomUUID.mockReturnValue('550e8400-e29b-41d4-a716-446655440001');
mockDate.mockReturnValue('2023-06-20T12:00:00.250Z');
describe('createHandler', () => {
- let ttlActions: any;
+ let statusActionResolver: any;
let eventPublisher: any;
let logger: any;
let handler: any;
const eventBusEvent = {
- detail: nhsAppStatusEvent,
+ detail: channelStatusEvent,
};
const digitalLetterReadEvent: DigitalLetterRead = {
@@ -39,18 +39,20 @@ describe('createHandler', () => {
data: {
messageReference: messageDownloadedEvent.data.messageReference,
senderId: messageDownloadedEvent.data.senderId,
+ supplierStatus: 'paper_letter_opted_in',
},
};
beforeEach(() => {
- ttlActions = { markWithdrawn: jest.fn() };
+ statusActionResolver = { resolve: jest.fn() };
eventPublisher = { sendEvents: jest.fn().mockResolvedValue([]) };
logger = { error: jest.fn(), info: jest.fn(), warn: jest.fn() };
- handler = createHandler({ ttlActions, eventPublisher, logger });
+ handler = createHandler({ eventPublisher, logger, statusActionResolver });
});
it('processes a valid SQS event and returns success', async () => {
- ttlActions.markWithdrawn.mockResolvedValue({
+ statusActionResolver.resolve.mockResolvedValue({
+ publish: { supplierStatus: 'paper_letter_opted_in' },
result: 'success',
ttlItem: { event: messageDownloadedEvent },
});
@@ -61,7 +63,9 @@ describe('createHandler', () => {
const res = await handler(event);
expect(res.batchItemFailures).toEqual([]);
- expect(ttlActions.markWithdrawn).toHaveBeenCalledWith(nhsAppStatusEvent);
+ expect(statusActionResolver.resolve).toHaveBeenCalledWith(
+ channelStatusEvent,
+ );
expect(eventPublisher.sendEvents).toHaveBeenCalledWith(
[digitalLetterReadEvent],
validateDigitalLetterRead,
@@ -77,6 +81,7 @@ describe('createHandler', () => {
description: 'Processed SQS Event.',
failed: 0,
retrieved: 1,
+ skipped: 0,
success: 1,
});
});
@@ -99,6 +104,7 @@ describe('createHandler', () => {
description: 'Processed SQS Event.',
failed: 1,
retrieved: 1,
+ skipped: 0,
success: 0,
});
});
@@ -127,25 +133,29 @@ describe('createHandler', () => {
description: 'Processed SQS Event.',
failed: 1,
retrieved: 1,
+ skipped: 0,
success: 0,
});
});
- it('handles ttlActions.markWithdrawn failure', async () => {
- ttlActions.markWithdrawn.mockResolvedValue({ result: 'failed' });
+ it('handles statusActionResolver.resolve failure', async () => {
+ statusActionResolver.resolve.mockResolvedValue({ result: 'failed' });
const event: SQSEvent = {
Records: [{ body: JSON.stringify(eventBusEvent), messageId: 'msg1' }],
} as any;
const res = await handler(event);
- expect(ttlActions.markWithdrawn).toHaveBeenCalledWith(nhsAppStatusEvent);
+ expect(statusActionResolver.resolve).toHaveBeenCalledWith(
+ channelStatusEvent,
+ );
expect(eventPublisher.sendEvents).not.toHaveBeenCalled();
expect(res.batchItemFailures).toEqual([{ itemIdentifier: 'msg1' }]);
expect(logger.info).toHaveBeenCalledWith({
description: 'Processed SQS Event.',
failed: 1,
retrieved: 1,
+ skipped: 0,
success: 0,
});
});
@@ -172,6 +182,7 @@ describe('createHandler', () => {
description: 'Processed SQS Event.',
failed: 1,
retrieved: 1,
+ skipped: 0,
success: 0,
});
});
@@ -197,6 +208,7 @@ describe('createHandler', () => {
description: 'Processed SQS Event.',
failed: 1,
retrieved: 1,
+ skipped: 0,
success: 0,
});
@@ -204,7 +216,8 @@ describe('createHandler', () => {
});
it('processes multiple successful events and sends them as a batch', async () => {
- ttlActions.markWithdrawn.mockResolvedValue({
+ statusActionResolver.resolve.mockResolvedValue({
+ publish: { supplierStatus: 'paper_letter_opted_in' },
result: 'success',
ttlItem: { event: messageDownloadedEvent },
});
@@ -219,7 +232,7 @@ describe('createHandler', () => {
const res = await handler(sqsEvent);
expect(res.batchItemFailures).toEqual([]);
- expect(ttlActions.markWithdrawn).toHaveBeenCalledTimes(3);
+ expect(statusActionResolver.resolve).toHaveBeenCalledTimes(3);
expect(eventPublisher.sendEvents).toHaveBeenCalledWith(
[digitalLetterReadEvent, digitalLetterReadEvent, digitalLetterReadEvent],
validateDigitalLetterRead,
@@ -228,12 +241,14 @@ describe('createHandler', () => {
description: 'Processed SQS Event.',
failed: 0,
retrieved: 3,
+ skipped: 0,
success: 3,
});
});
it('handles partial event publishing failures and logs warning', async () => {
- ttlActions.markWithdrawn.mockResolvedValue({
+ statusActionResolver.resolve.mockResolvedValue({
+ publish: { supplierStatus: 'paper_letter_opted_in' },
result: 'success',
ttlItem: { event: messageDownloadedEvent },
});
@@ -262,7 +277,8 @@ describe('createHandler', () => {
});
it('handles event publishing exception and logs warning', async () => {
- ttlActions.markWithdrawn.mockResolvedValue({
+ statusActionResolver.resolve.mockResolvedValue({
+ publish: { supplierStatus: 'paper_letter_opted_in' },
result: 'success',
ttlItem: { event: messageDownloadedEvent },
});
@@ -288,7 +304,7 @@ describe('createHandler', () => {
});
it('does not call eventPublisher when no successful events', async () => {
- ttlActions.markWithdrawn.mockResolvedValue({ result: 'failed' });
+ statusActionResolver.resolve.mockResolvedValue({ result: 'failed' });
const event: SQSEvent = {
Records: [{ body: JSON.stringify(eventBusEvent), messageId: 'msg1' }],
@@ -302,12 +318,33 @@ describe('createHandler', () => {
description: 'Processed SQS Event.',
failed: 1,
retrieved: 1,
+ skipped: 0,
+ success: 0,
+ });
+ });
+
+ it('does not call eventPublisher for skipped events', async () => {
+ statusActionResolver.resolve.mockResolvedValue({ result: 'skipped' });
+
+ const event: SQSEvent = {
+ Records: [{ body: JSON.stringify(eventBusEvent), messageId: 'msg1' }],
+ } as any;
+
+ const res = await handler(event);
+
+ expect(res.batchItemFailures).toEqual([]);
+ expect(eventPublisher.sendEvents).not.toHaveBeenCalled();
+ expect(logger.info).toHaveBeenCalledWith({
+ description: 'Processed SQS Event.',
+ failed: 0,
+ retrieved: 1,
+ skipped: 1,
success: 0,
});
});
it('does not call eventPublisher when no TTL record is found', async () => {
- ttlActions.markWithdrawn.mockResolvedValue({ result: 'success' });
+ statusActionResolver.resolve.mockResolvedValue({ result: 'success' });
const event: SQSEvent = {
Records: [{ body: JSON.stringify(eventBusEvent), messageId: 'msg1' }],
@@ -321,23 +358,27 @@ describe('createHandler', () => {
description: 'Processed SQS Event.',
failed: 0,
retrieved: 1,
+ skipped: 0,
success: 1,
});
});
- it('handles mixed success and failure scenarios', async () => {
- ttlActions.markWithdrawn
+ it('handles mixed success, failure, and skipped scenarios', async () => {
+ statusActionResolver.resolve
.mockResolvedValueOnce({
+ publish: { supplierStatus: 'paper_letter_opted_in' },
result: 'success',
ttlItem: { event: messageDownloadedEvent },
})
- .mockResolvedValueOnce({ result: 'failed' });
+ .mockResolvedValueOnce({ result: 'failed' })
+ .mockResolvedValueOnce({ result: 'skipped' });
const event: SQSEvent = {
Records: [
{ body: JSON.stringify(eventBusEvent), messageId: 'msg1' },
{ body: '{}', messageId: 'msg2' },
{ body: JSON.stringify(eventBusEvent), messageId: 'msg3' },
+ { body: JSON.stringify(eventBusEvent), messageId: 'msg4' },
],
} as any;
@@ -354,7 +395,8 @@ describe('createHandler', () => {
expect(logger.info).toHaveBeenCalledWith({
description: 'Processed SQS Event.',
failed: 2,
- retrieved: 3,
+ retrieved: 4,
+ skipped: 1,
success: 1,
});
});
diff --git a/lambdas/core-status-handler/src/__tests__/app/status-action-resolver.test.ts b/lambdas/core-status-handler/src/__tests__/app/status-action-resolver.test.ts
new file mode 100644
index 000000000..4d16f806e
--- /dev/null
+++ b/lambdas/core-status-handler/src/__tests__/app/status-action-resolver.test.ts
@@ -0,0 +1,214 @@
+import {
+ channelStatusEvent,
+ messageDownloadedEvent,
+ messagesStatusEvent,
+} from '__tests__/data';
+import { StatusActionResolver } from 'app/status-action-resolver';
+import { TtlActions } from 'app/ttl-actions';
+import {
+ ChannelStatusPublishedEvent,
+ MessageStatusPublishedEvent,
+} from 'utils';
+
+const successOutcome = {
+ result: 'success' as const,
+ ttlItem: { event: messageDownloadedEvent },
+};
+
+describe('StatusActionResolver', () => {
+ let ttlActions: jest.Mocked>;
+ let logger: { warn: jest.Mock; info: jest.Mock };
+ let resolver: StatusActionResolver;
+
+ beforeEach(() => {
+ ttlActions = { markWithdrawn: jest.fn(), delete: jest.fn() } as any;
+ logger = { warn: jest.fn(), info: jest.fn() };
+ resolver = new StatusActionResolver(ttlActions as any, logger as any);
+ });
+
+ describe('ChannelStatusPublishedEvent', () => {
+ it('when supplierStatus is paper_letter_opted_out - calls markWithdrawn and publishes DigitalLetterRead event', async () => {
+ const event: ChannelStatusPublishedEvent = {
+ ...channelStatusEvent,
+ data: {
+ ...channelStatusEvent.data,
+ supplierStatus: 'paper_letter_opted_out',
+ },
+ };
+ ttlActions.markWithdrawn.mockResolvedValue(successOutcome);
+
+ const result = await resolver.resolve(event);
+
+ expect(ttlActions.markWithdrawn).toHaveBeenCalledWith(event);
+ expect(ttlActions.delete).not.toHaveBeenCalled();
+ expect(result).toEqual({
+ ...successOutcome,
+ publish: { supplierStatus: 'paper_letter_opted_out' },
+ });
+ });
+
+ it('when supplierStatus is paper_letter_opted_in - calls delete and publishes DigitalLetterRead event', async () => {
+ const event: ChannelStatusPublishedEvent = {
+ ...channelStatusEvent,
+ data: {
+ ...channelStatusEvent.data,
+ supplierStatus: 'paper_letter_opted_in',
+ },
+ };
+ ttlActions.delete.mockResolvedValue(successOutcome);
+
+ const result = await resolver.resolve(event);
+
+ expect(ttlActions.delete).toHaveBeenCalledWith(event);
+ expect(ttlActions.markWithdrawn).not.toHaveBeenCalled();
+ expect(result).toEqual({
+ ...successOutcome,
+ publish: { supplierStatus: 'paper_letter_opted_in' },
+ });
+ });
+
+ it('when supplierStatus is rejected, channelStatus is failed - calls delete', async () => {
+ const event: ChannelStatusPublishedEvent = {
+ ...channelStatusEvent,
+ data: {
+ ...channelStatusEvent.data,
+ supplierStatus: 'rejected',
+ channelStatus: 'failed',
+ },
+ };
+ ttlActions.delete.mockResolvedValue(successOutcome);
+
+ const result = await resolver.resolve(event);
+
+ expect(ttlActions.delete).toHaveBeenCalledWith(event);
+ expect(ttlActions.markWithdrawn).not.toHaveBeenCalled();
+ expect(result).toEqual({ ...successOutcome });
+ });
+
+ it('when supplierStatus is rejected, but channelStatus is not failed - skips', async () => {
+ const event: ChannelStatusPublishedEvent = {
+ ...channelStatusEvent,
+ data: {
+ ...channelStatusEvent.data,
+ supplierStatus: 'rejected',
+ channelStatus: 'retry',
+ } as unknown as ChannelStatusPublishedEvent['data'],
+ };
+ ttlActions.delete.mockResolvedValue(successOutcome);
+
+ const result = await resolver.resolve(event);
+
+ expect(ttlActions.delete).not.toHaveBeenCalled();
+ expect(ttlActions.markWithdrawn).not.toHaveBeenCalled();
+ expect(result).toEqual({ result: 'skipped' });
+ });
+
+ it('when supplierStatus is unrecognised - skips', async () => {
+ const event: ChannelStatusPublishedEvent = {
+ ...channelStatusEvent,
+ data: {
+ ...channelStatusEvent.data,
+ supplierStatus: 'some_other_status',
+ } as unknown as ChannelStatusPublishedEvent['data'],
+ };
+
+ const result = await resolver.resolve(event);
+
+ expect(ttlActions.markWithdrawn).not.toHaveBeenCalled();
+ expect(ttlActions.delete).not.toHaveBeenCalled();
+ expect(result).toEqual({ result: 'skipped' });
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: 'Event skipped',
+ type: 'uk.nhs.notify.channel.status.PUBLISHED.v1',
+ data: event.data,
+ }),
+ );
+ });
+ });
+
+ describe('MessageStatusPublishedEvent', () => {
+ it('when messageStatus is failed and resolvedChannels is empty - calls markWithdrawn', async () => {
+ const event: MessageStatusPublishedEvent = {
+ ...messagesStatusEvent,
+ data: {
+ ...messagesStatusEvent.data,
+ messageStatus: 'failed',
+ resolvedChannels: [],
+ },
+ };
+ ttlActions.markWithdrawn.mockResolvedValue(successOutcome);
+
+ const result = await resolver.resolve(event);
+
+ expect(ttlActions.markWithdrawn).toHaveBeenCalledWith(event);
+ expect(ttlActions.delete).not.toHaveBeenCalled();
+ expect(result).toEqual({ ...successOutcome });
+ });
+
+ it('when messageStatus is failed and resolvedChannels is absent - calls markWithdrawn', async () => {
+ const event: MessageStatusPublishedEvent = {
+ ...messagesStatusEvent,
+ data: {
+ ...messagesStatusEvent.data,
+ messageStatus: 'failed',
+ resolvedChannels: undefined,
+ },
+ };
+ ttlActions.markWithdrawn.mockResolvedValue(successOutcome);
+
+ const result = await resolver.resolve(event);
+
+ expect(ttlActions.markWithdrawn).toHaveBeenCalledWith(event);
+ expect(result).toEqual({ ...successOutcome });
+ });
+
+ it('when messageStatus is failed but resolvedChannels is non-empty - skips', async () => {
+ const event: MessageStatusPublishedEvent = {
+ ...messagesStatusEvent,
+ data: {
+ ...messagesStatusEvent.data,
+ messageStatus: 'failed',
+ resolvedChannels: ['letter'],
+ },
+ };
+
+ const result = await resolver.resolve(event);
+
+ expect(ttlActions.markWithdrawn).not.toHaveBeenCalled();
+ expect(ttlActions.delete).not.toHaveBeenCalled();
+ expect(result).toEqual({ result: 'skipped' });
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: 'Event skipped',
+ type: 'uk.nhs.notify.message.status.PUBLISHED.v1',
+ data: event.data,
+ }),
+ );
+ });
+
+ it('when messageStatus is not failed - skips', async () => {
+ const event: MessageStatusPublishedEvent = {
+ ...messagesStatusEvent,
+ data: {
+ ...messagesStatusEvent.data,
+ messageStatus: 'delivered',
+ resolvedChannels: [],
+ } as unknown as MessageStatusPublishedEvent['data'],
+ };
+
+ const result = await resolver.resolve(event);
+
+ expect(ttlActions.markWithdrawn).not.toHaveBeenCalled();
+ expect(ttlActions.delete).not.toHaveBeenCalled();
+ expect(result).toEqual({ result: 'skipped' });
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: 'Event skipped',
+ type: 'uk.nhs.notify.message.status.PUBLISHED.v1',
+ data: event.data,
+ }),
+ );
+ });
+ });
+});
diff --git a/lambdas/core-status-handler/src/__tests__/app/ttl-actions.test.ts b/lambdas/core-status-handler/src/__tests__/app/ttl-actions.test.ts
new file mode 100644
index 000000000..ec37a0901
--- /dev/null
+++ b/lambdas/core-status-handler/src/__tests__/app/ttl-actions.test.ts
@@ -0,0 +1,137 @@
+import { channelStatusEvent, messageDownloadedEvent } from '__tests__/data';
+import { TtlActions } from 'app/ttl-actions';
+import { TtlRepository } from 'infra/ttl-repository';
+
+describe('TtlActions', () => {
+ let repo: jest.Mocked;
+ let logger: any;
+ let ttlActions: TtlActions;
+
+ describe('markWithdrawn', () => {
+ beforeEach(() => {
+ repo = { markWithdrawn: jest.fn() } as any;
+ logger = { warn: jest.fn(), info: jest.fn() };
+ ttlActions = new TtlActions(repo, logger);
+ });
+
+ it('returns success when markWithdrawn succeeds', async () => {
+ repo.markWithdrawn.mockResolvedValue({ event: messageDownloadedEvent });
+
+ const result = await ttlActions.markWithdrawn(channelStatusEvent);
+
+ expect(result).toEqual({
+ result: 'success',
+ ttlItem: { event: messageDownloadedEvent },
+ });
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: expect.stringContaining(
+ 'TTL record marked as withdrawn',
+ ),
+ messageReference: channelStatusEvent.data.messageReference,
+ }),
+ );
+ expect(repo.markWithdrawn).toHaveBeenCalledWith(
+ channelStatusEvent.data.messageReference,
+ );
+ });
+
+ it('returns success when TTL record not found', async () => {
+ // eslint-disable-next-line unicorn/no-useless-undefined
+ repo.markWithdrawn.mockResolvedValue(undefined);
+
+ const result = await ttlActions.markWithdrawn(channelStatusEvent);
+
+ expect(result).toEqual({ result: 'success' });
+
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: expect.stringContaining('TTL record not found'),
+ messageReference: channelStatusEvent.data.messageReference,
+ }),
+ );
+ expect(repo.markWithdrawn).toHaveBeenCalledWith(
+ channelStatusEvent.data.messageReference,
+ );
+ });
+
+ it('returns failed and logs error when markWithdrawn throws', async () => {
+ const error = new Error('fail');
+ repo.markWithdrawn.mockRejectedValue(error);
+
+ const result = await ttlActions.markWithdrawn(channelStatusEvent);
+
+ expect(result).toEqual({ result: 'failed' });
+ expect(logger.warn).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: expect.stringContaining('Error marking TTL withdrawn'),
+ messageReference: channelStatusEvent.data.messageReference,
+ err: error,
+ }),
+ );
+ });
+ });
+
+ describe('delete', () => {
+ beforeEach(() => {
+ repo = { delete: jest.fn() } as any;
+ logger = { warn: jest.fn(), info: jest.fn() };
+ ttlActions = new TtlActions(repo, logger);
+ });
+
+ it('returns success when delete succeeds', async () => {
+ repo.delete.mockResolvedValue({ event: messageDownloadedEvent });
+
+ const result = await ttlActions.delete(channelStatusEvent);
+
+ expect(result).toEqual({
+ result: 'success',
+ ttlItem: { event: messageDownloadedEvent },
+ });
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: expect.stringContaining('TTL record deleted'),
+ messageReference: channelStatusEvent.data.messageReference,
+ }),
+ );
+ expect(repo.delete).toHaveBeenCalledWith(
+ channelStatusEvent.data.messageReference,
+ );
+ });
+
+ it('returns success when TTL record not found', async () => {
+ // eslint-disable-next-line unicorn/no-useless-undefined
+ repo.delete.mockResolvedValue(undefined);
+
+ const result = await ttlActions.delete(channelStatusEvent);
+
+ expect(result).toEqual({ result: 'success' });
+
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: expect.stringContaining('TTL record not found'),
+ messageReference: channelStatusEvent.data.messageReference,
+ }),
+ );
+ expect(repo.delete).toHaveBeenCalledWith(
+ channelStatusEvent.data.messageReference,
+ );
+ });
+
+ it('returns failed and logs error when delete throws', async () => {
+ const error = new Error('fail');
+ repo.delete.mockRejectedValue(error);
+
+ const result = await ttlActions.delete(channelStatusEvent);
+
+ expect(result).toEqual({ result: 'failed' });
+ expect(logger.warn).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: expect.stringContaining('Error deleting TTL record'),
+ messageReference: channelStatusEvent.data.messageReference,
+ err: error,
+ }),
+ );
+ });
+ });
+});
diff --git a/lambdas/nhsapp-status-handler/src/__tests__/container.test.ts b/lambdas/core-status-handler/src/__tests__/container.test.ts
similarity index 100%
rename from lambdas/nhsapp-status-handler/src/__tests__/container.test.ts
rename to lambdas/core-status-handler/src/__tests__/container.test.ts
diff --git a/lambdas/nhsapp-status-handler/src/__tests__/data.ts b/lambdas/core-status-handler/src/__tests__/data.ts
similarity index 64%
rename from lambdas/nhsapp-status-handler/src/__tests__/data.ts
rename to lambdas/core-status-handler/src/__tests__/data.ts
index 6235d612b..30f4d8f28 100644
--- a/lambdas/nhsapp-status-handler/src/__tests__/data.ts
+++ b/lambdas/core-status-handler/src/__tests__/data.ts
@@ -1,5 +1,8 @@
import { MESHInboxMessageDownloaded } from 'digital-letters-events';
-import { ChannelStatusPublishedEvent } from 'utils';
+import {
+ ChannelStatusPublishedEvent,
+ MessageStatusPublishedEvent,
+} from 'utils';
export const messageDownloadedEvent: MESHInboxMessageDownloaded = {
id: '550e8400-e29b-41d4-a716-446655440001',
@@ -26,9 +29,22 @@ export const messageDownloadedEvent: MESHInboxMessageDownloaded = {
},
};
-export const nhsAppStatusEvent: ChannelStatusPublishedEvent = {
+export const channelStatusEvent: ChannelStatusPublishedEvent = {
+ source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
+ type: 'uk.nhs.notify.channel.status.PUBLISHED.v1',
data: {
+ channelStatus: 'delivered',
messageReference: `${messageDownloadedEvent.data.senderId}_${messageDownloadedEvent.data.messageReference}`,
supplierStatus: 'paper_letter_opted_out',
},
};
+
+export const messagesStatusEvent: MessageStatusPublishedEvent = {
+ source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
+ type: 'uk.nhs.notify.message.status.PUBLISHED.v1',
+ data: {
+ messageReference: `${messageDownloadedEvent.data.senderId}_${messageDownloadedEvent.data.messageReference}`,
+ messageStatus: 'failed',
+ resolvedChannels: ['letter'],
+ },
+};
diff --git a/lambdas/nhsapp-status-handler/src/__tests__/index.test.ts b/lambdas/core-status-handler/src/__tests__/index.test.ts
similarity index 100%
rename from lambdas/nhsapp-status-handler/src/__tests__/index.test.ts
rename to lambdas/core-status-handler/src/__tests__/index.test.ts
diff --git a/lambdas/nhsapp-status-handler/src/__tests__/infra/config.test.ts b/lambdas/core-status-handler/src/__tests__/infra/config.test.ts
similarity index 100%
rename from lambdas/nhsapp-status-handler/src/__tests__/infra/config.test.ts
rename to lambdas/core-status-handler/src/__tests__/infra/config.test.ts
diff --git a/lambdas/core-status-handler/src/__tests__/infra/ttl-repository.test.ts b/lambdas/core-status-handler/src/__tests__/infra/ttl-repository.test.ts
new file mode 100644
index 000000000..ad87f367a
--- /dev/null
+++ b/lambdas/core-status-handler/src/__tests__/infra/ttl-repository.test.ts
@@ -0,0 +1,86 @@
+import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
+import { DeleteCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb';
+import { channelStatusEvent } from '__tests__/data';
+import { TtlRepository } from 'infra/ttl-repository';
+
+describe('TtlRepository', () => {
+ let dynamoDocumentClient: any;
+ let repo: TtlRepository;
+ const tableName = 'table';
+
+ beforeEach(() => {
+ dynamoDocumentClient = { send: jest.fn().mockResolvedValue({}) };
+ repo = new TtlRepository(tableName, dynamoDocumentClient);
+ });
+
+ describe('markWithdrawn', () => {
+ it('marks item as withdrawn', async () => {
+ await repo.markWithdrawn(channelStatusEvent.data.messageReference);
+
+ const updateCommand: UpdateCommand =
+ dynamoDocumentClient.send.mock.calls[0][0];
+ expect(updateCommand.input).toStrictEqual({
+ TableName: tableName,
+ Key: {
+ PK: channelStatusEvent.data.messageReference,
+ SK: 'TTL',
+ },
+ ConditionExpression: 'attribute_exists(PK)',
+ UpdateExpression: 'set withdrawn = :val1',
+ ExpressionAttributeValues: {
+ ':val1': true,
+ },
+ ReturnValues: 'ALL_NEW',
+ });
+ });
+
+ it('returns undefined on ConditionalCheckFailedException', async () => {
+ const error = new ConditionalCheckFailedException({
+ message: 'ConditionalCheckFailedException',
+ $metadata: {},
+ });
+ dynamoDocumentClient.send.mockRejectedValue(error);
+
+ const result = await repo.markWithdrawn(
+ channelStatusEvent.data.messageReference,
+ );
+
+ expect(result).toBeUndefined();
+ });
+
+ it('errors on dynamo error', async () => {
+ const error = new Error('fail');
+ dynamoDocumentClient.send.mockRejectedValue(error);
+
+ await expect(
+ repo.markWithdrawn(channelStatusEvent.data.messageReference),
+ ).rejects.toThrow(error);
+ });
+ });
+
+ describe('delete', () => {
+ it('deletes item', async () => {
+ await repo.delete(channelStatusEvent.data.messageReference);
+
+ const deleteCommand: DeleteCommand =
+ dynamoDocumentClient.send.mock.calls[0][0];
+ expect(deleteCommand.input).toStrictEqual({
+ TableName: tableName,
+ Key: {
+ PK: channelStatusEvent.data.messageReference,
+ SK: 'TTL',
+ },
+ ReturnValues: 'ALL_OLD',
+ });
+ });
+
+ it('errors on dynamo error', async () => {
+ const error = new Error('fail');
+ dynamoDocumentClient.send.mockRejectedValue(error);
+
+ await expect(
+ repo.delete(channelStatusEvent.data.messageReference),
+ ).rejects.toThrow(error);
+ });
+ });
+});
diff --git a/lambdas/nhsapp-status-handler/src/apis/sqs-trigger-lambda.ts b/lambdas/core-status-handler/src/apis/sqs-trigger-lambda.ts
similarity index 75%
rename from lambdas/nhsapp-status-handler/src/apis/sqs-trigger-lambda.ts
rename to lambdas/core-status-handler/src/apis/sqs-trigger-lambda.ts
index 93b4d3a74..d9a3c9b70 100644
--- a/lambdas/nhsapp-status-handler/src/apis/sqs-trigger-lambda.ts
+++ b/lambdas/core-status-handler/src/apis/sqs-trigger-lambda.ts
@@ -4,20 +4,29 @@ import type {
SQSEvent,
} from 'aws-lambda';
import { randomUUID } from 'node:crypto';
-import type { TtlActionOutcome, TtlActions } from 'app/ttl-actions';
-import { $ChannelStatusPublishedEvent, EventPublisher, Logger } from 'utils';
+import {
+ StatusActionResolver,
+ StatusActionResolverOutcome,
+} from 'app/status-action-resolver';
import {
DigitalLetterRead,
MESHInboxMessageDownloaded,
validateDigitalLetterRead,
} from 'digital-letters-events';
+import {
+ $StatusPublishedEvent,
+ EventPublisher,
+ Logger,
+ StatusPublishedEvent,
+} from 'utils';
interface ProcessingResult {
- outcome: TtlActionOutcome;
+ outcome: StatusActionResolverOutcome;
+ item?: StatusPublishedEvent;
}
interface CreateHandlerDependencies {
- ttlActions: TtlActions;
+ statusActionResolver: StatusActionResolver;
eventPublisher: EventPublisher;
logger: Logger;
}
@@ -25,7 +34,7 @@ interface CreateHandlerDependencies {
export const createHandler = ({
eventPublisher,
logger,
- ttlActions,
+ statusActionResolver,
}: CreateHandlerDependencies) =>
async function handler(sqsEvent: SQSEvent): Promise {
const batchItemFailures: SQSBatchItemFailure[] = [];
@@ -35,12 +44,11 @@ export const createHandler = ({
try {
const sqsEventBody = JSON.parse(body);
const sqsEventDetail = sqsEventBody.detail;
-
const {
data: item,
error: parseError,
success: parseSuccess,
- } = $ChannelStatusPublishedEvent.safeParse(sqsEventDetail);
+ } = $StatusPublishedEvent.safeParse(sqsEventDetail);
if (!parseSuccess) {
logger.warn({
@@ -54,7 +62,7 @@ export const createHandler = ({
return { outcome: { result: 'failed' } };
}
- const result = await ttlActions.markWithdrawn(item);
+ const result = await statusActionResolver.resolve(item);
if (result.result === 'failed') {
batchItemFailures.push({ itemIdentifier: messageId });
@@ -77,22 +85,35 @@ export const createHandler = ({
const results = await Promise.allSettled(promises);
- const processed: Record =
- {
- retrieved: results.length,
- success: 0,
- failed: 0,
- };
-
- const successfulEvents: MESHInboxMessageDownloaded[] = [];
+ const processed: Record<
+ StatusActionResolverOutcome['result'] | 'retrieved',
+ number
+ > = {
+ retrieved: results.length,
+ success: 0,
+ failed: 0,
+ skipped: 0,
+ };
+
+ const successfulEvents: {
+ event: MESHInboxMessageDownloaded;
+ supplierStatus: string;
+ }[] = [];
for (const result of results) {
if (result.status === 'fulfilled') {
const { outcome } = result.value;
processed[outcome.result] += 1;
- if (outcome.result === 'success' && outcome.ttlItem) {
- successfulEvents.push(outcome.ttlItem.event);
+ if (
+ outcome.result === 'success' &&
+ outcome.ttlItem &&
+ outcome.publish
+ ) {
+ successfulEvents.push({
+ event: outcome.ttlItem.event,
+ supplierStatus: outcome.publish.supplierStatus,
+ });
}
} else {
logger.warn({ err: result.reason });
@@ -103,7 +124,7 @@ export const createHandler = ({
if (successfulEvents.length > 0) {
try {
const failedEvents = await eventPublisher.sendEvents(
- successfulEvents.map((event) => ({
+ successfulEvents.map(({ event, supplierStatus }) => ({
...event,
id: randomUUID(),
time: new Date().toISOString(),
@@ -115,6 +136,7 @@ export const createHandler = ({
data: {
messageReference: event.data.messageReference,
senderId: event.data.senderId,
+ supplierStatus,
},
})),
validateDigitalLetterRead,
diff --git a/lambdas/core-status-handler/src/app/status-action-resolver.ts b/lambdas/core-status-handler/src/app/status-action-resolver.ts
new file mode 100644
index 000000000..0700d6483
--- /dev/null
+++ b/lambdas/core-status-handler/src/app/status-action-resolver.ts
@@ -0,0 +1,68 @@
+import { Logger, StatusPublishedEvent } from 'utils';
+import { TtlActionOutcome, TtlActions } from 'app/ttl-actions';
+
+export type StatusActionResolverOutcome = TtlActionOutcome & {
+ publish?: { supplierStatus: string };
+};
+
+type Action =
+ | { kind: 'markWithdrawn' | 'delete'; publish?: { supplierStatus: string } }
+ | { kind: 'skip' };
+
+function resolveAction(item: StatusPublishedEvent): Action {
+ if (item.type === 'uk.nhs.notify.channel.status.PUBLISHED.v1') {
+ if (item.data.supplierStatus === 'paper_letter_opted_out') {
+ return {
+ kind: 'markWithdrawn',
+ publish: { supplierStatus: item.data.supplierStatus },
+ };
+ }
+ if (item.data.supplierStatus === 'paper_letter_opted_in') {
+ return {
+ kind: 'delete',
+ publish: { supplierStatus: item.data.supplierStatus },
+ };
+ }
+ if (
+ item.data.supplierStatus === 'rejected' &&
+ item.data.channelStatus === 'failed'
+ ) {
+ return { kind: 'delete' };
+ }
+ }
+
+ if (
+ item.type === 'uk.nhs.notify.message.status.PUBLISHED.v1' &&
+ item.data.messageStatus === 'failed' &&
+ !item.data.resolvedChannels?.length
+ ) {
+ return { kind: 'markWithdrawn' };
+ }
+
+ return { kind: 'skip' };
+}
+
+export class StatusActionResolver {
+ constructor(
+ private readonly ttlActions: TtlActions,
+ private readonly logger: Logger,
+ ) {}
+
+ async resolve(
+ item: StatusPublishedEvent,
+ ): Promise {
+ const action = resolveAction(item);
+
+ if (action.kind === 'skip') {
+ this.logger.info({
+ description: 'Event skipped',
+ type: item.type,
+ data: item.data,
+ });
+ return { result: 'skipped' };
+ }
+
+ const outcome = await this.ttlActions[action.kind](item);
+ return { ...outcome, publish: action.publish };
+ }
+}
diff --git a/lambdas/core-status-handler/src/app/ttl-actions.ts b/lambdas/core-status-handler/src/app/ttl-actions.ts
new file mode 100644
index 000000000..a0c628964
--- /dev/null
+++ b/lambdas/core-status-handler/src/app/ttl-actions.ts
@@ -0,0 +1,79 @@
+import { Logger, StatusPublishedEvent } from 'utils';
+import { TtlRepository } from 'infra/ttl-repository';
+import { TtlItem } from 'types/types';
+
+export type TtlActionOutcome =
+ | { result: 'success'; ttlItem: TtlItem }
+ | { result: 'failed' }
+ | { result: 'skipped' };
+
+export class TtlActions {
+ constructor(
+ private readonly ttlRepository: TtlRepository,
+ private readonly logger: Logger,
+ ) {}
+
+ async markWithdrawn(item: StatusPublishedEvent): Promise {
+ const { messageReference } = item.data;
+
+ let ttlItem: TtlItem;
+
+ try {
+ ttlItem = await this.ttlRepository.markWithdrawn(messageReference);
+ } catch (error) {
+ this.logger.warn({
+ description: 'Error marking TTL withdrawn',
+ messageReference,
+ err: error,
+ });
+
+ return { result: 'failed' };
+ }
+
+ if (ttlItem) {
+ this.logger.info({
+ description: 'TTL record marked as withdrawn',
+ messageReference,
+ });
+ } else {
+ this.logger.info({
+ description: 'TTL record not found',
+ messageReference,
+ });
+ }
+
+ return { result: 'success', ttlItem };
+ }
+
+ async delete(item: StatusPublishedEvent): Promise {
+ const { messageReference } = item.data;
+
+ let ttlItem: TtlItem;
+
+ try {
+ ttlItem = await this.ttlRepository.delete(messageReference);
+ } catch (error) {
+ this.logger.warn({
+ description: 'Error deleting TTL record',
+ messageReference,
+ err: error,
+ });
+
+ return { result: 'failed' };
+ }
+
+ if (ttlItem) {
+ this.logger.info({
+ description: 'TTL record deleted',
+ messageReference,
+ });
+ } else {
+ this.logger.info({
+ description: 'TTL record not found',
+ messageReference,
+ });
+ }
+
+ return { result: 'success', ttlItem };
+ }
+}
diff --git a/lambdas/nhsapp-status-handler/src/container.ts b/lambdas/core-status-handler/src/container.ts
similarity index 85%
rename from lambdas/nhsapp-status-handler/src/container.ts
rename to lambdas/core-status-handler/src/container.ts
index 4f829ccf1..c406ee35d 100644
--- a/lambdas/nhsapp-status-handler/src/container.ts
+++ b/lambdas/core-status-handler/src/container.ts
@@ -8,6 +8,7 @@ import {
} from 'utils';
import { loadConfig } from 'infra/config';
import { TtlRepository } from 'infra/ttl-repository';
+import { StatusActionResolver } from 'app/status-action-resolver';
import { TtlActions } from 'app/ttl-actions';
export const createContainer = () => {
@@ -26,6 +27,8 @@ export const createContainer = () => {
const ttlActions = new TtlActions(requestTtlRepository, logger);
+ const statusActionResolver = new StatusActionResolver(ttlActions, logger);
+
const eventPublisher = new EventPublisher({
eventBusArn: eventPublisherEventBusArn,
dlqUrl: eventPublisherDlqUrl,
@@ -38,9 +41,9 @@ export const createContainer = () => {
});
return {
- ttlActions,
eventPublisher,
logger,
+ statusActionResolver,
};
};
diff --git a/lambdas/nhsapp-status-handler/src/index.ts b/lambdas/core-status-handler/src/index.ts
similarity index 100%
rename from lambdas/nhsapp-status-handler/src/index.ts
rename to lambdas/core-status-handler/src/index.ts
diff --git a/lambdas/nhsapp-status-handler/src/infra/config.ts b/lambdas/core-status-handler/src/infra/config.ts
similarity index 100%
rename from lambdas/nhsapp-status-handler/src/infra/config.ts
rename to lambdas/core-status-handler/src/infra/config.ts
diff --git a/lambdas/nhsapp-status-handler/src/infra/ttl-repository.ts b/lambdas/core-status-handler/src/infra/ttl-repository.ts
similarity index 58%
rename from lambdas/nhsapp-status-handler/src/infra/ttl-repository.ts
rename to lambdas/core-status-handler/src/infra/ttl-repository.ts
index f37753996..06bac389d 100644
--- a/lambdas/nhsapp-status-handler/src/infra/ttl-repository.ts
+++ b/lambdas/core-status-handler/src/infra/ttl-repository.ts
@@ -1,9 +1,14 @@
import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
-import { UpdateCommand, UpdateCommandOutput } from '@aws-sdk/lib-dynamodb';
+import {
+ DeleteCommand,
+ DeleteCommandOutput,
+ UpdateCommand,
+ UpdateCommandOutput,
+} from '@aws-sdk/lib-dynamodb';
import { TtlRecord } from 'types/types';
interface IDynamoCaller {
- send: (command: UpdateCommand) => Promise;
+ send(command: UpdateCommand | DeleteCommand): Promise;
}
export class TtlRepository {
@@ -30,7 +35,8 @@ export class TtlRepository {
};
const request = new UpdateCommand(params);
try {
- const output = await this.dynamoDocumentClient.send(request);
+ const output =
+ await this.dynamoDocumentClient.send(request);
return output.Attributes as TtlRecord;
} catch (error) {
@@ -40,6 +46,25 @@ export class TtlRepository {
throw error;
}
}
+
+ public async delete(
+ messageReference: string,
+ ): Promise {
+ const params = {
+ TableName: this.tableName,
+ Key: {
+ PK: messageReference,
+ SK: 'TTL',
+ },
+ ReturnValues: 'ALL_OLD' as const,
+ };
+
+ const output = await this.dynamoDocumentClient.send(
+ new DeleteCommand(params),
+ );
+
+ return output.Attributes as TtlRecord;
+ }
}
export default TtlRepository;
diff --git a/lambdas/nhsapp-status-handler/src/types/types.ts b/lambdas/core-status-handler/src/types/types.ts
similarity index 74%
rename from lambdas/nhsapp-status-handler/src/types/types.ts
rename to lambdas/core-status-handler/src/types/types.ts
index 7e4bab326..24872109a 100644
--- a/lambdas/nhsapp-status-handler/src/types/types.ts
+++ b/lambdas/core-status-handler/src/types/types.ts
@@ -3,3 +3,5 @@ import { MESHInboxMessageDownloaded } from 'digital-letters-events';
export type TtlRecord = {
event: MESHInboxMessageDownloaded;
};
+
+export type TtlItem = TtlRecord | undefined;
diff --git a/lambdas/nhsapp-status-handler/tsconfig.json b/lambdas/core-status-handler/tsconfig.json
similarity index 100%
rename from lambdas/nhsapp-status-handler/tsconfig.json
rename to lambdas/core-status-handler/tsconfig.json
diff --git a/lambdas/nhsapp-status-handler/src/__tests__/app/ttl-actions.test.ts b/lambdas/nhsapp-status-handler/src/__tests__/app/ttl-actions.test.ts
deleted file mode 100644
index b39e5dc7b..000000000
--- a/lambdas/nhsapp-status-handler/src/__tests__/app/ttl-actions.test.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { messageDownloadedEvent, nhsAppStatusEvent } from '__tests__/data';
-import { TtlActions } from 'app/ttl-actions';
-import { TtlRepository } from 'infra/ttl-repository';
-
-describe('TtlActions', () => {
- let repo: jest.Mocked;
- let logger: any;
- let ttlActions: TtlActions;
-
- beforeEach(() => {
- repo = { markWithdrawn: jest.fn() } as any;
- logger = { warn: jest.fn(), info: jest.fn() };
- ttlActions = new TtlActions(repo, logger);
- });
-
- it('returns success when markWithdrawn succeeds', async () => {
- repo.markWithdrawn.mockResolvedValue({ event: messageDownloadedEvent });
-
- const result = await ttlActions.markWithdrawn(nhsAppStatusEvent);
-
- expect(result).toEqual({
- result: 'success',
- ttlItem: { event: messageDownloadedEvent },
- });
- expect(logger.info).toHaveBeenCalledWith(
- expect.objectContaining({
- description: expect.stringContaining('TTL record marked as withdrawn'),
- messageReference: nhsAppStatusEvent.data.messageReference,
- }),
- );
- expect(repo.markWithdrawn).toHaveBeenCalledWith(
- nhsAppStatusEvent.data.messageReference,
- );
- });
-
- it('returns success when TTL record not found', async () => {
- // eslint-disable-next-line unicorn/no-useless-undefined
- repo.markWithdrawn.mockResolvedValue(undefined);
-
- const result = await ttlActions.markWithdrawn(nhsAppStatusEvent);
-
- expect(result).toEqual({ result: 'success' });
-
- expect(logger.info).toHaveBeenCalledWith(
- expect.objectContaining({
- description: expect.stringContaining('TTL record not found'),
- messageReference: nhsAppStatusEvent.data.messageReference,
- }),
- );
- expect(repo.markWithdrawn).toHaveBeenCalledWith(
- nhsAppStatusEvent.data.messageReference,
- );
- });
-
- it('returns failed and logs error when markWithdrawn throws', async () => {
- const error = new Error('fail');
- repo.markWithdrawn.mockRejectedValue(error);
-
- const result = await ttlActions.markWithdrawn(nhsAppStatusEvent);
-
- expect(result).toEqual({ result: 'failed' });
- expect(logger.warn).toHaveBeenCalledWith(
- expect.objectContaining({
- description: expect.stringContaining('Error marking TTL withdrawn'),
- messageReference: nhsAppStatusEvent.data.messageReference,
- err: error,
- }),
- );
- });
-});
diff --git a/lambdas/nhsapp-status-handler/src/__tests__/infra/ttl-repository.test.ts b/lambdas/nhsapp-status-handler/src/__tests__/infra/ttl-repository.test.ts
deleted file mode 100644
index 6fdf14a9a..000000000
--- a/lambdas/nhsapp-status-handler/src/__tests__/infra/ttl-repository.test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
-import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
-import { nhsAppStatusEvent } from '__tests__/data';
-import { TtlRepository } from 'infra/ttl-repository';
-
-describe('TtlRepository', () => {
- let dynamoDocumentClient: any;
- let repo: TtlRepository;
- const tableName = 'table';
-
- beforeEach(() => {
- dynamoDocumentClient = { send: jest.fn().mockResolvedValue({}) };
- repo = new TtlRepository(tableName, dynamoDocumentClient);
- });
-
- it('marks item as withdrawn', async () => {
- await repo.markWithdrawn(nhsAppStatusEvent.data.messageReference);
-
- const updateCommand: UpdateCommand =
- dynamoDocumentClient.send.mock.calls[0][0];
- expect(updateCommand.input).toStrictEqual({
- TableName: tableName,
- Key: {
- PK: nhsAppStatusEvent.data.messageReference,
- SK: 'TTL',
- },
- ConditionExpression: 'attribute_exists(PK)',
- UpdateExpression: 'set withdrawn = :val1',
- ExpressionAttributeValues: {
- ':val1': true,
- },
- ReturnValues: 'ALL_NEW',
- });
- });
-
- it('returns undefined on ConditionalCheckFailedException', async () => {
- const error = new ConditionalCheckFailedException({
- message: 'ConditionalCheckFailedException',
- $metadata: {},
- });
- dynamoDocumentClient.send.mockRejectedValue(error);
-
- const result = await repo.markWithdrawn(
- nhsAppStatusEvent.data.messageReference,
- );
-
- expect(result).toBeUndefined();
- });
-
- it('errors on dynamo error', async () => {
- const error = new Error('fail');
- dynamoDocumentClient.send.mockRejectedValue(error);
-
- await expect(
- repo.markWithdrawn(nhsAppStatusEvent.data.messageReference),
- ).rejects.toThrow(error);
- });
-});
diff --git a/lambdas/nhsapp-status-handler/src/app/ttl-actions.ts b/lambdas/nhsapp-status-handler/src/app/ttl-actions.ts
deleted file mode 100644
index ae6b0f22f..000000000
--- a/lambdas/nhsapp-status-handler/src/app/ttl-actions.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { ChannelStatusPublishedEvent, Logger } from 'utils';
-import { TtlRepository } from 'infra/ttl-repository';
-import { TtlRecord } from 'types/types';
-
-export type TtlItem = TtlRecord | undefined;
-
-export type TtlActionOutcome =
- | { result: 'success'; ttlItem: TtlItem }
- | { result: 'failed' };
-
-export class TtlActions {
- constructor(
- private readonly ttlRepository: TtlRepository,
- private readonly logger: Logger,
- ) {}
-
- async markWithdrawn(
- item: ChannelStatusPublishedEvent,
- ): Promise {
- const { messageReference } = item.data;
-
- let ttlItem: TtlItem;
-
- try {
- ttlItem = await this.ttlRepository.markWithdrawn(messageReference);
- } catch (error) {
- this.logger.warn({
- description: 'Error marking TTL withdrawn',
- messageReference,
- err: error,
- });
-
- return { result: 'failed' };
- }
-
- if (ttlItem) {
- this.logger.info({
- description: 'TTL record marked as withdrawn',
- messageReference,
- });
- } else {
- this.logger.info({
- description: 'TTL record not found',
- messageReference,
- });
- }
-
- return { result: 'success', ttlItem };
- }
-}
diff --git a/package-lock.json b/package-lock.json
index e5f263e90..67dcb835a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,7 +26,7 @@
"lambdas/report-event-transformer",
"lambdas/move-scanned-files-lambda",
"lambdas/report-generator",
- "lambdas/nhsapp-status-handler",
+ "lambdas/core-status-handler",
"utils/utils",
"utils/sender-management",
"scripts",
@@ -398,11 +398,12 @@
"dev": true,
"license": "MIT"
},
- "lambdas/file-scanner-lambda": {
- "name": "nhs-notify-digital-letters-file-scanner-lambda",
+ "lambdas/core-status-handler": {
+ "name": "nhs-notify-digital-letters-core-status-handler",
"version": "0.0.1",
"dependencies": {
- "@aws-sdk/client-s3": "^3.908.0",
+ "@aws-sdk/client-dynamodb": "^3.981.0",
+ "@aws-sdk/lib-dynamodb": "^3.908.0",
"digital-letters-events": "^0.0.1",
"utils": "^0.0.1"
},
@@ -411,12 +412,13 @@
"@types/aws-lambda": "^8.10.155",
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
- "jest-mock-extended": "^3.0.7",
"typescript": "^5.9.3"
}
},
- "lambdas/file-scanner-lambda/node_modules/@jest/core": {
+ "lambdas/core-status-handler/node_modules/@jest/core": {
"version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz",
+ "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -461,8 +463,10 @@
}
}
},
- "lambdas/file-scanner-lambda/node_modules/@jest/schemas": {
+ "lambdas/core-status-handler/node_modules/@jest/schemas": {
"version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+ "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -472,8 +476,10 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/file-scanner-lambda/node_modules/@jest/types": {
+ "lambdas/core-status-handler/node_modules/@jest/types": {
"version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
+ "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -488,15 +494,17 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/file-scanner-lambda/node_modules/@sinclair/typebox": {
+ "lambdas/core-status-handler/node_modules/@sinclair/typebox": {
"version": "0.27.10",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
"integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==",
"dev": true,
"license": "MIT"
},
- "lambdas/file-scanner-lambda/node_modules/@types/jest": {
+ "lambdas/core-status-handler/node_modules/@types/jest": {
"version": "29.5.14",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz",
+ "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -504,8 +512,10 @@
"pretty-format": "^29.0.0"
}
},
- "lambdas/file-scanner-lambda/node_modules/expect": {
+ "lambdas/core-status-handler/node_modules/expect": {
"version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -519,11 +529,12 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/file-scanner-lambda/node_modules/jest": {
+ "lambdas/core-status-handler/node_modules/jest": {
"version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
+ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -545,8 +556,10 @@
}
}
},
- "lambdas/file-scanner-lambda/node_modules/jest-cli": {
+ "lambdas/core-status-handler/node_modules/jest-cli": {
"version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz",
+ "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -577,8 +590,10 @@
}
}
},
- "lambdas/file-scanner-lambda/node_modules/jest-message-util": {
+ "lambdas/core-status-handler/node_modules/jest-message-util": {
"version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
+ "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -596,28 +611,20 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/file-scanner-lambda/node_modules/jest-mock-extended": {
- "version": "3.0.7",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ts-essentials": "^10.0.0"
- },
- "peerDependencies": {
- "jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0",
- "typescript": "^3.0.0 || ^4.0.0 || ^5.0.0"
- }
- },
- "lambdas/file-scanner-lambda/node_modules/jest-regex-util": {
+ "lambdas/core-status-handler/node_modules/jest-regex-util": {
"version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz",
+ "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/file-scanner-lambda/node_modules/jest-snapshot": {
+ "lambdas/core-status-handler/node_modules/jest-snapshot": {
"version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
+ "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -646,8 +653,10 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/file-scanner-lambda/node_modules/jest-util": {
+ "lambdas/core-status-handler/node_modules/jest-util": {
"version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
+ "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -662,7 +671,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/file-scanner-lambda/node_modules/picomatch": {
+ "lambdas/core-status-handler/node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
@@ -675,8 +684,10 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "lambdas/file-scanner-lambda/node_modules/pretty-format": {
+ "lambdas/core-status-handler/node_modules/pretty-format": {
"version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -688,32 +699,31 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/file-scanner-lambda/node_modules/react-is": {
+ "lambdas/core-status-handler/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
- "lambdas/key-generation": {
+ "lambdas/file-scanner-lambda": {
+ "name": "nhs-notify-digital-letters-file-scanner-lambda",
"version": "0.0.1",
"dependencies": {
- "date-fns": "^4.1.0",
- "esbuild": "^0.25.9",
- "jose": "^5.10.0",
- "utils": "*"
+ "@aws-sdk/client-s3": "^3.908.0",
+ "digital-letters-events": "^0.0.1",
+ "utils": "^0.0.1"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
- "@types/aws-lambda": "^8.10.148",
+ "@types/aws-lambda": "^8.10.155",
"@types/jest": "^29.5.14",
- "@types/node": "^24.0.10",
"jest": "^29.7.0",
"jest-mock-extended": "^3.0.7",
- "typescript": "^5.8.2"
+ "typescript": "^5.9.3"
}
},
- "lambdas/key-generation/node_modules/@jest/core": {
+ "lambdas/file-scanner-lambda/node_modules/@jest/core": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -759,7 +769,7 @@
}
}
},
- "lambdas/key-generation/node_modules/@jest/schemas": {
+ "lambdas/file-scanner-lambda/node_modules/@jest/schemas": {
"version": "29.6.3",
"dev": true,
"license": "MIT",
@@ -770,7 +780,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/key-generation/node_modules/@jest/types": {
+ "lambdas/file-scanner-lambda/node_modules/@jest/types": {
"version": "29.6.3",
"dev": true,
"license": "MIT",
@@ -786,14 +796,14 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/key-generation/node_modules/@sinclair/typebox": {
+ "lambdas/file-scanner-lambda/node_modules/@sinclair/typebox": {
"version": "0.27.10",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
"integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==",
"dev": true,
"license": "MIT"
},
- "lambdas/key-generation/node_modules/@types/jest": {
+ "lambdas/file-scanner-lambda/node_modules/@types/jest": {
"version": "29.5.14",
"dev": true,
"license": "MIT",
@@ -802,17 +812,7 @@
"pretty-format": "^29.0.0"
}
},
- "lambdas/key-generation/node_modules/@types/node": {
- "version": "24.11.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
- "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~7.16.0"
- }
- },
- "lambdas/key-generation/node_modules/expect": {
+ "lambdas/file-scanner-lambda/node_modules/expect": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -827,7 +827,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/key-generation/node_modules/jest": {
+ "lambdas/file-scanner-lambda/node_modules/jest": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -853,7 +853,7 @@
}
}
},
- "lambdas/key-generation/node_modules/jest-cli": {
+ "lambdas/file-scanner-lambda/node_modules/jest-cli": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -885,7 +885,7 @@
}
}
},
- "lambdas/key-generation/node_modules/jest-message-util": {
+ "lambdas/file-scanner-lambda/node_modules/jest-message-util": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -904,7 +904,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/key-generation/node_modules/jest-mock-extended": {
+ "lambdas/file-scanner-lambda/node_modules/jest-mock-extended": {
"version": "3.0.7",
"dev": true,
"license": "MIT",
@@ -916,7 +916,7 @@
"typescript": "^3.0.0 || ^4.0.0 || ^5.0.0"
}
},
- "lambdas/key-generation/node_modules/jest-regex-util": {
+ "lambdas/file-scanner-lambda/node_modules/jest-regex-util": {
"version": "29.6.3",
"dev": true,
"license": "MIT",
@@ -924,7 +924,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/key-generation/node_modules/jest-snapshot": {
+ "lambdas/file-scanner-lambda/node_modules/jest-snapshot": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -954,7 +954,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/key-generation/node_modules/jest-util": {
+ "lambdas/file-scanner-lambda/node_modules/jest-util": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -970,16 +970,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/key-generation/node_modules/jose": {
- "version": "5.10.0",
- "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
- "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/panva"
- }
- },
- "lambdas/key-generation/node_modules/picomatch": {
+ "lambdas/file-scanner-lambda/node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
@@ -992,7 +983,7 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "lambdas/key-generation/node_modules/pretty-format": {
+ "lambdas/file-scanner-lambda/node_modules/pretty-format": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -1005,31 +996,32 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/key-generation/node_modules/react-is": {
+ "lambdas/file-scanner-lambda/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
- "lambdas/move-scanned-files-lambda": {
- "name": "nhs-notify-digital-move-scanned-files-lambda",
+ "lambdas/key-generation": {
"version": "0.0.1",
"dependencies": {
- "axios": "^1.15.0",
- "digital-letters-events": "^0.0.1",
- "utils": "^0.0.1"
+ "date-fns": "^4.1.0",
+ "esbuild": "^0.25.9",
+ "jose": "^5.10.0",
+ "utils": "*"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
- "@types/aws-lambda": "^8.10.155",
+ "@types/aws-lambda": "^8.10.148",
"@types/jest": "^29.5.14",
+ "@types/node": "^24.0.10",
"jest": "^29.7.0",
"jest-mock-extended": "^3.0.7",
- "typescript": "^5.9.3"
+ "typescript": "^5.8.2"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/@jest/core": {
+ "lambdas/key-generation/node_modules/@jest/core": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -1075,7 +1067,7 @@
}
}
},
- "lambdas/move-scanned-files-lambda/node_modules/@jest/schemas": {
+ "lambdas/key-generation/node_modules/@jest/schemas": {
"version": "29.6.3",
"dev": true,
"license": "MIT",
@@ -1086,7 +1078,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/@jest/types": {
+ "lambdas/key-generation/node_modules/@jest/types": {
"version": "29.6.3",
"dev": true,
"license": "MIT",
@@ -1102,12 +1094,14 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/@sinclair/typebox": {
+ "lambdas/key-generation/node_modules/@sinclair/typebox": {
"version": "0.27.10",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
+ "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==",
"dev": true,
"license": "MIT"
},
- "lambdas/move-scanned-files-lambda/node_modules/@types/jest": {
+ "lambdas/key-generation/node_modules/@types/jest": {
"version": "29.5.14",
"dev": true,
"license": "MIT",
@@ -1116,7 +1110,17 @@
"pretty-format": "^29.0.0"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/expect": {
+ "lambdas/key-generation/node_modules/@types/node": {
+ "version": "24.11.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
+ "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "lambdas/key-generation/node_modules/expect": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -1131,7 +1135,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/jest": {
+ "lambdas/key-generation/node_modules/jest": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -1157,7 +1161,7 @@
}
}
},
- "lambdas/move-scanned-files-lambda/node_modules/jest-cli": {
+ "lambdas/key-generation/node_modules/jest-cli": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -1189,7 +1193,7 @@
}
}
},
- "lambdas/move-scanned-files-lambda/node_modules/jest-message-util": {
+ "lambdas/key-generation/node_modules/jest-message-util": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -1208,7 +1212,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/jest-mock-extended": {
+ "lambdas/key-generation/node_modules/jest-mock-extended": {
"version": "3.0.7",
"dev": true,
"license": "MIT",
@@ -1220,7 +1224,7 @@
"typescript": "^3.0.0 || ^4.0.0 || ^5.0.0"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/jest-regex-util": {
+ "lambdas/key-generation/node_modules/jest-regex-util": {
"version": "29.6.3",
"dev": true,
"license": "MIT",
@@ -1228,7 +1232,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/jest-snapshot": {
+ "lambdas/key-generation/node_modules/jest-snapshot": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -1258,7 +1262,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/jest-util": {
+ "lambdas/key-generation/node_modules/jest-util": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -1274,7 +1278,16 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/picomatch": {
+ "lambdas/key-generation/node_modules/jose": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
+ "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "lambdas/key-generation/node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
@@ -1287,7 +1300,7 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/pretty-format": {
+ "lambdas/key-generation/node_modules/pretty-format": {
"version": "29.7.0",
"dev": true,
"license": "MIT",
@@ -1300,17 +1313,18 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/move-scanned-files-lambda/node_modules/react-is": {
+ "lambdas/key-generation/node_modules/react-is": {
"version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
- "lambdas/nhsapp-status-handler": {
- "name": "nhs-notify-digital-letters-nhsapp-status-handler",
+ "lambdas/move-scanned-files-lambda": {
+ "name": "nhs-notify-digital-move-scanned-files-lambda",
"version": "0.0.1",
"dependencies": {
- "@aws-sdk/client-dynamodb": "^3.981.0",
- "@aws-sdk/lib-dynamodb": "^3.908.0",
+ "axios": "^1.15.0",
"digital-letters-events": "^0.0.1",
"utils": "^0.0.1"
},
@@ -1319,13 +1333,12 @@
"@types/aws-lambda": "^8.10.155",
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
+ "jest-mock-extended": "^3.0.7",
"typescript": "^5.9.3"
}
},
- "lambdas/nhsapp-status-handler/node_modules/@jest/core": {
+ "lambdas/move-scanned-files-lambda/node_modules/@jest/core": {
"version": "29.7.0",
- "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz",
- "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1370,10 +1383,8 @@
}
}
},
- "lambdas/nhsapp-status-handler/node_modules/@jest/schemas": {
+ "lambdas/move-scanned-files-lambda/node_modules/@jest/schemas": {
"version": "29.6.3",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
- "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1383,10 +1394,8 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/nhsapp-status-handler/node_modules/@jest/types": {
+ "lambdas/move-scanned-files-lambda/node_modules/@jest/types": {
"version": "29.6.3",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
- "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1401,17 +1410,13 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/nhsapp-status-handler/node_modules/@sinclair/typebox": {
+ "lambdas/move-scanned-files-lambda/node_modules/@sinclair/typebox": {
"version": "0.27.10",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
- "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==",
"dev": true,
"license": "MIT"
},
- "lambdas/nhsapp-status-handler/node_modules/@types/jest": {
+ "lambdas/move-scanned-files-lambda/node_modules/@types/jest": {
"version": "29.5.14",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz",
- "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1419,10 +1424,8 @@
"pretty-format": "^29.0.0"
}
},
- "lambdas/nhsapp-status-handler/node_modules/expect": {
+ "lambdas/move-scanned-files-lambda/node_modules/expect": {
"version": "29.7.0",
- "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
- "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1436,12 +1439,11 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/nhsapp-status-handler/node_modules/jest": {
+ "lambdas/move-scanned-files-lambda/node_modules/jest": {
"version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
- "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -1463,10 +1465,8 @@
}
}
},
- "lambdas/nhsapp-status-handler/node_modules/jest-cli": {
+ "lambdas/move-scanned-files-lambda/node_modules/jest-cli": {
"version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz",
- "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1497,10 +1497,8 @@
}
}
},
- "lambdas/nhsapp-status-handler/node_modules/jest-message-util": {
+ "lambdas/move-scanned-files-lambda/node_modules/jest-message-util": {
"version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
- "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1518,20 +1516,28 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/nhsapp-status-handler/node_modules/jest-regex-util": {
+ "lambdas/move-scanned-files-lambda/node_modules/jest-mock-extended": {
+ "version": "3.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ts-essentials": "^10.0.0"
+ },
+ "peerDependencies": {
+ "jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0",
+ "typescript": "^3.0.0 || ^4.0.0 || ^5.0.0"
+ }
+ },
+ "lambdas/move-scanned-files-lambda/node_modules/jest-regex-util": {
"version": "29.6.3",
- "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz",
- "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/nhsapp-status-handler/node_modules/jest-snapshot": {
+ "lambdas/move-scanned-files-lambda/node_modules/jest-snapshot": {
"version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
- "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1560,10 +1566,8 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/nhsapp-status-handler/node_modules/jest-util": {
+ "lambdas/move-scanned-files-lambda/node_modules/jest-util": {
"version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
- "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1578,7 +1582,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/nhsapp-status-handler/node_modules/picomatch": {
+ "lambdas/move-scanned-files-lambda/node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
@@ -1591,10 +1595,8 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "lambdas/nhsapp-status-handler/node_modules/pretty-format": {
+ "lambdas/move-scanned-files-lambda/node_modules/pretty-format": {
"version": "29.7.0",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
- "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1606,10 +1608,8 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "lambdas/nhsapp-status-handler/node_modules/react-is": {
+ "lambdas/move-scanned-files-lambda/node_modules/react-is": {
"version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
@@ -22257,6 +22257,10 @@
"resolved": "lambdas/core-notifier-lambda",
"link": true
},
+ "node_modules/nhs-notify-digital-letters-core-status-handler": {
+ "resolved": "lambdas/core-status-handler",
+ "link": true
+ },
"node_modules/nhs-notify-digital-letters-file-scanner-lambda": {
"resolved": "lambdas/file-scanner-lambda",
"link": true
@@ -22265,10 +22269,6 @@
"resolved": "tests/playwright",
"link": true
},
- "node_modules/nhs-notify-digital-letters-nhsapp-status-handler": {
- "resolved": "lambdas/nhsapp-status-handler",
- "link": true
- },
"node_modules/nhs-notify-digital-letters-pact-tests": {
"resolved": "tests/pact-tests",
"link": true
diff --git a/package.json b/package.json
index 9a2b25239..1b79a51f7 100644
--- a/package.json
+++ b/package.json
@@ -81,7 +81,7 @@
"lambdas/report-event-transformer",
"lambdas/move-scanned-files-lambda",
"lambdas/report-generator",
- "lambdas/nhsapp-status-handler",
+ "lambdas/core-status-handler",
"utils/utils",
"utils/sender-management",
"scripts",
diff --git a/src/cloudevents/domains/digital-letters/2025-10-draft/data/digital-letters-queue-digital-letter-read-data.schema.yaml b/src/cloudevents/domains/digital-letters/2025-10-draft/data/digital-letters-queue-digital-letter-read-data.schema.yaml
index dc7823190..fca4f08a3 100644
--- a/src/cloudevents/domains/digital-letters/2025-10-draft/data/digital-letters-queue-digital-letter-read-data.schema.yaml
+++ b/src/cloudevents/domains/digital-letters/2025-10-draft/data/digital-letters-queue-digital-letter-read-data.schema.yaml
@@ -8,6 +8,9 @@ properties:
$ref: ../defs/requests.schema.yaml#/properties/messageReference
senderId:
$ref: ../defs/requests.schema.yaml#/properties/senderId
+ supplierStatus:
+ $ref: ../defs/core.schema.yaml#/properties/supplierStatus
required:
- messageReference
- senderId
+ - supplierStatus
diff --git a/src/cloudevents/domains/digital-letters/2025-10-draft/defs/core.schema.yaml b/src/cloudevents/domains/digital-letters/2025-10-draft/defs/core.schema.yaml
index f0b211191..43314912b 100644
--- a/src/cloudevents/domains/digital-letters/2025-10-draft/defs/core.schema.yaml
+++ b/src/cloudevents/domains/digital-letters/2025-10-draft/defs/core.schema.yaml
@@ -21,6 +21,11 @@ properties:
description: "A human readable desription of the reason for failure"
examples:
- "Failed reason: Not registered with NHS App"
+ supplierStatus:
+ type: string
+ description: "Status returned by the supplier for this channel"
+ examples:
+ - "paper_letter_opted_out"
time:
title: "Event Time"
description: "Timestamp when the event occurred (RFC 3339)."
diff --git a/tests/pact-tests/consumer/channel-status-published.consumer.pact.test.ts b/tests/pact-tests/consumer/channel-status-published.consumer.pact.test.ts
index e85f905d7..4ba183c98 100644
--- a/tests/pact-tests/consumer/channel-status-published.consumer.pact.test.ts
+++ b/tests/pact-tests/consumer/channel-status-published.consumer.pact.test.ts
@@ -13,7 +13,7 @@ import {
import { getPathFromProvider } from '../utils/path-utils';
async function handle(event: unknown) {
- // The schema used by the nhsapp-status-handler to validate the event.
+ // The schema used by the core-status-handler to validate the event.
$ChannelStatusPublishedEvent.parse(event);
}
diff --git a/tests/playwright/constants/backend-constants.ts b/tests/playwright/constants/backend-constants.ts
index 280192bd7..cd3cfff6d 100644
--- a/tests/playwright/constants/backend-constants.ts
+++ b/tests/playwright/constants/backend-constants.ts
@@ -38,7 +38,7 @@ export const PRINT_SENDER_DLQ_NAME = `${CSI}-print-sender-dlq`;
export const MOVE_SCANNED_FILES_NAME = `${CSI}-move-scanned-files-queue`;
export const MOVE_SCANNED_FILES_DLQ_NAME = `${CSI}-move-scanned-files-dlq`;
export const REPORT_SENDER_DLQ_NAME = `${CSI}-report-sender-dlq`;
-export const NHSAPP_STATUS_HANDLER_DLQ_NAME = `${CSI}-nhsapp-status-handler-dlq`;
+export const CORE_STATUS_HANDLER_DLQ_NAME = `${CSI}-core-status-handler-dlq`;
// Queue Url Prefix
export const SQS_URL_PREFIX = `https://sqs.${REGION}.amazonaws.com/${AWS_ACCOUNT_ID}/`;
@@ -78,7 +78,7 @@ export const PRINT_SENDER_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-print-send
export const MOVE_SCANNED_FILES_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-move-scanned-files`;
export const MESH_DOWNLOAD_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-mesh-download`;
export const CREATE_TTL_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-ttl-create`;
-export const NHSAPP_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-nhsapp-status-handler`;
+export const CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-core-status-handler`;
// Data Firehose
export const FIREHOSE_STREAM_NAME = `${CSI}-to-s3-reporting`;
diff --git a/tests/playwright/digital-letters-component-tests/core-status-handler.component.spec.ts b/tests/playwright/digital-letters-component-tests/core-status-handler.component.spec.ts
new file mode 100644
index 000000000..93160c91d
--- /dev/null
+++ b/tests/playwright/digital-letters-component-tests/core-status-handler.component.spec.ts
@@ -0,0 +1,666 @@
+import { expect, test } from '@playwright/test';
+import {
+ CORE_STATUS_HANDLER_DLQ_NAME,
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ EVENT_BUS_LOG_GROUP_NAME,
+} from 'constants/backend-constants';
+import { SENDER_ID_VALID_FOR_NOTIFY_SANDBOX } from 'constants/tests-constants';
+import { MESHInboxMessageDownloaded } from 'digital-letters-events';
+import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers';
+import { getTtl, putTtl } from 'helpers/dynamodb-helpers';
+import eventPublisher from 'helpers/event-bus-helpers';
+import expectToPassEventually from 'helpers/expectations';
+import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers';
+import {
+ ChannelStatusPublishedEvent,
+ MessageStatusPublishedEvent,
+} from 'utils';
+import { v4 as uuidv4 } from 'uuid';
+
+test.describe('Digital Letters - Core Status Handler', () => {
+ test.beforeAll(async () => {
+ await purgeQueue(CORE_STATUS_HANDLER_DLQ_NAME);
+ });
+
+ const baseEvent: MESHInboxMessageDownloaded = {
+ id: 'id',
+ specversion: '1.0',
+ source: '/nhs/england/notify/production/primary/digitalletters/mesh',
+ subject:
+ 'customer/920fca11-596a-4eca-9c47-99f624614658/recipient/769acdd4-6a47-496f-999f-76a6fd2c3959',
+ type: 'uk.nhs.notify.digital.letters.mesh.inbox.message.downloaded.v1',
+ time: '2023-06-20T12:00:00Z',
+ plane: 'data',
+ recordedtime: '2023-06-20T12:00:00.250Z',
+ severitynumber: 2,
+ traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
+ datacontenttype: 'application/json',
+ dataschema:
+ 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-mesh-inbox-message-downloaded-data.schema.json',
+ dataschemaversion: '1.0.0',
+ severitytext: 'INFO',
+ data: {
+ meshMessageId: '12345',
+ messageUri: 'uri',
+ messageReference: 'ref1',
+ senderId: SENDER_ID_VALID_FOR_NOTIFY_SANDBOX,
+ },
+ };
+
+ test.describe('channel.status.PUBLISHED', () => {
+ const channelFailedEvent: ChannelStatusPublishedEvent = {
+ source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
+ type: 'uk.nhs.notify.channel.status.PUBLISHED.v1',
+ data: {
+ channelStatus: 'failed',
+ supplierStatus: 'rejected',
+ messageReference: '',
+ },
+ };
+
+ test('when supplierStatus is rejected and channelStatus is failed - delete TTL', async () => {
+ const event = {
+ ...baseEvent,
+ data: {
+ ...baseEvent.data,
+ messageReference: uuidv4(),
+ },
+ };
+
+ const concatedReference = `${event.data.senderId}_${event.data.messageReference}`;
+
+ const ttlItem = {
+ PK: concatedReference,
+ SK: 'TTL',
+ dateOfExpiry: '2023-12-31#0',
+ event,
+ ttl: Date.now() / 1000 + 3600,
+ };
+
+ const putResponseCode = await putTtl(ttlItem);
+ expect(putResponseCode).toBe(200);
+
+ await eventPublisher.sendEvents(
+ [
+ {
+ ...channelFailedEvent,
+ data: {
+ ...channelFailedEvent.data,
+ messageReference: concatedReference,
+ },
+ },
+ ],
+ () => true,
+ );
+
+ await expectToPassEventually(async () => {
+ const ttl = await getTtl(
+ event.data.senderId,
+ event.data.messageReference,
+ );
+
+ expect(ttl.length).toBe(0);
+ });
+
+ await expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "TTL record deleted"',
+ `$.message.messageReference = "${concatedReference}"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }, 150);
+ });
+
+ test.describe('paper_letter_opted_out', () => {
+ const optedOutEvent: ChannelStatusPublishedEvent = {
+ source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
+ type: 'uk.nhs.notify.channel.status.PUBLISHED.v1',
+ data: {
+ channelStatus: 'delivered',
+ supplierStatus: 'paper_letter_opted_out',
+ messageReference: '',
+ },
+ };
+
+ test('when supplierStatus is paper_letter_opted_out - mark TTL withdrawn and publish digital.letter.read event', async () => {
+ const event = {
+ ...baseEvent,
+ data: {
+ ...baseEvent.data,
+ messageReference: uuidv4(),
+ },
+ };
+
+ const concatedReference = `${event.data.senderId}_${event.data.messageReference}`;
+
+ const ttlItem = {
+ PK: concatedReference,
+ SK: 'TTL',
+ dateOfExpiry: '2023-12-31#0',
+ event,
+ ttl: Date.now() / 1000 + 3600,
+ };
+
+ const putResponseCode = await putTtl(ttlItem);
+ expect(putResponseCode).toBe(200);
+
+ await eventPublisher.sendEvents(
+ [
+ {
+ ...optedOutEvent,
+ data: {
+ ...optedOutEvent.data,
+ messageReference: concatedReference,
+ },
+ },
+ ],
+ () => true,
+ );
+
+ await expectToPassEventually(async () => {
+ const ttl = await getTtl(
+ event.data.senderId,
+ event.data.messageReference,
+ );
+
+ expect(ttl.length).toBe(1);
+ expect(ttl[0]).toHaveProperty('withdrawn', true);
+ });
+
+ await Promise.all([
+ expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ EVENT_BUS_LOG_GROUP_NAME,
+ [
+ '$.message_type = "EVENT_RECEIPT"',
+ '$.details.detail_type = "uk.nhs.notify.digital.letters.queue.digital.letter.read.v1"',
+ `$.details.event_detail = "*\\"messageReference\\":\\"${event.data.messageReference}\\"*"`,
+ `$.details.event_detail = "*\\"supplierStatus\\":\\"${optedOutEvent.data.supplierStatus}\\"*"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }),
+
+ expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "TTL record marked as withdrawn"',
+ `$.message.messageReference = "${concatedReference}"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }, 150),
+ ]);
+ });
+
+ test('when duplicate event is received for the same TTL record - process them both', async () => {
+ const event = {
+ ...baseEvent,
+ data: {
+ ...baseEvent.data,
+ messageReference: uuidv4(),
+ },
+ };
+
+ const concatedReference = `${event.data.senderId}_${event.data.messageReference}`;
+
+ const ttlItem = {
+ PK: concatedReference,
+ SK: 'TTL',
+ dateOfExpiry: '2023-12-31#0',
+ event,
+ ttl: Date.now() / 1000 + 3600,
+ };
+
+ const channelStatusPublishedEvent = {
+ ...optedOutEvent,
+ data: {
+ ...optedOutEvent.data,
+ messageReference: concatedReference,
+ },
+ };
+
+ const putResponseCode = await putTtl(ttlItem);
+ expect(putResponseCode).toBe(200);
+
+ await eventPublisher.sendEvents(
+ [channelStatusPublishedEvent, channelStatusPublishedEvent],
+ () => true,
+ );
+
+ await expectToPassEventually(async () => {
+ const ttl = await getTtl(
+ event.data.senderId,
+ event.data.messageReference,
+ );
+
+ expect(ttl.length).toBe(1);
+ expect(ttl[0]).toHaveProperty('withdrawn', true);
+ });
+
+ await Promise.all([
+ expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ EVENT_BUS_LOG_GROUP_NAME,
+ [
+ '$.message_type = "EVENT_RECEIPT"',
+ '$.details.detail_type = "uk.nhs.notify.digital.letters.queue.digital.letter.read.v1"',
+ `$.details.event_detail = "*\\"messageReference\\":\\"${event.data.messageReference}\\"*"`,
+ `$.details.event_detail = "*\\"supplierStatus\\":\\"${optedOutEvent.data.supplierStatus}\\"*"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(2);
+ }),
+
+ expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "TTL record marked as withdrawn"',
+ `$.message.messageReference = "${concatedReference}"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(2);
+ }, 150),
+ ]);
+ });
+
+ test('when TTL record is not found - perform no operations', async () => {
+ const concatedReference = `${uuidv4()}_${uuidv4()}`;
+
+ await eventPublisher.sendEvents(
+ [
+ {
+ ...optedOutEvent,
+ data: {
+ ...optedOutEvent.data,
+ messageReference: concatedReference,
+ },
+ },
+ ],
+ () => true,
+ );
+
+ await expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "TTL record not found"',
+ `$.message.messageReference = "${concatedReference}"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }, 150);
+ });
+ });
+
+ test.describe('paper_letter_opted_in', () => {
+ const optedInEvent: ChannelStatusPublishedEvent = {
+ source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
+ type: 'uk.nhs.notify.channel.status.PUBLISHED.v1',
+ data: {
+ channelStatus: 'delivered',
+ supplierStatus: 'paper_letter_opted_in',
+ messageReference: '',
+ },
+ };
+
+ test('when supplierStatus is paper_letter_opted_in - delete TTL and publish digital.letter.read event', async () => {
+ const event = {
+ ...baseEvent,
+ data: {
+ ...baseEvent.data,
+ messageReference: uuidv4(),
+ },
+ };
+
+ const concatedReference = `${event.data.senderId}_${event.data.messageReference}`;
+
+ const ttlItem = {
+ PK: concatedReference,
+ SK: 'TTL',
+ dateOfExpiry: '2023-12-31#0',
+ event,
+ ttl: Date.now() / 1000 + 3600,
+ };
+
+ const putResponseCode = await putTtl(ttlItem);
+ expect(putResponseCode).toBe(200);
+
+ await eventPublisher.sendEvents(
+ [
+ {
+ ...optedInEvent,
+ data: {
+ ...optedInEvent.data,
+ messageReference: concatedReference,
+ },
+ },
+ ],
+ () => true,
+ );
+
+ await expectToPassEventually(async () => {
+ const ttl = await getTtl(
+ event.data.senderId,
+ event.data.messageReference,
+ );
+
+ expect(ttl.length).toBe(0);
+ });
+
+ await Promise.all([
+ expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ EVENT_BUS_LOG_GROUP_NAME,
+ [
+ '$.message_type = "EVENT_RECEIPT"',
+ '$.details.detail_type = "uk.nhs.notify.digital.letters.queue.digital.letter.read.v1"',
+ `$.details.event_detail = "*\\"messageReference\\":\\"${event.data.messageReference}\\"*"`,
+ `$.details.event_detail = "*\\"supplierStatus\\":\\"${optedInEvent.data.supplierStatus}\\"*"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }),
+ expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "TTL record deleted"',
+ `$.message.messageReference = "${concatedReference}"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }, 150),
+ ]);
+ });
+
+ test('when duplicate event is received for the same TTL record - process them both1', async () => {
+ const event = {
+ ...baseEvent,
+ data: {
+ ...baseEvent.data,
+ messageReference: uuidv4(),
+ },
+ };
+
+ const concatedReference = `${event.data.senderId}_${event.data.messageReference}`;
+
+ const ttlItem = {
+ PK: concatedReference,
+ SK: 'TTL',
+ dateOfExpiry: '2023-12-31#0',
+ event,
+ ttl: Date.now() / 1000 + 3600,
+ };
+
+ const channelStatusPublishedEvent = {
+ ...optedInEvent,
+ data: {
+ ...optedInEvent.data,
+ messageReference: concatedReference,
+ },
+ };
+
+ const putResponseCode = await putTtl(ttlItem);
+ expect(putResponseCode).toBe(200);
+
+ await eventPublisher.sendEvents(
+ [channelStatusPublishedEvent, channelStatusPublishedEvent],
+ () => true,
+ );
+
+ await expectToPassEventually(async () => {
+ const ttl = await getTtl(
+ event.data.senderId,
+ event.data.messageReference,
+ );
+
+ expect(ttl.length).toBe(0);
+ });
+
+ await Promise.all([
+ expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ EVENT_BUS_LOG_GROUP_NAME,
+ [
+ '$.message_type = "EVENT_RECEIPT"',
+ '$.details.detail_type = "uk.nhs.notify.digital.letters.queue.digital.letter.read.v1"',
+ `$.details.event_detail = "*\\"messageReference\\":\\"${event.data.messageReference}\\"*"`,
+ `$.details.event_detail = "*\\"supplierStatus\\":\\"${optedInEvent.data.supplierStatus}\\"*"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }),
+
+ expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "TTL record deleted"',
+ `$.message.messageReference = "${concatedReference}"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }, 150),
+
+ expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "TTL record not found"',
+ `$.message.messageReference = "${concatedReference}"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }, 150),
+ ]);
+ });
+
+ test('when TTL record is not found - perform no operations', async () => {
+ const concatedReference = `${uuidv4()}_${uuidv4()}`;
+
+ await eventPublisher.sendEvents(
+ [
+ {
+ ...optedInEvent,
+ data: {
+ ...optedInEvent.data,
+ messageReference: concatedReference,
+ },
+ },
+ ],
+ () => true,
+ );
+
+ await expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "TTL record not found"',
+ `$.message.messageReference = "${concatedReference}"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }, 150);
+ });
+ });
+ });
+
+ test.describe('message.status.PUBLISHED', () => {
+ const messageEvent: MessageStatusPublishedEvent = {
+ source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
+ type: 'uk.nhs.notify.message.status.PUBLISHED.v1',
+ data: {
+ messageStatus: 'failed',
+ messageReference: '',
+ resolvedChannels: [],
+ },
+ };
+
+ test('when messageStatus is failed and resolvedChannels is empty - mark TTL withdrawn', async () => {
+ const event = {
+ ...baseEvent,
+ data: {
+ ...baseEvent.data,
+ messageReference: uuidv4(),
+ },
+ };
+
+ const concatedReference = `${event.data.senderId}_${event.data.messageReference}`;
+
+ const ttlItem = {
+ PK: concatedReference,
+ SK: 'TTL',
+ dateOfExpiry: '2023-12-31#0',
+ event,
+ ttl: Date.now() / 1000 + 3600,
+ };
+
+ const putResponseCode = await putTtl(ttlItem);
+ expect(putResponseCode).toBe(200);
+
+ await eventPublisher.sendEvents(
+ [
+ {
+ ...messageEvent,
+ data: {
+ ...messageEvent.data,
+ messageReference: concatedReference,
+ },
+ },
+ ],
+ () => true,
+ );
+
+ await expectToPassEventually(async () => {
+ const ttl = await getTtl(
+ event.data.senderId,
+ event.data.messageReference,
+ );
+
+ expect(ttl.length).toBe(1);
+ expect(ttl[0]).toHaveProperty('withdrawn', true);
+ });
+
+ await expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "TTL record marked as withdrawn"',
+ `$.message.messageReference = "${concatedReference}"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }, 150);
+ });
+
+ test('when messageStatus is failed and resolvedChannels is NOT empty - skip', async () => {
+ const event = {
+ ...baseEvent,
+ data: {
+ ...baseEvent.data,
+ messageReference: uuidv4(),
+ },
+ };
+
+ const concatedReference = `${event.data.senderId}_${event.data.messageReference}`;
+
+ const ttlItem = {
+ PK: concatedReference,
+ SK: 'TTL',
+ dateOfExpiry: '2023-12-31#0',
+ event,
+ ttl: Date.now() / 1000 + 3600,
+ };
+
+ const putResponseCode = await putTtl(ttlItem);
+ expect(putResponseCode).toBe(200);
+
+ await eventPublisher.sendEvents(
+ [
+ {
+ ...messageEvent,
+ data: {
+ ...messageEvent.data,
+ messageReference: concatedReference,
+ resolvedChannels: ['LETTER'],
+ },
+ },
+ ],
+ () => true,
+ );
+
+ await expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "Event skipped"',
+ `$.message.data.messageReference = "${concatedReference}"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }, 150);
+ });
+ });
+
+ test('when event is invalid - send to dlq', async () => {
+ test.setTimeout(160_000);
+
+ const concatedReference = `${uuidv4()}_${uuidv4()}`;
+
+ await eventPublisher.sendEvents(
+ [
+ {
+ source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
+ type: 'uk.nhs.notify.message.status.PUBLISHED.v1',
+ data: {
+ messageReference: concatedReference,
+ messageStatus: 'I am not valid',
+ },
+ },
+ ],
+ () => true,
+ );
+
+ await Promise.all([
+ expectToPassEventually(async () => {
+ const eventLogEntry = await getLogsFromCloudwatch(
+ CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
+ [
+ '$.message.description = "Error parsing sqs record"',
+ `$.message.messageReference = "${concatedReference}"`,
+ String.raw`$.message.err.message = "*\"invalid_value\"*"`,
+ String.raw`$.message.err.message = "*\"messageStatus\"*"`,
+ ],
+ );
+
+ expect(eventLogEntry.length).toEqual(1);
+ }, 150),
+
+ expectMessageContainingString(
+ CORE_STATUS_HANDLER_DLQ_NAME,
+ concatedReference,
+ 150,
+ ),
+ ]);
+ });
+});
diff --git a/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts b/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts
index e96cbb604..2dc09427d 100644
--- a/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts
+++ b/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts
@@ -24,138 +24,140 @@ test.describe('File Scanner', () => {
await purgeQueue(FILE_SCANNER_DLQ_NAME);
test.setTimeout(250_000);
});
-});
-test('should extract PDF from DocumentReference and store in unscanned bucket with metadata', async () => {
- const messageReference = uuidv4();
- const senderId = 'TEST_SENDER_001';
- const documentReferenceKey = `${PREFIX_DL_FILES}${messageReference}`;
-
- const pdfContent = Buffer.from('Sample PDF content for test');
- const documentReference = {
- resourceType: 'DocumentReference',
- id: messageReference,
- content: [
- {
- attachment: {
- contentType: 'application/pdf',
- data: pdfContent.toString('base64'),
+ test('should extract PDF from DocumentReference and store in unscanned bucket with metadata', async () => {
+ const messageReference = uuidv4();
+ const senderId = 'TEST_SENDER_001';
+ const documentReferenceKey = `${PREFIX_DL_FILES}${messageReference}`;
+
+ const pdfContent = Buffer.from('Sample PDF content for test');
+ const documentReference = {
+ resourceType: 'DocumentReference',
+ id: messageReference,
+ content: [
+ {
+ attachment: {
+ contentType: 'application/pdf',
+ data: pdfContent.toString('base64'),
+ },
},
- },
- ],
- };
-
- await putDataS3(documentReference, {
- Bucket: DOCUMENT_REFERENCE_BUCKET,
- Key: documentReferenceKey,
- });
+ ],
+ };
- const eventId = uuidv4();
- const messageUri = `s3://${DOCUMENT_REFERENCE_BUCKET}/${documentReferenceKey}`;
- const eventTime = new Date().toISOString();
-
- await eventPublisher.sendEvents(
- [
- {
- id: eventId,
- plane: 'data',
- dataschemaversion: '1.0.0',
- specversion: '1.0',
- source: `/nhs/england/notify/development/dev-1/digitalletters/queue`,
- subject: `message/${messageReference}`,
- type: 'uk.nhs.notify.digital.letters.queue.item.dequeued.v1',
- time: eventTime,
- recordedtime: eventTime,
- severitynumber: 2,
- traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
- datacontenttype: 'application/json',
- dataschema:
- 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-queue-item-dequeued-data.schema.json',
- severitytext: 'INFO',
- data: {
- messageReference,
- senderId,
- messageUri,
- },
- },
- ],
- validateItemDequeued,
- );
-
- await expectToPassEventually(async () => {
- const expectedKey = `${PREFIX_DL_FILES}${messageReference}.pdf`;
- const expectedUri = `s3://${UNSCANNED_FILES_BUCKET}/${expectedKey}`;
-
- const storedPdf = await getS3ObjectBufferFromUri(expectedUri);
- expect(storedPdf).toBeDefined();
- expect(storedPdf.toString()).toEqual(pdfContent.toString());
-
- const metadata = await getS3ObjectMetadata({
- Bucket: UNSCANNED_FILES_BUCKET,
- Key: expectedKey,
+ await putDataS3(documentReference, {
+ Bucket: DOCUMENT_REFERENCE_BUCKET,
+ Key: documentReferenceKey,
});
- expect(metadata).toBeDefined();
- expect(metadata?.messagereference).toEqual(messageReference);
- expect(metadata?.senderid).toEqual(senderId);
- expect(metadata?.createdat).toBeDefined();
- }, 120);
-});
-test('should handle validation errors by sending messages to DLQ', async () => {
- test.setTimeout(160_000);
- const messageReference = uuidv4();
- const senderId = 'TEST_SENDER_002';
- const documentReferenceKey = `document-reference/${messageReference}`;
-
- const documentReference = {
- resourceType: 'DocumentReference',
- id: messageReference,
- content: [],
- };
-
- await putDataS3(documentReference, {
- Bucket: DOCUMENT_REFERENCE_BUCKET,
- Key: documentReferenceKey,
+ const eventId = uuidv4();
+ const messageUri = `s3://${DOCUMENT_REFERENCE_BUCKET}/${documentReferenceKey}`;
+ const eventTime = new Date().toISOString();
+
+ await eventPublisher.sendEvents(
+ [
+ {
+ id: eventId,
+ plane: 'data',
+ dataschemaversion: '1.0.0',
+ specversion: '1.0',
+ source: `/nhs/england/notify/development/dev-1/digitalletters/queue`,
+ subject: `message/${messageReference}`,
+ type: 'uk.nhs.notify.digital.letters.queue.item.dequeued.v1',
+ time: eventTime,
+ recordedtime: eventTime,
+ severitynumber: 2,
+ traceparent:
+ '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
+ datacontenttype: 'application/json',
+ dataschema:
+ 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-queue-item-dequeued-data.schema.json',
+ severitytext: 'INFO',
+ data: {
+ messageReference,
+ senderId,
+ messageUri,
+ },
+ },
+ ],
+ validateItemDequeued,
+ );
+
+ await expectToPassEventually(async () => {
+ const expectedKey = `${PREFIX_DL_FILES}${messageReference}.pdf`;
+ const expectedUri = `s3://${UNSCANNED_FILES_BUCKET}/${expectedKey}`;
+
+ const storedPdf = await getS3ObjectBufferFromUri(expectedUri);
+ expect(storedPdf).toBeDefined();
+ expect(storedPdf.toString()).toEqual(pdfContent.toString());
+
+ const metadata = await getS3ObjectMetadata({
+ Bucket: UNSCANNED_FILES_BUCKET,
+ Key: expectedKey,
+ });
+ expect(metadata).toBeDefined();
+ expect(metadata?.messagereference).toEqual(messageReference);
+ expect(metadata?.senderid).toEqual(senderId);
+ expect(metadata?.createdat).toBeDefined();
+ }, 120);
});
- const eventId = uuidv4();
- const messageUri = `s3://${DOCUMENT_REFERENCE_BUCKET}/${documentReferenceKey}`;
- const eventTime = new Date().toISOString();
-
- await eventPublisher.sendEvents(
- [
- {
- id: eventId,
- specversion: '1.0',
- plane: 'data',
- dataschemaversion: '1.0.0',
- source: `/nhs/england/notify/development/dev-1/digitalletters/queue`,
- subject: `message/${messageReference}`,
- type: 'uk.nhs.notify.digital.letters.queue.item.dequeued.v1',
- time: eventTime,
- recordedtime: eventTime,
- severitynumber: 2,
- traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
- datacontenttype: 'application/json',
- dataschema:
- 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-queue-item-dequeued-data.schema.json',
- severitytext: 'INFO',
- data: {
- messageReference,
- senderId,
- messageUri,
+ test('should handle validation errors by sending messages to DLQ', async () => {
+ test.setTimeout(160_000);
+ const messageReference = uuidv4();
+ const senderId = 'TEST_SENDER_002';
+ const documentReferenceKey = `document-reference/${messageReference}`;
+
+ const documentReference = {
+ resourceType: 'DocumentReference',
+ id: messageReference,
+ content: [],
+ };
+
+ await putDataS3(documentReference, {
+ Bucket: DOCUMENT_REFERENCE_BUCKET,
+ Key: documentReferenceKey,
+ });
+
+ const eventId = uuidv4();
+ const messageUri = `s3://${DOCUMENT_REFERENCE_BUCKET}/${documentReferenceKey}`;
+ const eventTime = new Date().toISOString();
+
+ await eventPublisher.sendEvents(
+ [
+ {
+ id: eventId,
+ specversion: '1.0',
+ plane: 'data',
+ dataschemaversion: '1.0.0',
+ source: `/nhs/england/notify/development/dev-1/digitalletters/queue`,
+ subject: `message/${messageReference}`,
+ type: 'uk.nhs.notify.digital.letters.queue.item.dequeued.v1',
+ time: eventTime,
+ recordedtime: eventTime,
+ severitynumber: 2,
+ traceparent:
+ '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
+ datacontenttype: 'application/json',
+ dataschema:
+ 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-queue-item-dequeued-data.schema.json',
+ severitytext: 'INFO',
+ data: {
+ messageReference,
+ senderId,
+ messageUri,
+ },
},
- },
- ],
- validateItemDequeued,
- );
-
- // Verify the file was NOT processed successfully
- await expectToPassEventually(async () => {
- const expectedKey = `${ENV}/${messageReference}.pdf`;
- const expectedUri = `s3://${UNSCANNED_FILES_BUCKET}/${expectedKey}`;
- await expect(getS3ObjectBufferFromUri(expectedUri)).rejects.toThrow();
- }, 150);
- // Verify there is a message in the DLQ
- await expectMessageContainingString(FILE_SCANNER_DLQ_NAME, eventId, 150);
+ ],
+ validateItemDequeued,
+ );
+
+ // Verify the file was NOT processed successfully
+ await expectToPassEventually(async () => {
+ const expectedKey = `${ENV}/${messageReference}.pdf`;
+ const expectedUri = `s3://${UNSCANNED_FILES_BUCKET}/${expectedKey}`;
+ await expect(getS3ObjectBufferFromUri(expectedUri)).rejects.toThrow();
+ }, 150);
+ // Verify there is a message in the DLQ
+ await expectMessageContainingString(FILE_SCANNER_DLQ_NAME, eventId, 150);
+ });
});
diff --git a/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts b/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts
index c65a1ed37..259316ad8 100644
--- a/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts
+++ b/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts
@@ -15,7 +15,7 @@ import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers';
import eventPublisher from 'helpers/event-bus-helpers';
import expectToPassEventually from 'helpers/expectations';
import { downloadFromS3 } from 'helpers/s3-helpers';
-import { expectMessageContainingString } from 'helpers/sqs-helpers';
+import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers';
import { v4 as uuidv4 } from 'uuid';
test.describe('Digital Letters - Mesh Acknowledger', () => {
@@ -55,6 +55,11 @@ test.describe('Digital Letters - Mesh Acknowledger', () => {
},
};
+ test.beforeAll(async () => {
+ await purgeQueue(MESH_ACKNOWLEDGE_DLQ_NAME);
+ test.setTimeout(250_000);
+ });
+
test('should send MESH acknowledgement and publish message acknowledged event following message downloaded event', async () => {
const letterId = uuidv4();
const messageReference = uuidv4();
diff --git a/tests/playwright/digital-letters-component-tests/nhsapp-status-handler.component.spec.ts b/tests/playwright/digital-letters-component-tests/nhsapp-status-handler.component.spec.ts
deleted file mode 100644
index eb9f65a60..000000000
--- a/tests/playwright/digital-letters-component-tests/nhsapp-status-handler.component.spec.ts
+++ /dev/null
@@ -1,265 +0,0 @@
-import { expect, test } from '@playwright/test';
-import {
- ENV,
- NHSAPP_STATUS_HANDLER_DLQ_NAME,
- NHSAPP_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
-} from 'constants/backend-constants';
-import { SENDER_ID_VALID_FOR_NOTIFY_SANDBOX } from 'constants/tests-constants';
-import { MESHInboxMessageDownloaded } from 'digital-letters-events';
-import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers';
-import { getTtl, putTtl } from 'helpers/dynamodb-helpers';
-import eventPublisher from 'helpers/event-bus-helpers';
-import expectToPassEventually from 'helpers/expectations';
-import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers';
-import { v4 as uuidv4 } from 'uuid';
-
-test.describe('Digital Letters - NHSApp Status Handler', () => {
- test.beforeAll(async () => {
- await purgeQueue(NHSAPP_STATUS_HANDLER_DLQ_NAME);
- });
-
- const baseEvent: MESHInboxMessageDownloaded = {
- id: 'id',
- specversion: '1.0',
- source: '/nhs/england/notify/production/primary/digitalletters/mesh',
- subject:
- 'customer/920fca11-596a-4eca-9c47-99f624614658/recipient/769acdd4-6a47-496f-999f-76a6fd2c3959',
- type: 'uk.nhs.notify.digital.letters.mesh.inbox.message.downloaded.v1',
- time: '2023-06-20T12:00:00Z',
- plane: 'data',
- recordedtime: '2023-06-20T12:00:00.250Z',
- severitynumber: 2,
- traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
- datacontenttype: 'application/json',
- dataschema:
- 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-mesh-inbox-message-downloaded-data.schema.json',
- dataschemaversion: '1.0.0',
- severitytext: 'INFO',
- data: {
- meshMessageId: '12345',
- messageUri: 'uri',
- messageReference: 'ref1',
- senderId: SENDER_ID_VALID_FOR_NOTIFY_SANDBOX,
- },
- };
-
- test('should mark TTL withdrawn and publish digital.letter.read event', async () => {
- const event = {
- ...baseEvent,
- data: {
- ...baseEvent.data,
- messageReference: uuidv4(),
- },
- };
-
- const concatedReference = `${event.data.senderId}_${event.data.messageReference}`;
-
- const ttlItem = {
- PK: concatedReference,
- SK: 'TTL',
- dateOfExpiry: '2023-12-31#0',
- event,
- ttl: Date.now() / 1000 + 3600,
- };
-
- const putResponseCode = await putTtl(ttlItem);
- expect(putResponseCode).toBe(200);
-
- await eventPublisher.sendEvents(
- [
- {
- source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
- type: 'uk.nhs.notify.channel.status.PUBLISHED.v1',
- data: {
- messageReference: concatedReference,
- supplierStatus: 'paper_letter_opted_out',
- },
- },
- ],
- () => true,
- );
-
- await expectToPassEventually(async () => {
- const ttl = await getTtl(
- event.data.senderId,
- event.data.messageReference,
- );
-
- expect(ttl.length).toBe(1);
- expect(ttl[0]).toHaveProperty('withdrawn', true);
- });
-
- await Promise.all([
- expectToPassEventually(async () => {
- const eventLogEntry = await getLogsFromCloudwatch(
- `/aws/vendedlogs/events/event-bus/nhs-${ENV}-dl`,
- [
- '$.message_type = "EVENT_RECEIPT"',
- '$.details.detail_type = "uk.nhs.notify.digital.letters.queue.digital.letter.read.v1"',
- `$.details.event_detail = "*\\"messageReference\\":\\"${event.data.messageReference}\\"*"`,
- ],
- );
-
- expect(eventLogEntry.length).toEqual(1);
- }),
-
- expectToPassEventually(async () => {
- const eventLogEntry = await getLogsFromCloudwatch(
- NHSAPP_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
- [
- '$.message.description = "TTL record marked as withdrawn"',
- `$.message.messageReference = "${concatedReference}"`,
- ],
- );
-
- expect(eventLogEntry.length).toEqual(1);
- }, 150),
- ]);
- });
-
- test('should handle duplicate event for the same TTL record', async () => {
- const event = {
- ...baseEvent,
- data: {
- ...baseEvent.data,
- messageReference: uuidv4(),
- },
- };
-
- const concatedReference = `${event.data.senderId}_${event.data.messageReference}`;
-
- const ttlItem = {
- PK: concatedReference,
- SK: 'TTL',
- dateOfExpiry: '2023-12-31#0',
- event,
- ttl: Date.now() / 1000 + 3600,
- };
-
- const channelStatusPublishedEvent = {
- source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
- type: 'uk.nhs.notify.channel.status.PUBLISHED.v1',
- data: {
- messageReference: concatedReference,
- supplierStatus: 'paper_letter_opted_out',
- },
- };
-
- const putResponseCode = await putTtl(ttlItem);
- expect(putResponseCode).toBe(200);
-
- await eventPublisher.sendEvents(
- [channelStatusPublishedEvent, channelStatusPublishedEvent],
- () => true,
- );
-
- await expectToPassEventually(async () => {
- const ttl = await getTtl(
- event.data.senderId,
- event.data.messageReference,
- );
-
- expect(ttl.length).toBe(1);
- expect(ttl[0]).toHaveProperty('withdrawn', true);
- });
-
- await Promise.all([
- expectToPassEventually(async () => {
- const eventLogEntry = await getLogsFromCloudwatch(
- `/aws/vendedlogs/events/event-bus/nhs-${ENV}-dl`,
- [
- '$.message_type = "EVENT_RECEIPT"',
- '$.details.detail_type = "uk.nhs.notify.digital.letters.queue.digital.letter.read.v1"',
- `$.details.event_detail = "*\\"messageReference\\":\\"${event.data.messageReference}\\"*"`,
- ],
- );
-
- expect(eventLogEntry.length).toEqual(2);
- }),
-
- expectToPassEventually(async () => {
- const eventLogEntry = await getLogsFromCloudwatch(
- NHSAPP_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
- [
- '$.message.description = "TTL record marked as withdrawn"',
- `$.message.messageReference = "${concatedReference}"`,
- ],
- );
-
- expect(eventLogEntry.length).toEqual(2);
- }, 150),
- ]);
- });
-
- test('should handle missing TTL record', async () => {
- const concatedReference = `${uuidv4()}_${uuidv4()}`;
-
- await eventPublisher.sendEvents(
- [
- {
- source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
- type: 'uk.nhs.notify.channel.status.PUBLISHED.v1',
- data: {
- messageReference: concatedReference,
- supplierStatus: 'paper_letter_opted_out',
- },
- },
- ],
- () => true,
- );
-
- await expectToPassEventually(async () => {
- const eventLogEntry = await getLogsFromCloudwatch(
- NHSAPP_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
- [
- '$.message.description = "TTL record not found"',
- `$.message.messageReference = "${concatedReference}"`,
- ],
- );
-
- expect(eventLogEntry.length).toEqual(1);
- }, 150);
- });
-
- test('should send invalid event to nhsapp status handler dlq', async () => {
- test.setTimeout(160_000);
-
- const concatedReference = `${uuidv4()}_${uuidv4()}`;
-
- await eventPublisher.sendEvents(
- [
- {
- source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging',
- type: 'uk.nhs.notify.channel.status.PUBLISHED.v1',
- data: {
- messageReference: concatedReference,
- supplierStatus: 'I am not valid',
- },
- },
- ],
- () => true,
- );
-
- await Promise.all([
- expectToPassEventually(async () => {
- const eventLogEntry = await getLogsFromCloudwatch(
- NHSAPP_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME,
- [
- '$.message.description = "Error parsing sqs record"',
- `$.message.messageReference = "${concatedReference}"`,
- String.raw`$.message.err.message = "*\"invalid_value\"*"`,
- String.raw`$.message.err.message = "*\"supplierStatus\"*"`,
- ],
- );
-
- expect(eventLogEntry.length).toEqual(1);
- }, 150),
-
- expectMessageContainingString(
- NHSAPP_STATUS_HANDLER_DLQ_NAME,
- concatedReference,
- 150,
- ),
- ]);
- });
-});
diff --git a/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts
index fc02e0995..23b5634cd 100644
--- a/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts
+++ b/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts
@@ -8,10 +8,15 @@ import { PDFAnalysed, validatePDFAnalysed } from 'digital-letters-events';
import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers';
import eventPublisher from 'helpers/event-bus-helpers';
import expectToPassEventually from 'helpers/expectations';
-import { expectMessageContainingString } from 'helpers/sqs-helpers';
+import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers';
import { v4 as uuidv4 } from 'uuid';
test.describe('Digital Letters - Print Sender', () => {
+ test.beforeAll(async () => {
+ await purgeQueue(PRINT_SENDER_DLQ_NAME);
+ test.setTimeout(250_000);
+ });
+
test('should send Letter Prepared event from PDF Analysed event', async () => {
test.setTimeout(120_000);
diff --git a/tests/playwright/helpers/event-builders.ts b/tests/playwright/helpers/event-builders.ts
index 9632d0e82..ae85c2692 100644
--- a/tests/playwright/helpers/event-builders.ts
+++ b/tests/playwright/helpers/event-builders.ts
@@ -32,6 +32,7 @@ export function buildDigitalLetterReadEvent(
time: string,
messageReference: string,
senderId: string,
+ supplierStatus = 'paper_letter_opted_out',
): DigitalLetterRead {
const baseEvent = buildBaseEvent('queue', time);
return {
@@ -43,6 +44,7 @@ export function buildDigitalLetterReadEvent(
data: {
messageReference,
senderId,
+ supplierStatus,
},
} as DigitalLetterRead;
}
diff --git a/turbo.json b/turbo.json
index 75822f803..eab76cc64 100644
--- a/turbo.json
+++ b/turbo.json
@@ -4,6 +4,11 @@
"generate-dependencies": {
"dependsOn": [
"^generate-dependencies"
+ ],
+ "inputs": [
+ "src/**",
+ "../../schemas/**",
+ "../../src/cloudevents/**"
]
},
"lint": {
diff --git a/utils/utils/src/types/channel-status-published-event.ts b/utils/utils/src/types/channel-status-published-event.ts
deleted file mode 100644
index c34873551..000000000
--- a/utils/utils/src/types/channel-status-published-event.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { z } from 'zod';
-
-export const $ChannelStatusPublishedEvent = z.object({
- data: z.object({
- messageReference: z.string(),
- supplierStatus: z.literal('paper_letter_opted_out'),
- }),
-});
-
-export type ChannelStatusPublishedEvent = z.infer<
- typeof $ChannelStatusPublishedEvent
->;
diff --git a/utils/utils/src/types/core-status-published-event.ts b/utils/utils/src/types/core-status-published-event.ts
new file mode 100644
index 000000000..2418bf8ea
--- /dev/null
+++ b/utils/utils/src/types/core-status-published-event.ts
@@ -0,0 +1,46 @@
+import { z } from 'zod';
+
+export const $ChannelStatusPublishedEvent = z.object({
+ source: z.string(),
+ type: z.literal('uk.nhs.notify.channel.status.PUBLISHED.v1'),
+ data: z.union([
+ z.object({
+ channelStatus: z.literal('failed'),
+ messageReference: z.string(),
+ supplierStatus: z.literal('rejected'),
+ }),
+ z.object({
+ channelStatus: z.string(),
+ messageReference: z.string(),
+ supplierStatus: z.enum([
+ 'paper_letter_opted_in',
+ 'paper_letter_opted_out',
+ ]),
+ }),
+ ]),
+});
+
+export type ChannelStatusPublishedEvent = z.infer<
+ typeof $ChannelStatusPublishedEvent
+>;
+
+export const $MessageStatusPublishedEvent = z.object({
+ source: z.string(),
+ type: z.literal('uk.nhs.notify.message.status.PUBLISHED.v1'),
+ data: z.object({
+ messageReference: z.string(),
+ messageStatus: z.string('failed'),
+ resolvedChannels: z.array(z.unknown()).optional(),
+ }),
+});
+
+export type MessageStatusPublishedEvent = z.infer<
+ typeof $MessageStatusPublishedEvent
+>;
+
+export const $StatusPublishedEvent = z.discriminatedUnion('type', [
+ $ChannelStatusPublishedEvent,
+ $MessageStatusPublishedEvent,
+]);
+
+export type StatusPublishedEvent = z.infer;
diff --git a/utils/utils/src/types/index.ts b/utils/utils/src/types/index.ts
index c5272f269..1c270e0b5 100644
--- a/utils/utils/src/types/index.ts
+++ b/utils/utils/src/types/index.ts
@@ -1,4 +1,4 @@
-export * from './channel-status-published-event';
+export * from './core-status-published-event';
export * from './pdm-types';
export * from './sender';
export * from './supplier-api-letter-event';