From 8b32f7581117c2a14de56e6eb0eea03cbbc449f1 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Thu, 14 May 2026 14:49:38 +0100 Subject: [PATCH 1/2] CCM-17640: rename nhsapp-status-handler to core-status-handler --- .../c4/notifhir/viewer/callback/index.md | 2 +- ...ndler.md => c4code-core-status-handler.md} | 4 +- .../terraform/components/dl/README.md | 4 +- ...tch_event_rule_channel_status_published.tf | 4 +- ...vent_source_mapping_core_status_handler.tf | 10 + ...nt_source_mapping_nhsapp_status_handler.tf | 10 - ...f => module_lambda_core_status_handler.tf} | 16 +- ...r.tf => module_sqs_core_status_handler.tf} | 10 +- .../jest.config.ts | 0 .../package.json | 2 +- .../__tests__/apis/sqs-trigger-lambda.test.ts | 8 +- .../src/__tests__/app/ttl-actions.test.ts | 18 +- .../src/__tests__/container.test.ts | 0 .../src/__tests__/data.ts | 2 +- .../src/__tests__/index.test.ts | 0 .../src/__tests__/infra/config.test.ts | 0 .../__tests__/infra/ttl-repository.test.ts | 10 +- .../src/apis/sqs-trigger-lambda.ts | 0 .../src/app/ttl-actions.ts | 0 .../src/container.ts | 0 .../src/index.ts | 0 .../src/infra/config.ts | 0 .../src/infra/ttl-repository.ts | 0 .../src/types/types.ts | 0 .../tsconfig.json | 0 package-lock.json | 300 +++++++++--------- package.json | 2 +- ...nel-status-published.consumer.pact.test.ts | 2 +- .../playwright/constants/backend-constants.ts | 4 +- ... => core-status-handler.component.spec.ts} | 20 +- 30 files changed, 214 insertions(+), 214 deletions(-) rename docs/collections/_diagrams/{c4code-nhsapp-status-handler.md => c4code-core-status-handler.md} (89%) create mode 100644 infrastructure/terraform/components/dl/lambda_event_source_mapping_core_status_handler.tf delete mode 100644 infrastructure/terraform/components/dl/lambda_event_source_mapping_nhsapp_status_handler.tf rename infrastructure/terraform/components/dl/{module_lambda_nhsapp_status_handler.tf => module_lambda_core_status_handler.tf} (84%) rename infrastructure/terraform/components/dl/{module_sqs_nhsapp_status_handler.tf => module_sqs_core_status_handler.tf} (83%) rename lambdas/{nhsapp-status-handler => core-status-handler}/jest.config.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/package.json (92%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/__tests__/apis/sqs-trigger-lambda.test.ts (97%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/__tests__/app/ttl-actions.test.ts (75%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/__tests__/container.test.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/__tests__/data.ts (95%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/__tests__/index.test.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/__tests__/infra/config.test.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/__tests__/infra/ttl-repository.test.ts (83%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/apis/sqs-trigger-lambda.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/app/ttl-actions.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/container.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/index.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/infra/config.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/infra/ttl-repository.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/src/types/types.ts (100%) rename lambdas/{nhsapp-status-handler => core-status-handler}/tsconfig.json (100%) rename tests/playwright/digital-letters-component-tests/{nhsapp-status-handler.component.spec.ts => core-status-handler.component.spec.ts} (93%) 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..691ebbef0 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" "sqs_core_status_handler_target" { 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/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 a4e1d6b26..0fbd4ec68 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" @@ -41,7 +41,7 @@ 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" @@ -70,7 +70,7 @@ data "aws_iam_policy_document" "nhsapp_status_handler" { } statement { - sid = "SQSPermissionsNhsappStatusHandlerQueue" + sid = "SQSPermissionsCoreStatusHandlerQueue" effect = "Allow" actions = [ @@ -81,7 +81,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 83% 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..e547801f8 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,7 +29,7 @@ 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 { 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 97% 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..bfb0b8222 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 { coreStatusEvent, messageDownloadedEvent } from '__tests__/data'; import { createHandler } from 'apis/sqs-trigger-lambda'; import type { SQSEvent } from 'aws-lambda'; import { @@ -24,7 +24,7 @@ describe('createHandler', () => { let handler: any; const eventBusEvent = { - detail: nhsAppStatusEvent, + detail: coreStatusEvent, }; const digitalLetterReadEvent: DigitalLetterRead = { @@ -61,7 +61,7 @@ describe('createHandler', () => { const res = await handler(event); expect(res.batchItemFailures).toEqual([]); - expect(ttlActions.markWithdrawn).toHaveBeenCalledWith(nhsAppStatusEvent); + expect(ttlActions.markWithdrawn).toHaveBeenCalledWith(coreStatusEvent); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [digitalLetterReadEvent], validateDigitalLetterRead, @@ -139,7 +139,7 @@ describe('createHandler', () => { const res = await handler(event); - expect(ttlActions.markWithdrawn).toHaveBeenCalledWith(nhsAppStatusEvent); + expect(ttlActions.markWithdrawn).toHaveBeenCalledWith(coreStatusEvent); expect(eventPublisher.sendEvents).not.toHaveBeenCalled(); expect(res.batchItemFailures).toEqual([{ itemIdentifier: 'msg1' }]); expect(logger.info).toHaveBeenCalledWith({ diff --git a/lambdas/nhsapp-status-handler/src/__tests__/app/ttl-actions.test.ts b/lambdas/core-status-handler/src/__tests__/app/ttl-actions.test.ts similarity index 75% rename from lambdas/nhsapp-status-handler/src/__tests__/app/ttl-actions.test.ts rename to lambdas/core-status-handler/src/__tests__/app/ttl-actions.test.ts index b39e5dc7b..2b8dfb0cc 100644 --- a/lambdas/nhsapp-status-handler/src/__tests__/app/ttl-actions.test.ts +++ b/lambdas/core-status-handler/src/__tests__/app/ttl-actions.test.ts @@ -1,4 +1,4 @@ -import { messageDownloadedEvent, nhsAppStatusEvent } from '__tests__/data'; +import { coreStatusEvent, messageDownloadedEvent } from '__tests__/data'; import { TtlActions } from 'app/ttl-actions'; import { TtlRepository } from 'infra/ttl-repository'; @@ -16,7 +16,7 @@ describe('TtlActions', () => { it('returns success when markWithdrawn succeeds', async () => { repo.markWithdrawn.mockResolvedValue({ event: messageDownloadedEvent }); - const result = await ttlActions.markWithdrawn(nhsAppStatusEvent); + const result = await ttlActions.markWithdrawn(coreStatusEvent); expect(result).toEqual({ result: 'success', @@ -25,11 +25,11 @@ describe('TtlActions', () => { expect(logger.info).toHaveBeenCalledWith( expect.objectContaining({ description: expect.stringContaining('TTL record marked as withdrawn'), - messageReference: nhsAppStatusEvent.data.messageReference, + messageReference: coreStatusEvent.data.messageReference, }), ); expect(repo.markWithdrawn).toHaveBeenCalledWith( - nhsAppStatusEvent.data.messageReference, + coreStatusEvent.data.messageReference, ); }); @@ -37,18 +37,18 @@ describe('TtlActions', () => { // eslint-disable-next-line unicorn/no-useless-undefined repo.markWithdrawn.mockResolvedValue(undefined); - const result = await ttlActions.markWithdrawn(nhsAppStatusEvent); + const result = await ttlActions.markWithdrawn(coreStatusEvent); expect(result).toEqual({ result: 'success' }); expect(logger.info).toHaveBeenCalledWith( expect.objectContaining({ description: expect.stringContaining('TTL record not found'), - messageReference: nhsAppStatusEvent.data.messageReference, + messageReference: coreStatusEvent.data.messageReference, }), ); expect(repo.markWithdrawn).toHaveBeenCalledWith( - nhsAppStatusEvent.data.messageReference, + coreStatusEvent.data.messageReference, ); }); @@ -56,13 +56,13 @@ describe('TtlActions', () => { const error = new Error('fail'); repo.markWithdrawn.mockRejectedValue(error); - const result = await ttlActions.markWithdrawn(nhsAppStatusEvent); + const result = await ttlActions.markWithdrawn(coreStatusEvent); expect(result).toEqual({ result: 'failed' }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ description: expect.stringContaining('Error marking TTL withdrawn'), - messageReference: nhsAppStatusEvent.data.messageReference, + messageReference: coreStatusEvent.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 95% rename from lambdas/nhsapp-status-handler/src/__tests__/data.ts rename to lambdas/core-status-handler/src/__tests__/data.ts index 6235d612b..674754b63 100644 --- a/lambdas/nhsapp-status-handler/src/__tests__/data.ts +++ b/lambdas/core-status-handler/src/__tests__/data.ts @@ -26,7 +26,7 @@ export const messageDownloadedEvent: MESHInboxMessageDownloaded = { }, }; -export const nhsAppStatusEvent: ChannelStatusPublishedEvent = { +export const coreStatusEvent: ChannelStatusPublishedEvent = { data: { messageReference: `${messageDownloadedEvent.data.senderId}_${messageDownloadedEvent.data.messageReference}`, supplierStatus: 'paper_letter_opted_out', 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/nhsapp-status-handler/src/__tests__/infra/ttl-repository.test.ts b/lambdas/core-status-handler/src/__tests__/infra/ttl-repository.test.ts similarity index 83% rename from lambdas/nhsapp-status-handler/src/__tests__/infra/ttl-repository.test.ts rename to lambdas/core-status-handler/src/__tests__/infra/ttl-repository.test.ts index 6fdf14a9a..f9a3f82b2 100644 --- a/lambdas/nhsapp-status-handler/src/__tests__/infra/ttl-repository.test.ts +++ b/lambdas/core-status-handler/src/__tests__/infra/ttl-repository.test.ts @@ -1,6 +1,6 @@ import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb'; import { UpdateCommand } from '@aws-sdk/lib-dynamodb'; -import { nhsAppStatusEvent } from '__tests__/data'; +import { coreStatusEvent } from '__tests__/data'; import { TtlRepository } from 'infra/ttl-repository'; describe('TtlRepository', () => { @@ -14,14 +14,14 @@ describe('TtlRepository', () => { }); it('marks item as withdrawn', async () => { - await repo.markWithdrawn(nhsAppStatusEvent.data.messageReference); + await repo.markWithdrawn(coreStatusEvent.data.messageReference); const updateCommand: UpdateCommand = dynamoDocumentClient.send.mock.calls[0][0]; expect(updateCommand.input).toStrictEqual({ TableName: tableName, Key: { - PK: nhsAppStatusEvent.data.messageReference, + PK: coreStatusEvent.data.messageReference, SK: 'TTL', }, ConditionExpression: 'attribute_exists(PK)', @@ -41,7 +41,7 @@ describe('TtlRepository', () => { dynamoDocumentClient.send.mockRejectedValue(error); const result = await repo.markWithdrawn( - nhsAppStatusEvent.data.messageReference, + coreStatusEvent.data.messageReference, ); expect(result).toBeUndefined(); @@ -52,7 +52,7 @@ describe('TtlRepository', () => { dynamoDocumentClient.send.mockRejectedValue(error); await expect( - repo.markWithdrawn(nhsAppStatusEvent.data.messageReference), + repo.markWithdrawn(coreStatusEvent.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 100% rename from lambdas/nhsapp-status-handler/src/apis/sqs-trigger-lambda.ts rename to lambdas/core-status-handler/src/apis/sqs-trigger-lambda.ts diff --git a/lambdas/nhsapp-status-handler/src/app/ttl-actions.ts b/lambdas/core-status-handler/src/app/ttl-actions.ts similarity index 100% rename from lambdas/nhsapp-status-handler/src/app/ttl-actions.ts rename to lambdas/core-status-handler/src/app/ttl-actions.ts diff --git a/lambdas/nhsapp-status-handler/src/container.ts b/lambdas/core-status-handler/src/container.ts similarity index 100% rename from lambdas/nhsapp-status-handler/src/container.ts rename to lambdas/core-status-handler/src/container.ts 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 100% rename from lambdas/nhsapp-status-handler/src/infra/ttl-repository.ts rename to lambdas/core-status-handler/src/infra/ttl-repository.ts diff --git a/lambdas/nhsapp-status-handler/src/types/types.ts b/lambdas/core-status-handler/src/types/types.ts similarity index 100% rename from lambdas/nhsapp-status-handler/src/types/types.ts rename to lambdas/core-status-handler/src/types/types.ts 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/package-lock.json b/package-lock.json index 2fa724e5f..991e9e3a7 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" }, @@ -21840,6 +21840,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 @@ -21848,10 +21852,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/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/nhsapp-status-handler.component.spec.ts b/tests/playwright/digital-letters-component-tests/core-status-handler.component.spec.ts similarity index 93% rename from tests/playwright/digital-letters-component-tests/nhsapp-status-handler.component.spec.ts rename to tests/playwright/digital-letters-component-tests/core-status-handler.component.spec.ts index eb9f65a60..d0c7a949a 100644 --- a/tests/playwright/digital-letters-component-tests/nhsapp-status-handler.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/core-status-handler.component.spec.ts @@ -1,8 +1,8 @@ import { expect, test } from '@playwright/test'; import { + CORE_STATUS_HANDLER_DLQ_NAME, + CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, 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'; @@ -13,9 +13,9 @@ 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.describe('Digital Letters - Core Status Handler', () => { test.beforeAll(async () => { - await purgeQueue(NHSAPP_STATUS_HANDLER_DLQ_NAME); + await purgeQueue(CORE_STATUS_HANDLER_DLQ_NAME); }); const baseEvent: MESHInboxMessageDownloaded = { @@ -105,7 +105,7 @@ test.describe('Digital Letters - NHSApp Status Handler', () => { expectToPassEventually(async () => { const eventLogEntry = await getLogsFromCloudwatch( - NHSAPP_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, + CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, [ '$.message.description = "TTL record marked as withdrawn"', `$.message.messageReference = "${concatedReference}"`, @@ -179,7 +179,7 @@ test.describe('Digital Letters - NHSApp Status Handler', () => { expectToPassEventually(async () => { const eventLogEntry = await getLogsFromCloudwatch( - NHSAPP_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, + CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, [ '$.message.description = "TTL record marked as withdrawn"', `$.message.messageReference = "${concatedReference}"`, @@ -210,7 +210,7 @@ test.describe('Digital Letters - NHSApp Status Handler', () => { await expectToPassEventually(async () => { const eventLogEntry = await getLogsFromCloudwatch( - NHSAPP_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, + CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, [ '$.message.description = "TTL record not found"', `$.message.messageReference = "${concatedReference}"`, @@ -221,7 +221,7 @@ test.describe('Digital Letters - NHSApp Status Handler', () => { }, 150); }); - test('should send invalid event to nhsapp status handler dlq', async () => { + test('should send invalid event to core status handler dlq', async () => { test.setTimeout(160_000); const concatedReference = `${uuidv4()}_${uuidv4()}`; @@ -243,7 +243,7 @@ test.describe('Digital Letters - NHSApp Status Handler', () => { await Promise.all([ expectToPassEventually(async () => { const eventLogEntry = await getLogsFromCloudwatch( - NHSAPP_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, + CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, [ '$.message.description = "Error parsing sqs record"', `$.message.messageReference = "${concatedReference}"`, @@ -256,7 +256,7 @@ test.describe('Digital Letters - NHSApp Status Handler', () => { }, 150), expectMessageContainingString( - NHSAPP_STATUS_HANDLER_DLQ_NAME, + CORE_STATUS_HANDLER_DLQ_NAME, concatedReference, 150, ), From 8a61afa190e0a12fde6e02bdb64c0c74d5cecce7 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Fri, 22 May 2026 09:07:18 +0100 Subject: [PATCH 2/2] CCM-17640: add handling of extra event statuses --- ...tch_event_rule_channel_status_published.tf | 2 +- ...tch_event_rule_message_status_published.tf | 19 + .../dl/module_lambda_core_status_handler.tf | 1 + .../dl/module_sqs_core_status_handler.tf | 5 +- .../__tests__/apis/sqs-trigger-lambda.test.ts | 82 ++- .../app/status-action-resolver.test.ts | 214 ++++++ .../src/__tests__/app/ttl-actions.test.ts | 163 +++-- .../core-status-handler/src/__tests__/data.ts | 20 +- .../__tests__/infra/ttl-repository.test.ts | 98 ++- .../src/apis/sqs-trigger-lambda.ts | 60 +- .../src/app/status-action-resolver.ts | 68 ++ .../src/app/ttl-actions.ts | 45 +- lambdas/core-status-handler/src/container.ts | 5 +- .../src/infra/ttl-repository.ts | 31 +- .../core-status-handler/src/types/types.ts | 2 + ...queue-digital-letter-read-data.schema.yaml | 3 + .../2025-10-draft/defs/core.schema.yaml | 5 + .../core-status-handler.component.spec.ts | 657 ++++++++++++++---- .../file-scanner.component.spec.ts | 254 +++---- .../mesh-acknowledge.component.spec.ts | 7 +- .../print-sender.component.spec.ts | 7 +- tests/playwright/helpers/event-builders.ts | 2 + turbo.json | 5 + .../types/channel-status-published-event.ts | 12 - .../src/types/core-status-published-event.ts | 46 ++ utils/utils/src/types/index.ts | 2 +- 26 files changed, 1408 insertions(+), 407 deletions(-) create mode 100644 infrastructure/terraform/components/dl/cloudwatch_event_rule_message_status_published.tf create mode 100644 lambdas/core-status-handler/src/__tests__/app/status-action-resolver.test.ts create mode 100644 lambdas/core-status-handler/src/app/status-action-resolver.ts delete mode 100644 utils/utils/src/types/channel-status-published-event.ts create mode 100644 utils/utils/src/types/core-status-published-event.ts 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 691ebbef0..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,7 +12,7 @@ resource "aws_cloudwatch_event_rule" "channel_status_published" { }) } -resource "aws_cloudwatch_event_target" "sqs_core_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_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/module_lambda_core_status_handler.tf b/infrastructure/terraform/components/dl/module_lambda_core_status_handler.tf index bb6253514..428a4e276 100644 --- a/infrastructure/terraform/components/dl/module_lambda_core_status_handler.tf +++ b/infrastructure/terraform/components/dl/module_lambda_core_status_handler.tf @@ -50,6 +50,7 @@ data "aws_iam_policy_document" "core_status_handler" { actions = [ "dynamodb:UpdateItem", + "dynamodb:DeleteItem", ] resources = [ diff --git a/infrastructure/terraform/components/dl/module_sqs_core_status_handler.tf b/infrastructure/terraform/components/dl/module_sqs_core_status_handler.tf index e547801f8..c11a46640 100644 --- a/infrastructure/terraform/components/dl/module_sqs_core_status_handler.tf +++ b/infrastructure/terraform/components/dl/module_sqs_core_status_handler.tf @@ -35,7 +35,10 @@ data "aws_iam_policy_document" "sqs_core_status_handler" { 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/core-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/core-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts index bfb0b8222..73a1f08bc 100644 --- a/lambdas/core-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 { coreStatusEvent, messageDownloadedEvent } 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: coreStatusEvent, + 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(coreStatusEvent); + 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(coreStatusEvent); + 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 index 2b8dfb0cc..ec37a0901 100644 --- a/lambdas/core-status-handler/src/__tests__/app/ttl-actions.test.ts +++ b/lambdas/core-status-handler/src/__tests__/app/ttl-actions.test.ts @@ -1,4 +1,4 @@ -import { coreStatusEvent, messageDownloadedEvent } from '__tests__/data'; +import { channelStatusEvent, messageDownloadedEvent } from '__tests__/data'; import { TtlActions } from 'app/ttl-actions'; import { TtlRepository } from 'infra/ttl-repository'; @@ -7,64 +7,131 @@ describe('TtlActions', () => { 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); - }); + 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 }); + it('returns success when markWithdrawn succeeds', async () => { + repo.markWithdrawn.mockResolvedValue({ event: messageDownloadedEvent }); - const result = await ttlActions.markWithdrawn(coreStatusEvent); + const result = await ttlActions.markWithdrawn(channelStatusEvent); - expect(result).toEqual({ - result: 'success', - ttlItem: { event: messageDownloadedEvent }, + 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, + ); }); - expect(logger.info).toHaveBeenCalledWith( - expect.objectContaining({ - description: expect.stringContaining('TTL record marked as withdrawn'), - messageReference: coreStatusEvent.data.messageReference, - }), - ); - expect(repo.markWithdrawn).toHaveBeenCalledWith( - coreStatusEvent.data.messageReference, - ); - }); - it('returns success when TTL record not found', async () => { - // eslint-disable-next-line unicorn/no-useless-undefined - repo.markWithdrawn.mockResolvedValue(undefined); + 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' }); - const result = await ttlActions.markWithdrawn(coreStatusEvent); + 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); - expect(result).toEqual({ result: 'success' }); + const result = await ttlActions.markWithdrawn(channelStatusEvent); - expect(logger.info).toHaveBeenCalledWith( - expect.objectContaining({ - description: expect.stringContaining('TTL record not found'), - messageReference: coreStatusEvent.data.messageReference, - }), - ); - expect(repo.markWithdrawn).toHaveBeenCalledWith( - coreStatusEvent.data.messageReference, - ); + expect(result).toEqual({ result: 'failed' }); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + description: expect.stringContaining('Error marking TTL withdrawn'), + messageReference: channelStatusEvent.data.messageReference, + err: error, + }), + ); + }); }); - it('returns failed and logs error when markWithdrawn throws', async () => { - const error = new Error('fail'); - repo.markWithdrawn.mockRejectedValue(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.markWithdrawn(coreStatusEvent); + const result = await ttlActions.delete(channelStatusEvent); - expect(result).toEqual({ result: 'failed' }); - expect(logger.warn).toHaveBeenCalledWith( - expect.objectContaining({ - description: expect.stringContaining('Error marking TTL withdrawn'), - messageReference: coreStatusEvent.data.messageReference, - err: error, - }), - ); + 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/core-status-handler/src/__tests__/data.ts b/lambdas/core-status-handler/src/__tests__/data.ts index 674754b63..30f4d8f28 100644 --- a/lambdas/core-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 coreStatusEvent: 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/core-status-handler/src/__tests__/infra/ttl-repository.test.ts b/lambdas/core-status-handler/src/__tests__/infra/ttl-repository.test.ts index f9a3f82b2..ad87f367a 100644 --- a/lambdas/core-status-handler/src/__tests__/infra/ttl-repository.test.ts +++ b/lambdas/core-status-handler/src/__tests__/infra/ttl-repository.test.ts @@ -1,6 +1,6 @@ import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb'; -import { UpdateCommand } from '@aws-sdk/lib-dynamodb'; -import { coreStatusEvent } from '__tests__/data'; +import { DeleteCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb'; +import { channelStatusEvent } from '__tests__/data'; import { TtlRepository } from 'infra/ttl-repository'; describe('TtlRepository', () => { @@ -13,46 +13,74 @@ describe('TtlRepository', () => { repo = new TtlRepository(tableName, dynamoDocumentClient); }); - it('marks item as withdrawn', async () => { - await repo.markWithdrawn(coreStatusEvent.data.messageReference); - - const updateCommand: UpdateCommand = - dynamoDocumentClient.send.mock.calls[0][0]; - expect(updateCommand.input).toStrictEqual({ - TableName: tableName, - Key: { - PK: coreStatusEvent.data.messageReference, - SK: 'TTL', - }, - ConditionExpression: 'attribute_exists(PK)', - UpdateExpression: 'set withdrawn = :val1', - ExpressionAttributeValues: { - ':val1': true, - }, - ReturnValues: 'ALL_NEW', + 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: {}, + 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(); }); - dynamoDocumentClient.send.mockRejectedValue(error); - const result = await repo.markWithdrawn( - coreStatusEvent.data.messageReference, - ); + it('errors on dynamo error', async () => { + const error = new Error('fail'); + dynamoDocumentClient.send.mockRejectedValue(error); - expect(result).toBeUndefined(); + await expect( + repo.markWithdrawn(channelStatusEvent.data.messageReference), + ).rejects.toThrow(error); + }); }); - it('errors on dynamo error', async () => { - const error = new Error('fail'); - dynamoDocumentClient.send.mockRejectedValue(error); + describe('delete', () => { + it('deletes item', async () => { + await repo.delete(channelStatusEvent.data.messageReference); - await expect( - repo.markWithdrawn(coreStatusEvent.data.messageReference), - ).rejects.toThrow(error); + 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/core-status-handler/src/apis/sqs-trigger-lambda.ts b/lambdas/core-status-handler/src/apis/sqs-trigger-lambda.ts index 93b4d3a74..d9a3c9b70 100644 --- a/lambdas/core-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 index ae6b0f22f..a0c628964 100644 --- a/lambdas/core-status-handler/src/app/ttl-actions.ts +++ b/lambdas/core-status-handler/src/app/ttl-actions.ts @@ -1,12 +1,11 @@ -import { ChannelStatusPublishedEvent, Logger } from 'utils'; +import { Logger, StatusPublishedEvent } from 'utils'; import { TtlRepository } from 'infra/ttl-repository'; -import { TtlRecord } from 'types/types'; - -export type TtlItem = TtlRecord | undefined; +import { TtlItem } from 'types/types'; export type TtlActionOutcome = | { result: 'success'; ttlItem: TtlItem } - | { result: 'failed' }; + | { result: 'failed' } + | { result: 'skipped' }; export class TtlActions { constructor( @@ -14,9 +13,7 @@ export class TtlActions { private readonly logger: Logger, ) {} - async markWithdrawn( - item: ChannelStatusPublishedEvent, - ): Promise { + async markWithdrawn(item: StatusPublishedEvent): Promise { const { messageReference } = item.data; let ttlItem: TtlItem; @@ -47,4 +44,36 @@ export class TtlActions { 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/core-status-handler/src/container.ts b/lambdas/core-status-handler/src/container.ts index 4f829ccf1..c406ee35d 100644 --- a/lambdas/core-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/core-status-handler/src/infra/ttl-repository.ts b/lambdas/core-status-handler/src/infra/ttl-repository.ts index f37753996..06bac389d 100644 --- a/lambdas/core-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/core-status-handler/src/types/types.ts b/lambdas/core-status-handler/src/types/types.ts index 7e4bab326..24872109a 100644 --- a/lambdas/core-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/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/playwright/digital-letters-component-tests/core-status-handler.component.spec.ts b/tests/playwright/digital-letters-component-tests/core-status-handler.component.spec.ts index d0c7a949a..93160c91d 100644 --- 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 @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { CORE_STATUS_HANDLER_DLQ_NAME, CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, - ENV, + 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'; @@ -11,6 +11,10 @@ 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', () => { @@ -43,141 +47,519 @@ test.describe('Digital Letters - Core Status Handler', () => { }, }; - test('should mark TTL withdrawn and publish digital.letter.read event', async () => { - const event = { - ...baseEvent, + 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: { - ...baseEvent.data, - messageReference: uuidv4(), + channelStatus: 'failed', + supplierStatus: 'rejected', + messageReference: '', }, }; - const concatedReference = `${event.data.senderId}_${event.data.messageReference}`; + test('when supplierStatus is rejected and channelStatus is failed - delete TTL', async () => { + const event = { + ...baseEvent, + data: { + ...baseEvent.data, + messageReference: uuidv4(), + }, + }; - const ttlItem = { - PK: concatedReference, - SK: 'TTL', - dateOfExpiry: '2023-12-31#0', - event, - ttl: Date.now() / 1000 + 3600, - }; + const concatedReference = `${event.data.senderId}_${event.data.messageReference}`; - const putResponseCode = await putTtl(ttlItem); - expect(putResponseCode).toBe(200); + const ttlItem = { + PK: concatedReference, + SK: 'TTL', + dateOfExpiry: '2023-12-31#0', + event, + ttl: Date.now() / 1000 + 3600, + }; - 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, - ); + const putResponseCode = await putTtl(ttlItem); + expect(putResponseCode).toBe(200); - await expectToPassEventually(async () => { - const ttl = await getTtl( - event.data.senderId, - event.data.messageReference, + await eventPublisher.sendEvents( + [ + { + ...channelFailedEvent, + data: { + ...channelFailedEvent.data, + messageReference: concatedReference, + }, + }, + ], + () => true, ); - expect(ttl.length).toBe(1); - expect(ttl[0]).toHaveProperty('withdrawn', true); - }); + await expectToPassEventually(async () => { + const ttl = await getTtl( + event.data.senderId, + event.data.messageReference, + ); - await Promise.all([ - expectToPassEventually(async () => { + expect(ttl.length).toBe(0); + }); + + await expectToPassEventually(async () => { const eventLogEntry = await getLogsFromCloudwatch( - `/aws/vendedlogs/events/event-bus/nhs-${ENV}-dl`, + CORE_STATUS_HANDLER_LAMBDA_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}\\"*"`, + '$.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, + 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( [ - '$.message.description = "TTL record marked as withdrawn"', - `$.message.messageReference = "${concatedReference}"`, + { + ...optedOutEvent, + data: { + ...optedOutEvent.data, + messageReference: concatedReference, + }, + }, ], + () => true, ); - expect(eventLogEntry.length).toEqual(1); - }, 150), - ]); - }); + 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(), + }, + }; - 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 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 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); - const channelStatusPublishedEvent = { + 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.channel.status.PUBLISHED.v1', + type: 'uk.nhs.notify.message.status.PUBLISHED.v1', data: { - messageReference: concatedReference, - supplierStatus: 'paper_letter_opted_out', + messageStatus: 'failed', + messageReference: '', + resolvedChannels: [], }, }; - const putResponseCode = await putTtl(ttlItem); - expect(putResponseCode).toBe(200); + test('when messageStatus is failed and resolvedChannels is empty - mark TTL withdrawn', async () => { + const event = { + ...baseEvent, + data: { + ...baseEvent.data, + messageReference: uuidv4(), + }, + }; - await eventPublisher.sendEvents( - [channelStatusPublishedEvent, channelStatusPublishedEvent], - () => true, - ); + const concatedReference = `${event.data.senderId}_${event.data.messageReference}`; - await expectToPassEventually(async () => { - const ttl = await getTtl( - event.data.senderId, - event.data.messageReference, - ); + const ttlItem = { + PK: concatedReference, + SK: 'TTL', + dateOfExpiry: '2023-12-31#0', + event, + ttl: Date.now() / 1000 + 3600, + }; - expect(ttl.length).toBe(1); - expect(ttl[0]).toHaveProperty('withdrawn', true); - }); + const putResponseCode = await putTtl(ttlItem); + expect(putResponseCode).toBe(200); - 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}\\"*"`, - ], + await eventPublisher.sendEvents( + [ + { + ...messageEvent, + data: { + ...messageEvent.data, + messageReference: concatedReference, + }, + }, + ], + () => true, + ); + + await expectToPassEventually(async () => { + const ttl = await getTtl( + event.data.senderId, + event.data.messageReference, ); - expect(eventLogEntry.length).toEqual(2); - }), + expect(ttl.length).toBe(1); + expect(ttl[0]).toHaveProperty('withdrawn', true); + }); - expectToPassEventually(async () => { + await expectToPassEventually(async () => { const eventLogEntry = await getLogsFromCloudwatch( CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, [ @@ -186,42 +568,61 @@ test.describe('Digital Letters - Core Status Handler', () => { ], ); - expect(eventLogEntry.length).toEqual(2); - }, 150), - ]); - }); - - test('should handle missing TTL record', async () => { - const concatedReference = `${uuidv4()}_${uuidv4()}`; + expect(eventLogEntry.length).toEqual(1); + }, 150); + }); - 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', - }, + test('when messageStatus is failed and resolvedChannels is NOT empty - skip', async () => { + const event = { + ...baseEvent, + data: { + ...baseEvent.data, + messageReference: uuidv4(), }, - ], - () => true, - ); + }; - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - CORE_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, + 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( [ - '$.message.description = "TTL record not found"', - `$.message.messageReference = "${concatedReference}"`, + { + ...messageEvent, + data: { + ...messageEvent.data, + messageReference: concatedReference, + resolvedChannels: ['LETTER'], + }, + }, ], + () => true, ); - expect(eventLogEntry.length).toEqual(1); - }, 150); + 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('should send invalid event to core status handler dlq', async () => { + test('when event is invalid - send to dlq', async () => { test.setTimeout(160_000); const concatedReference = `${uuidv4()}_${uuidv4()}`; @@ -230,10 +631,10 @@ test.describe('Digital Letters - Core Status Handler', () => { [ { source: '/nhs/england/notify/comms-mgr-dev/dev/data-plane/messaging', - type: 'uk.nhs.notify.channel.status.PUBLISHED.v1', + type: 'uk.nhs.notify.message.status.PUBLISHED.v1', data: { messageReference: concatedReference, - supplierStatus: 'I am not valid', + messageStatus: 'I am not valid', }, }, ], @@ -248,7 +649,7 @@ test.describe('Digital Letters - Core Status Handler', () => { '$.message.description = "Error parsing sqs record"', `$.message.messageReference = "${concatedReference}"`, String.raw`$.message.err.message = "*\"invalid_value\"*"`, - String.raw`$.message.err.message = "*\"supplierStatus\"*"`, + String.raw`$.message.err.message = "*\"messageStatus\"*"`, ], ); 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/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 a156bb92b..290a135f6 100644 --- a/tests/playwright/helpers/event-builders.ts +++ b/tests/playwright/helpers/event-builders.ts @@ -31,6 +31,7 @@ export function buildDigitalLetterReadEvent( time: string, messageReference: string, senderId: string, + supplierStatus = 'paper_letter_opted_out', ): DigitalLetterRead { const baseEvent = buildBaseEvent('queue', time); return { @@ -42,6 +43,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';