From 3c421d4cb4a957f69b804abc3318555db9646b0a Mon Sep 17 00:00:00 2001 From: Usmaan Ali Date: Thu, 9 Apr 2026 13:08:33 +0100 Subject: [PATCH 1/3] chore: implementation --- lambdas/src/order-status-lambda/index.test.ts | 31 ++++++ lambdas/src/order-status-lambda/index.ts | 4 +- lambdas/src/order-status-lambda/init.test.ts | 12 ++- lambdas/src/order-status-lambda/init.ts | 9 +- .../notify-message-builder.test.ts | 17 ++++ .../notify-message-builder.ts | 26 +++++ .../notify-service.test.ts | 98 +++++++++++-------- .../src/order-status-lambda/notify-service.ts | 39 ++++++-- lambdas/src/order-status-lambda/types.ts | 2 + lambdas/src/order-status-lambda/utils.ts | 12 +-- ui/package-lock.json | 24 +---- 11 files changed, 190 insertions(+), 84 deletions(-) diff --git a/lambdas/src/order-status-lambda/index.test.ts b/lambdas/src/order-status-lambda/index.test.ts index 9ba8c161..b7ce639d 100644 --- a/lambdas/src/order-status-lambda/index.test.ts +++ b/lambdas/src/order-status-lambda/index.test.ts @@ -263,6 +263,17 @@ describe("Order Status Lambda Handler", () => { expect(result.statusCode).toBe(201); }); + + it(`should accept ${IncomingBusinessStatus.CONFIRMED} business status`, async () => { + mockEvent.body = JSON.stringify({ + ...validTaskBody, + businessStatus: { text: IncomingBusinessStatus.CONFIRMED }, + } satisfies Partial); + + const result = await handler(mockEvent as APIGatewayProxyEvent, {} as Context); + + expect(result.statusCode).toBe(201); + }); }); describe("Idempotency via Correlation ID", () => { @@ -446,6 +457,26 @@ describe("Order Status Lambda Handler", () => { ); }); + it("should delegate confirmed status to the notification service", async () => { + mockEvent.body = JSON.stringify({ + ...validTaskBody, + businessStatus: { + text: IncomingBusinessStatus.CONFIRMED, + }, + } satisfies Partial); + + const result = await handler(mockEvent as APIGatewayProxyEvent, {} as Context); + + expect(result.statusCode).toBe(201); + expect(mockHandleOrderStatusUpdated).toHaveBeenCalledWith( + expect.objectContaining({ + statusUpdate: expect.objectContaining({ + statusCode: businessStatusMapping[IncomingBusinessStatus.CONFIRMED], + }), + }), + ); + }); + it("should return 500 when notification service fails", async () => { mockHandleOrderStatusUpdated.mockRejectedValueOnce(new Error("Unexpected side effect error")); mockEvent.body = JSON.stringify(validTaskBody); diff --git a/lambdas/src/order-status-lambda/index.ts b/lambdas/src/order-status-lambda/index.ts index b7a3c0ac..b42b9466 100644 --- a/lambdas/src/order-status-lambda/index.ts +++ b/lambdas/src/order-status-lambda/index.ts @@ -149,10 +149,12 @@ export const lambdaHandler = async ( return createFhirResponse(200, validatedTask); } + const mappedStatusCode = businessStatusMapping[validatedTask.businessStatus.text]; + // Process the update const statusOrderUpdateParams: OrderStatusUpdateParams = { orderId, - statusCode: businessStatusMapping[validatedTask.businessStatus.text], + statusCode: mappedStatusCode, createdAt: validatedTask.lastModified, correlationId, }; diff --git a/lambdas/src/order-status-lambda/init.test.ts b/lambdas/src/order-status-lambda/init.test.ts index c37fa91e..e17f26b1 100644 --- a/lambdas/src/order-status-lambda/init.test.ts +++ b/lambdas/src/order-status-lambda/init.test.ts @@ -1,6 +1,7 @@ import { PostgresDbClient } from "../lib/db/db-client"; import { postgresConfigFromEnv } from "../lib/db/db-config"; import { NotificationAuditDbClient } from "../lib/db/notification-audit-db-client"; +import { OrderDbClient } from "../lib/db/order-db-client"; import { OrderStatusService } from "../lib/db/order-status-db"; import { PatientDbClient } from "../lib/db/patient-db-client"; import { AwsSecretsClient } from "../lib/secrets/secrets-manager-client"; @@ -12,6 +13,7 @@ import { NotifyMessageBuilder } from "./notify-message-builder"; import { OrderStatusNotifyService } from "./notify-service"; jest.mock("../lib/db/order-status-db"); +jest.mock("../lib/db/order-db-client"); jest.mock("../lib/db/patient-db-client"); jest.mock("../lib/db/notification-audit-db-client"); jest.mock("../lib/db/db-client"); @@ -109,6 +111,8 @@ describe("init", () => { expect(result).toEqual({ orderStatusDb: expect.any(OrderStatusService), + patientDbClient: expect.any(PatientDbClient), + orderDbClient: expect.any(OrderDbClient), orderStatusNotifyService: expect.any(OrderStatusNotifyService), }); }); @@ -151,6 +155,11 @@ describe("init", () => { times: 1, calledWith: expect.any(PostgresDbClient), }, + { + mock: OrderDbClient as jest.Mock, + times: 1, + calledWith: expect.any(PostgresDbClient), + }, { mock: NotificationAuditDbClient as jest.Mock, times: 1, @@ -185,8 +194,9 @@ describe("init", () => { init(); expect(OrderStatusNotifyService).toHaveBeenCalledWith({ - orderStatusDb: expect.any(OrderStatusService), notificationAuditDbClient: expect.any(NotificationAuditDbClient), + patientDbClient: expect.any(PatientDbClient), + orderDbClient: expect.any(OrderDbClient), sqsClient: expect.any(AWSSQSClient), notifyMessageBuilder: expect.any(NotifyMessageBuilder), notifyMessagesQueueUrl: "https://example.queue.local/notify", diff --git a/lambdas/src/order-status-lambda/init.ts b/lambdas/src/order-status-lambda/init.ts index e2a30284..a419767d 100644 --- a/lambdas/src/order-status-lambda/init.ts +++ b/lambdas/src/order-status-lambda/init.ts @@ -1,6 +1,7 @@ import { PostgresDbClient } from "../lib/db/db-client"; import { postgresConfigFromEnv } from "../lib/db/db-config"; import { NotificationAuditDbClient } from "../lib/db/notification-audit-db-client"; +import { OrderDbClient } from "../lib/db/order-db-client"; import { OrderStatusService } from "../lib/db/order-status-db"; import { PatientDbClient } from "../lib/db/patient-db-client"; import { AwsSecretsClient } from "../lib/secrets/secrets-manager-client"; @@ -11,6 +12,8 @@ import { OrderStatusNotifyService } from "./notify-service"; export interface Environment { orderStatusDb: OrderStatusService; + patientDbClient: PatientDbClient; + orderDbClient: OrderDbClient; orderStatusNotifyService: OrderStatusNotifyService; } @@ -23,12 +26,14 @@ export function buildEnvironment(): Environment { const dbClient = new PostgresDbClient(postgresConfigFromEnv(secretsClient)); const orderStatusDb = new OrderStatusService(dbClient); const patientDbClient = new PatientDbClient(dbClient); + const orderDbClient = new OrderDbClient(dbClient); const notificationAuditDbClient = new NotificationAuditDbClient(dbClient); const sqsClient = new AWSSQSClient(); const notifyMessageBuilder = new NotifyMessageBuilder(patientDbClient, homeTestBaseUrl); const orderStatusNotifyService = new OrderStatusNotifyService({ - orderStatusDb, notificationAuditDbClient, + patientDbClient, + orderDbClient, sqsClient, notifyMessageBuilder, notifyMessagesQueueUrl, @@ -36,6 +41,8 @@ export function buildEnvironment(): Environment { return { orderStatusDb, + patientDbClient, + orderDbClient, orderStatusNotifyService, }; } diff --git a/lambdas/src/order-status-lambda/notify-message-builder.test.ts b/lambdas/src/order-status-lambda/notify-message-builder.test.ts index 9582ca90..a1beef51 100644 --- a/lambdas/src/order-status-lambda/notify-message-builder.test.ts +++ b/lambdas/src/order-status-lambda/notify-message-builder.test.ts @@ -95,4 +95,21 @@ describe("NotifyMessageBuilder", () => { "https://hometest.example.nhs.uk/orders/550e8400-e29b-41d4-a716-446655440000/tracking", }); }); + + it("should build confirmed notify message with orderedDate in personalisation", async () => { + const result = await builder.buildOrderConfirmedNotifyMessage({ + patientId: "550e8400-e29b-41d4-a716-446655440111", + correlationId: "123e4567-e89b-12d3-a456-426614174000", + orderId: "550e8400-e29b-41d4-a716-446655440000", + orderedAt: "2026-08-06T10:00:00Z", + }); + + expect(result.correlationId).toBe("123e4567-e89b-12d3-a456-426614174000"); + expect(result.eventCode).toBe(NotifyEventCode.OrderConfirmed); + expect(result.personalisation).toEqual({ + orderedDate: "6 August 2026", + orderLinkUrl: + "https://hometest.example.nhs.uk/orders/550e8400-e29b-41d4-a716-446655440000/tracking", + }); + }); }); diff --git a/lambdas/src/order-status-lambda/notify-message-builder.ts b/lambdas/src/order-status-lambda/notify-message-builder.ts index 537a4eb7..06dc78db 100644 --- a/lambdas/src/order-status-lambda/notify-message-builder.ts +++ b/lambdas/src/order-status-lambda/notify-message-builder.ts @@ -10,6 +10,13 @@ export interface BuildOrderDispatchedNotifyMessageInput { dispatchedAt: string; } +export interface BuildOrderConfirmedNotifyMessageInput { + patientId: string; + correlationId: string; + orderId: string; + orderedAt: string; +} + export interface BuildOrderReceivedNotifyMessageInput { patientId: string; correlationId: string; @@ -53,6 +60,25 @@ export class NotifyMessageBuilder { }); } + async buildOrderConfirmedNotifyMessage( + input: BuildOrderConfirmedNotifyMessageInput, + ): Promise { + const { patientId, correlationId, orderId, orderedAt } = input; + + const trackingUrl = `${this.normalizedHomeTestBaseUrl}/orders/${orderId}/tracking`; + + return this.buildOrderStatusNotifyMessage({ + patientId, + correlationId, + orderId, + eventCode: NotifyEventCode.OrderConfirmed, + personalisation: { + orderedDate: formatStatusDate(orderedAt), + orderLinkUrl: trackingUrl, + }, + }); + } + async buildOrderReceivedNotifyMessage( input: BuildOrderReceivedNotifyMessageInput, ): Promise { diff --git a/lambdas/src/order-status-lambda/notify-service.test.ts b/lambdas/src/order-status-lambda/notify-service.test.ts index a5aedb77..a5581171 100644 --- a/lambdas/src/order-status-lambda/notify-service.test.ts +++ b/lambdas/src/order-status-lambda/notify-service.test.ts @@ -4,7 +4,9 @@ import { NotifyEventCode } from "../lib/types/notify-message"; import { OrderStatusNotifyService } from "./notify-service"; describe("OrderStatusNotifyService", () => { - const mockIsFirstStatusOccurrence = jest.fn, [string, string]>(); + const mockGetPatient = jest.fn(); + const mockGetOrder = jest.fn(); + const mockBuildOrderConfirmedNotifyMessage = jest.fn(); const mockBuildOrderDispatchedNotifyMessage = jest.fn(); const mockBuildOrderReceivedNotifyMessage = jest.fn(); const mockSendMessage = jest.fn(); @@ -22,7 +24,24 @@ describe("OrderStatusNotifyService", () => { beforeEach(() => { jest.clearAllMocks(); - mockIsFirstStatusOccurrence.mockResolvedValue(true); + mockGetPatient.mockResolvedValue({ + nhsNumber: "1234567890", + birthDate: "1990-01-02", + }); + mockGetOrder.mockResolvedValue({ + created_at: new Date("2026-08-06T08:30:00Z"), + }); + + mockBuildOrderConfirmedNotifyMessage.mockResolvedValue({ + messageReference: "123e4567-e89b-12d3-a456-426614174090", + eventCode: NotifyEventCode.OrderConfirmed, + correlationId: statusUpdate.correlationId, + recipient: { + nhsNumber: "1234567890", + dateOfBirth: "1990-01-02", + }, + personalisation: {}, + }); mockBuildOrderDispatchedNotifyMessage.mockResolvedValue({ messageReference: "123e4567-e89b-12d3-a456-426614174099", eventCode: NotifyEventCode.OrderDispatched, @@ -47,16 +66,20 @@ describe("OrderStatusNotifyService", () => { mockInsertNotificationAuditEntry.mockResolvedValue(undefined); service = new OrderStatusNotifyService({ - orderStatusDb: { - isFirstStatusOccurrence: mockIsFirstStatusOccurrence, - } as never, notificationAuditDbClient: { insertNotificationAuditEntry: mockInsertNotificationAuditEntry, } as never, + patientDbClient: { + get: mockGetPatient, + } as never, + orderDbClient: { + getOrder: mockGetOrder, + } as never, sqsClient: { sendMessage: mockSendMessage, }, notifyMessageBuilder: { + buildOrderConfirmedNotifyMessage: mockBuildOrderConfirmedNotifyMessage, buildOrderDispatchedNotifyMessage: mockBuildOrderDispatchedNotifyMessage, buildOrderReceivedNotifyMessage: mockBuildOrderReceivedNotifyMessage, } as never, @@ -75,34 +98,45 @@ describe("OrderStatusNotifyService", () => { }, }); - expect(mockIsFirstStatusOccurrence).not.toHaveBeenCalled(); + expect(mockBuildOrderConfirmedNotifyMessage).not.toHaveBeenCalled(); expect(mockBuildOrderDispatchedNotifyMessage).not.toHaveBeenCalled(); expect(mockBuildOrderReceivedNotifyMessage).not.toHaveBeenCalled(); expect(mockSendMessage).not.toHaveBeenCalled(); expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); }); - it("should not send a dispatched notification when it is not the first occurrence", async () => { - mockIsFirstStatusOccurrence.mockResolvedValueOnce(false); - + it("should send and audit a confirmed notification", async () => { await service.handleOrderStatusUpdated({ orderId: statusUpdate.orderId, patientId: "patient-123", correlationId: statusUpdate.correlationId, - statusUpdate, + statusUpdate: { + ...statusUpdate, + statusCode: OrderStatusCodes.CONFIRMED, + }, }); - expect(mockIsFirstStatusOccurrence).toHaveBeenCalledWith( - statusUpdate.orderId, - OrderStatusCodes.DISPATCHED, + expect(mockGetPatient).toHaveBeenCalledWith("patient-123"); + expect(mockGetOrder).toHaveBeenCalledWith(statusUpdate.orderId, "1234567890", expect.any(Date)); + expect(mockBuildOrderConfirmedNotifyMessage).toHaveBeenCalledWith({ + patientId: "patient-123", + correlationId: statusUpdate.correlationId, + orderId: statusUpdate.orderId, + orderedAt: "2026-08-06T08:30:00.000Z", + }); + expect(mockSendMessage).toHaveBeenCalledWith( + "https://example.queue.local/notify", + expect.any(String), ); - expect(mockBuildOrderDispatchedNotifyMessage).not.toHaveBeenCalled(); - expect(mockBuildOrderReceivedNotifyMessage).not.toHaveBeenCalled(); - expect(mockSendMessage).not.toHaveBeenCalled(); - expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); + expect(mockInsertNotificationAuditEntry).toHaveBeenCalledWith({ + messageReference: "123e4567-e89b-12d3-a456-426614174090", + eventCode: NotifyEventCode.OrderConfirmed, + correlationId: statusUpdate.correlationId, + status: NotificationAuditStatus.QUEUED, + }); }); - it("should send and audit the first dispatched notification", async () => { + it("should send and audit a dispatched notification", async () => { await service.handleOrderStatusUpdated({ orderId: statusUpdate.orderId, patientId: "patient-123", @@ -116,6 +150,8 @@ describe("OrderStatusNotifyService", () => { orderId: statusUpdate.orderId, dispatchedAt: statusUpdate.createdAt, }); + expect(mockGetPatient).not.toHaveBeenCalled(); + expect(mockGetOrder).not.toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledWith( "https://example.queue.local/notify", expect.any(String), @@ -128,29 +164,7 @@ describe("OrderStatusNotifyService", () => { }); }); - it("should not send a received notification when it is not the first occurrence", async () => { - mockIsFirstStatusOccurrence.mockResolvedValueOnce(false); - - await service.handleOrderStatusUpdated({ - orderId: statusUpdate.orderId, - patientId: "patient-123", - correlationId: statusUpdate.correlationId, - statusUpdate: { - ...statusUpdate, - statusCode: OrderStatusCodes.RECEIVED, - }, - }); - - expect(mockIsFirstStatusOccurrence).toHaveBeenCalledWith( - statusUpdate.orderId, - OrderStatusCodes.RECEIVED, - ); - expect(mockBuildOrderReceivedNotifyMessage).not.toHaveBeenCalled(); - expect(mockSendMessage).not.toHaveBeenCalled(); - expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); - }); - - it("should send and audit the first received notification", async () => { + it("should send and audit a received notification", async () => { await service.handleOrderStatusUpdated({ orderId: statusUpdate.orderId, patientId: "patient-123", @@ -167,6 +181,8 @@ describe("OrderStatusNotifyService", () => { orderId: statusUpdate.orderId, receivedAt: statusUpdate.createdAt, }); + expect(mockGetPatient).not.toHaveBeenCalled(); + expect(mockGetOrder).not.toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledWith( "https://example.queue.local/notify", expect.any(String), diff --git a/lambdas/src/order-status-lambda/notify-service.ts b/lambdas/src/order-status-lambda/notify-service.ts index 2e38ee0d..a80b500f 100644 --- a/lambdas/src/order-status-lambda/notify-service.ts +++ b/lambdas/src/order-status-lambda/notify-service.ts @@ -3,12 +3,13 @@ import { NotificationAuditDbClient, NotificationAuditStatus, } from "../lib/db/notification-audit-db-client"; +import { OrderDbClient } from "../lib/db/order-db-client"; import { OrderStatusCode, OrderStatusCodes, - OrderStatusService, OrderStatusUpdateParams, } from "../lib/db/order-status-db"; +import { PatientDbClient } from "../lib/db/patient-db-client"; import { SQSClientInterface } from "../lib/sqs/sqs-client"; import type { NotifyMessage } from "../lib/types/notify-message"; import { NotifyMessageBuilder } from "./notify-message-builder"; @@ -17,8 +18,9 @@ const commons = new ConsoleCommons(); const name = "order-status-lambda"; export interface OrderStatusNotifyServiceDependencies { - orderStatusDb: OrderStatusService; notificationAuditDbClient: NotificationAuditDbClient; + patientDbClient: PatientDbClient; + orderDbClient: OrderDbClient; sqsClient: SQSClientInterface; notifyMessageBuilder: NotifyMessageBuilder; notifyMessagesQueueUrl: string; @@ -36,6 +38,7 @@ interface BuildNotifyMessageInput { patientId: string; correlationId: string; createdAt: string; + orderedAt?: string; } type NotifyMessageBuilderByStatus = Partial< @@ -45,6 +48,18 @@ type NotifyMessageBuilderByStatus = Partial< export class OrderStatusNotifyService { constructor(private readonly dependencies: OrderStatusNotifyServiceDependencies) {} + private async getOrderedAtValue(orderId: string, patientId: string): Promise { + const { patientDbClient, orderDbClient } = this.dependencies; + const patient = await patientDbClient.get(patientId); + const order = await orderDbClient.getOrder( + orderId, + patient.nhsNumber, + new Date(patient.birthDate), + ); + + return order?.created_at.toISOString() ?? null; + } + async handleOrderStatusUpdated( handleOrderStatusUpdatedInput: HandleOrderStatusUpdatedInput, ): Promise { @@ -52,6 +67,13 @@ export class OrderStatusNotifyService { const { notifyMessageBuilder } = this.dependencies; const buildNotifyMessageByStatus: NotifyMessageBuilderByStatus = { + [OrderStatusCodes.CONFIRMED]: ({ patientId, correlationId, orderId, createdAt, orderedAt }) => + notifyMessageBuilder.buildOrderConfirmedNotifyMessage({ + patientId, + correlationId, + orderId, + orderedAt: orderedAt ?? createdAt, + }), [OrderStatusCodes.DISPATCHED]: ({ patientId, correlationId, orderId, createdAt }) => notifyMessageBuilder.buildOrderDispatchedNotifyMessage({ patientId, @@ -83,21 +105,20 @@ export class OrderStatusNotifyService { ): Promise { const { orderId, patientId, correlationId, statusUpdate } = input; const { statusCode } = statusUpdate; - const { orderStatusDb, notificationAuditDbClient, sqsClient, notifyMessagesQueueUrl } = - this.dependencies; + const { notificationAuditDbClient, sqsClient, notifyMessagesQueueUrl } = this.dependencies; try { - const isFirstOccurrence = await orderStatusDb.isFirstStatusOccurrence(orderId, statusCode); - - if (!isFirstOccurrence) { - return; - } + const orderedAt = + statusCode === OrderStatusCodes.CONFIRMED + ? await this.getOrderedAtValue(orderId, patientId) + : null; const notifyMessage = await buildNotifyMessage({ patientId, correlationId, orderId, createdAt: statusUpdate.createdAt, + ...(orderedAt ? { orderedAt } : {}), }); await sqsClient.sendMessage(notifyMessagesQueueUrl, JSON.stringify(notifyMessage)); diff --git a/lambdas/src/order-status-lambda/types.ts b/lambdas/src/order-status-lambda/types.ts index 140f35ed..08a51105 100644 --- a/lambdas/src/order-status-lambda/types.ts +++ b/lambdas/src/order-status-lambda/types.ts @@ -1,9 +1,11 @@ export enum IncomingBusinessStatus { + CONFIRMED = "confirmed", DISPATCHED = "dispatched", RECEIVED_AT_LAB = "received-at-lab", } export enum AllowedInternalBusinessStatuses { + CONFIRMED = "CONFIRMED", DISPATCHED = "DISPATCHED", RECEIVED = "RECEIVED", } diff --git a/lambdas/src/order-status-lambda/utils.ts b/lambdas/src/order-status-lambda/utils.ts index ad2bd270..5adcd16e 100644 --- a/lambdas/src/order-status-lambda/utils.ts +++ b/lambdas/src/order-status-lambda/utils.ts @@ -1,7 +1,4 @@ -import { - AllowedInternalBusinessStatuses, - IncomingBusinessStatus, -} from "./types"; +import { AllowedInternalBusinessStatuses, IncomingBusinessStatus } from "./types"; /** * Extract UUID from a FHIR reference (e.g., "ServiceRequest/550e8400-e29b-41d4-a716-446655440000") @@ -19,8 +16,7 @@ export const businessStatusMapping: Record< IncomingBusinessStatus, AllowedInternalBusinessStatuses > = { - [IncomingBusinessStatus.DISPATCHED]: - AllowedInternalBusinessStatuses.DISPATCHED, - [IncomingBusinessStatus.RECEIVED_AT_LAB]: - AllowedInternalBusinessStatuses.RECEIVED, + [IncomingBusinessStatus.CONFIRMED]: AllowedInternalBusinessStatuses.CONFIRMED, + [IncomingBusinessStatus.DISPATCHED]: AllowedInternalBusinessStatuses.DISPATCHED, + [IncomingBusinessStatus.RECEIVED_AT_LAB]: AllowedInternalBusinessStatuses.RECEIVED, }; diff --git a/ui/package-lock.json b/ui/package-lock.json index 8ff7db7f..8012c199 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -96,7 +96,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -684,7 +683,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -708,7 +706,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2236,7 +2233,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2523,7 +2519,6 @@ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2534,7 +2529,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2545,7 +2539,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2626,7 +2619,6 @@ "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", @@ -3107,7 +3099,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3535,7 +3526,6 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -3652,7 +3642,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4545,7 +4534,6 @@ "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -4692,7 +4680,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7406,7 +7393,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -7896,7 +7882,6 @@ "resolved": "https://registry.npmjs.org/nhsuk-frontend/-/nhsuk-frontend-10.4.2.tgz", "integrity": "sha512-DYa7E/jwWtQPKqzeF9eB9nVcTKHpjMYf+SydKao379qQapIkblfS2BNvKsVKuWpI0w+QgI8XSDNNOUTQEGRb1w==", "license": "MIT", - "peer": true, "engines": { "node": "^20.9.0 || ^22.11.0 || >= 24.11.0" }, @@ -8340,7 +8325,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8575,7 +8559,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8585,7 +8568,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9734,7 +9716,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9813,8 +9794,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -9936,7 +9916,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10550,7 +10529,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 82942bd34641f7607f96b5dccb0daadd9264c966 Mon Sep 17 00:00:00 2001 From: Usmaan Ali Date: Thu, 9 Apr 2026 13:47:03 +0100 Subject: [PATCH 2/3] chore: add is first occurence check back in --- lambdas/src/order-status-lambda/init.test.ts | 1 + lambdas/src/order-status-lambda/init.ts | 1 + .../notify-service.test.ts | 84 +++++++++++++++++++ .../src/order-status-lambda/notify-service.ts | 11 ++- 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/lambdas/src/order-status-lambda/init.test.ts b/lambdas/src/order-status-lambda/init.test.ts index e17f26b1..af8c7ba5 100644 --- a/lambdas/src/order-status-lambda/init.test.ts +++ b/lambdas/src/order-status-lambda/init.test.ts @@ -194,6 +194,7 @@ describe("init", () => { init(); expect(OrderStatusNotifyService).toHaveBeenCalledWith({ + orderStatusDb: expect.any(OrderStatusService), notificationAuditDbClient: expect.any(NotificationAuditDbClient), patientDbClient: expect.any(PatientDbClient), orderDbClient: expect.any(OrderDbClient), diff --git a/lambdas/src/order-status-lambda/init.ts b/lambdas/src/order-status-lambda/init.ts index a419767d..2ad7dfde 100644 --- a/lambdas/src/order-status-lambda/init.ts +++ b/lambdas/src/order-status-lambda/init.ts @@ -31,6 +31,7 @@ export function buildEnvironment(): Environment { const sqsClient = new AWSSQSClient(); const notifyMessageBuilder = new NotifyMessageBuilder(patientDbClient, homeTestBaseUrl); const orderStatusNotifyService = new OrderStatusNotifyService({ + orderStatusDb, notificationAuditDbClient, patientDbClient, orderDbClient, diff --git a/lambdas/src/order-status-lambda/notify-service.test.ts b/lambdas/src/order-status-lambda/notify-service.test.ts index a5581171..c63379f3 100644 --- a/lambdas/src/order-status-lambda/notify-service.test.ts +++ b/lambdas/src/order-status-lambda/notify-service.test.ts @@ -4,6 +4,7 @@ import { NotifyEventCode } from "../lib/types/notify-message"; import { OrderStatusNotifyService } from "./notify-service"; describe("OrderStatusNotifyService", () => { + const mockIsFirstStatusOccurrence = jest.fn(); const mockGetPatient = jest.fn(); const mockGetOrder = jest.fn(); const mockBuildOrderConfirmedNotifyMessage = jest.fn(); @@ -24,6 +25,7 @@ describe("OrderStatusNotifyService", () => { beforeEach(() => { jest.clearAllMocks(); + mockIsFirstStatusOccurrence.mockResolvedValue(true); mockGetPatient.mockResolvedValue({ nhsNumber: "1234567890", birthDate: "1990-01-02", @@ -66,6 +68,9 @@ describe("OrderStatusNotifyService", () => { mockInsertNotificationAuditEntry.mockResolvedValue(undefined); service = new OrderStatusNotifyService({ + orderStatusDb: { + isFirstStatusOccurrence: mockIsFirstStatusOccurrence, + } as never, notificationAuditDbClient: { insertNotificationAuditEntry: mockInsertNotificationAuditEntry, } as never, @@ -98,6 +103,7 @@ describe("OrderStatusNotifyService", () => { }, }); + expect(mockIsFirstStatusOccurrence).not.toHaveBeenCalled(); expect(mockBuildOrderConfirmedNotifyMessage).not.toHaveBeenCalled(); expect(mockBuildOrderDispatchedNotifyMessage).not.toHaveBeenCalled(); expect(mockBuildOrderReceivedNotifyMessage).not.toHaveBeenCalled(); @@ -105,6 +111,30 @@ describe("OrderStatusNotifyService", () => { expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); }); + it("should not send a confirmed notification when it is not the first occurrence", async () => { + mockIsFirstStatusOccurrence.mockResolvedValueOnce(false); + + await service.handleOrderStatusUpdated({ + orderId: statusUpdate.orderId, + patientId: "patient-123", + correlationId: statusUpdate.correlationId, + statusUpdate: { + ...statusUpdate, + statusCode: OrderStatusCodes.CONFIRMED, + }, + }); + + expect(mockIsFirstStatusOccurrence).toHaveBeenCalledWith( + statusUpdate.orderId, + OrderStatusCodes.CONFIRMED, + ); + expect(mockGetPatient).not.toHaveBeenCalled(); + expect(mockGetOrder).not.toHaveBeenCalled(); + expect(mockBuildOrderConfirmedNotifyMessage).not.toHaveBeenCalled(); + expect(mockSendMessage).not.toHaveBeenCalled(); + expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); + }); + it("should send and audit a confirmed notification", async () => { await service.handleOrderStatusUpdated({ orderId: statusUpdate.orderId, @@ -116,6 +146,10 @@ describe("OrderStatusNotifyService", () => { }, }); + expect(mockIsFirstStatusOccurrence).toHaveBeenCalledWith( + statusUpdate.orderId, + OrderStatusCodes.CONFIRMED, + ); expect(mockGetPatient).toHaveBeenCalledWith("patient-123"); expect(mockGetOrder).toHaveBeenCalledWith(statusUpdate.orderId, "1234567890", expect.any(Date)); expect(mockBuildOrderConfirmedNotifyMessage).toHaveBeenCalledWith({ @@ -144,6 +178,10 @@ describe("OrderStatusNotifyService", () => { statusUpdate, }); + expect(mockIsFirstStatusOccurrence).toHaveBeenCalledWith( + statusUpdate.orderId, + OrderStatusCodes.DISPATCHED, + ); expect(mockBuildOrderDispatchedNotifyMessage).toHaveBeenCalledWith({ patientId: "patient-123", correlationId: statusUpdate.correlationId, @@ -164,6 +202,26 @@ describe("OrderStatusNotifyService", () => { }); }); + it("should not send a dispatched notification when it is not the first occurrence", async () => { + mockIsFirstStatusOccurrence.mockResolvedValueOnce(false); + + await service.handleOrderStatusUpdated({ + orderId: statusUpdate.orderId, + patientId: "patient-123", + correlationId: statusUpdate.correlationId, + statusUpdate, + }); + + expect(mockIsFirstStatusOccurrence).toHaveBeenCalledWith( + statusUpdate.orderId, + OrderStatusCodes.DISPATCHED, + ); + expect(mockBuildOrderDispatchedNotifyMessage).not.toHaveBeenCalled(); + expect(mockBuildOrderReceivedNotifyMessage).not.toHaveBeenCalled(); + expect(mockSendMessage).not.toHaveBeenCalled(); + expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); + }); + it("should send and audit a received notification", async () => { await service.handleOrderStatusUpdated({ orderId: statusUpdate.orderId, @@ -175,6 +233,10 @@ describe("OrderStatusNotifyService", () => { }, }); + expect(mockIsFirstStatusOccurrence).toHaveBeenCalledWith( + statusUpdate.orderId, + OrderStatusCodes.RECEIVED, + ); expect(mockBuildOrderReceivedNotifyMessage).toHaveBeenCalledWith({ patientId: "patient-123", correlationId: statusUpdate.correlationId, @@ -195,6 +257,28 @@ describe("OrderStatusNotifyService", () => { }); }); + it("should not send a received notification when it is not the first occurrence", async () => { + mockIsFirstStatusOccurrence.mockResolvedValueOnce(false); + + await service.handleOrderStatusUpdated({ + orderId: statusUpdate.orderId, + patientId: "patient-123", + correlationId: statusUpdate.correlationId, + statusUpdate: { + ...statusUpdate, + statusCode: OrderStatusCodes.RECEIVED, + }, + }); + + expect(mockIsFirstStatusOccurrence).toHaveBeenCalledWith( + statusUpdate.orderId, + OrderStatusCodes.RECEIVED, + ); + expect(mockBuildOrderReceivedNotifyMessage).not.toHaveBeenCalled(); + expect(mockSendMessage).not.toHaveBeenCalled(); + expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); + }); + it("should swallow errors when building the notify message fails", async () => { mockBuildOrderDispatchedNotifyMessage.mockRejectedValueOnce( new Error("Notify payload build failed"), diff --git a/lambdas/src/order-status-lambda/notify-service.ts b/lambdas/src/order-status-lambda/notify-service.ts index a80b500f..4fe281e0 100644 --- a/lambdas/src/order-status-lambda/notify-service.ts +++ b/lambdas/src/order-status-lambda/notify-service.ts @@ -7,6 +7,7 @@ import { OrderDbClient } from "../lib/db/order-db-client"; import { OrderStatusCode, OrderStatusCodes, + OrderStatusService, OrderStatusUpdateParams, } from "../lib/db/order-status-db"; import { PatientDbClient } from "../lib/db/patient-db-client"; @@ -18,6 +19,7 @@ const commons = new ConsoleCommons(); const name = "order-status-lambda"; export interface OrderStatusNotifyServiceDependencies { + orderStatusDb: OrderStatusService; notificationAuditDbClient: NotificationAuditDbClient; patientDbClient: PatientDbClient; orderDbClient: OrderDbClient; @@ -105,9 +107,16 @@ export class OrderStatusNotifyService { ): Promise { const { orderId, patientId, correlationId, statusUpdate } = input; const { statusCode } = statusUpdate; - const { notificationAuditDbClient, sqsClient, notifyMessagesQueueUrl } = this.dependencies; + const { orderStatusDb, notificationAuditDbClient, sqsClient, notifyMessagesQueueUrl } = + this.dependencies; try { + const isFirstOccurrence = await orderStatusDb.isFirstStatusOccurrence(orderId, statusCode); + + if (!isFirstOccurrence) { + return; + } + const orderedAt = statusCode === OrderStatusCodes.CONFIRMED ? await this.getOrderedAtValue(orderId, patientId) From 09f256c71d02724f13adfc19bc7905756437a161 Mon Sep 17 00:00:00 2001 From: Usmaan Ali Date: Thu, 9 Apr 2026 15:47:17 +0100 Subject: [PATCH 3/3] chore: avoid duplicate patient calls --- lambdas/src/order-status-lambda/init.test.ts | 3 +- lambdas/src/order-status-lambda/init.ts | 8 ++-- .../notify-message-builder.test.ts | 35 ++++++++++++++- .../notify-message-builder.ts | 43 ++++++++++++++----- .../notify-service.test.ts | 25 +---------- .../src/order-status-lambda/notify-service.ts | 22 ---------- 6 files changed, 74 insertions(+), 62 deletions(-) diff --git a/lambdas/src/order-status-lambda/init.test.ts b/lambdas/src/order-status-lambda/init.test.ts index af8c7ba5..a9a0bec8 100644 --- a/lambdas/src/order-status-lambda/init.test.ts +++ b/lambdas/src/order-status-lambda/init.test.ts @@ -186,6 +186,7 @@ describe("init", () => { expect(NotifyMessageBuilder).toHaveBeenCalledWith( expect.any(PatientDbClient), + expect.any(OrderDbClient), "https://hometest.example.nhs.uk", ); }); @@ -196,8 +197,6 @@ describe("init", () => { expect(OrderStatusNotifyService).toHaveBeenCalledWith({ orderStatusDb: expect.any(OrderStatusService), notificationAuditDbClient: expect.any(NotificationAuditDbClient), - patientDbClient: expect.any(PatientDbClient), - orderDbClient: expect.any(OrderDbClient), sqsClient: expect.any(AWSSQSClient), notifyMessageBuilder: expect.any(NotifyMessageBuilder), notifyMessagesQueueUrl: "https://example.queue.local/notify", diff --git a/lambdas/src/order-status-lambda/init.ts b/lambdas/src/order-status-lambda/init.ts index 2ad7dfde..4d7aed81 100644 --- a/lambdas/src/order-status-lambda/init.ts +++ b/lambdas/src/order-status-lambda/init.ts @@ -29,12 +29,14 @@ export function buildEnvironment(): Environment { const orderDbClient = new OrderDbClient(dbClient); const notificationAuditDbClient = new NotificationAuditDbClient(dbClient); const sqsClient = new AWSSQSClient(); - const notifyMessageBuilder = new NotifyMessageBuilder(patientDbClient, homeTestBaseUrl); + const notifyMessageBuilder = new NotifyMessageBuilder( + patientDbClient, + orderDbClient, + homeTestBaseUrl, + ); const orderStatusNotifyService = new OrderStatusNotifyService({ orderStatusDb, notificationAuditDbClient, - patientDbClient, - orderDbClient, sqsClient, notifyMessageBuilder, notifyMessagesQueueUrl, diff --git a/lambdas/src/order-status-lambda/notify-message-builder.test.ts b/lambdas/src/order-status-lambda/notify-message-builder.test.ts index a1beef51..8e51a7c6 100644 --- a/lambdas/src/order-status-lambda/notify-message-builder.test.ts +++ b/lambdas/src/order-status-lambda/notify-message-builder.test.ts @@ -1,14 +1,20 @@ +import type { OrderDbClient } from "../lib/db/order-db-client"; import type { Patient, PatientDbClient } from "../lib/db/patient-db-client"; import { NotifyEventCode } from "../lib/types/notify-message"; import { NotifyMessageBuilder } from "./notify-message-builder"; describe("NotifyMessageBuilder", () => { const mockGetPatient = jest.fn, [string]>(); + const mockGetOrder = jest.fn(); const mockPatientDbClient: Pick = { get: mockGetPatient, }; + const mockOrderDbClient: Pick = { + getOrder: mockGetOrder, + }; + let builder: NotifyMessageBuilder; beforeEach(() => { @@ -17,9 +23,13 @@ describe("NotifyMessageBuilder", () => { nhsNumber: "1234567890", birthDate: "1990-01-02", }); + mockGetOrder.mockResolvedValue({ + created_at: new Date("2026-08-06T08:30:00Z"), + }); builder = new NotifyMessageBuilder( mockPatientDbClient as PatientDbClient, + mockOrderDbClient as OrderDbClient, "https://hometest.example.nhs.uk", ); }); @@ -49,6 +59,7 @@ describe("NotifyMessageBuilder", () => { it("should normalize trailing slash in base url", async () => { const trailingSlashBuilder = new NotifyMessageBuilder( mockPatientDbClient as PatientDbClient, + mockOrderDbClient as OrderDbClient, "https://hometest.example.nhs.uk/", ); @@ -101,7 +112,7 @@ describe("NotifyMessageBuilder", () => { patientId: "550e8400-e29b-41d4-a716-446655440111", correlationId: "123e4567-e89b-12d3-a456-426614174000", orderId: "550e8400-e29b-41d4-a716-446655440000", - orderedAt: "2026-08-06T10:00:00Z", + orderedAt: "2026-08-01T10:00:00Z", }); expect(result.correlationId).toBe("123e4567-e89b-12d3-a456-426614174000"); @@ -111,5 +122,27 @@ describe("NotifyMessageBuilder", () => { orderLinkUrl: "https://hometest.example.nhs.uk/orders/550e8400-e29b-41d4-a716-446655440000/tracking", }); + expect(mockGetOrder).toHaveBeenCalledWith( + "550e8400-e29b-41d4-a716-446655440000", + "1234567890", + expect.any(Date), + ); + }); + + it("should fall back to provided orderedAt when order lookup returns null", async () => { + mockGetOrder.mockResolvedValueOnce(null); + + const result = await builder.buildOrderConfirmedNotifyMessage({ + patientId: "550e8400-e29b-41d4-a716-446655440111", + correlationId: "123e4567-e89b-12d3-a456-426614174000", + orderId: "550e8400-e29b-41d4-a716-446655440000", + orderedAt: "2026-08-01T10:00:00Z", + }); + + expect(result.personalisation).toEqual({ + orderedDate: "1 August 2026", + orderLinkUrl: + "https://hometest.example.nhs.uk/orders/550e8400-e29b-41d4-a716-446655440000/tracking", + }); }); }); diff --git a/lambdas/src/order-status-lambda/notify-message-builder.ts b/lambdas/src/order-status-lambda/notify-message-builder.ts index 06dc78db..f2698cb5 100644 --- a/lambdas/src/order-status-lambda/notify-message-builder.ts +++ b/lambdas/src/order-status-lambda/notify-message-builder.ts @@ -1,5 +1,6 @@ import { v4 as uuidv4 } from "uuid"; +import type { OrderDbClient } from "../lib/db/order-db-client"; import type { PatientDbClient } from "../lib/db/patient-db-client"; import { NotifyEventCode, NotifyMessage, NotifyRecipient } from "../lib/types/notify-message"; @@ -37,6 +38,7 @@ export class NotifyMessageBuilder { constructor( private readonly patientDbClient: PatientDbClient, + private readonly orderDbClient: OrderDbClient, homeTestBaseUrl: string, ) { this.normalizedHomeTestBaseUrl = homeTestBaseUrl.replaceAll(/\/$/g, ""); @@ -65,15 +67,26 @@ export class NotifyMessageBuilder { ): Promise { const { patientId, correlationId, orderId, orderedAt } = input; + const patient = await this.patientDbClient.get(patientId); + const order = await this.orderDbClient.getOrder( + orderId, + patient.nhsNumber, + new Date(patient.birthDate), + ); + const orderedAtValue = order?.created_at.toISOString() ?? orderedAt; + const trackingUrl = `${this.normalizedHomeTestBaseUrl}/orders/${orderId}/tracking`; return this.buildOrderStatusNotifyMessage({ - patientId, + recipient: { + nhsNumber: patient.nhsNumber, + dateOfBirth: patient.birthDate, + }, correlationId, orderId, eventCode: NotifyEventCode.OrderConfirmed, personalisation: { - orderedDate: formatStatusDate(orderedAt), + orderedDate: formatStatusDate(orderedAtValue), orderLinkUrl: trackingUrl, }, }); @@ -99,26 +112,36 @@ export class NotifyMessageBuilder { } private async buildOrderStatusNotifyMessage(input: { - patientId: string; + patientId?: string; + recipient?: NotifyRecipient; correlationId: string; orderId: string; eventCode: NotifyEventCode; personalisation: Record; }): Promise { - const { patientId, correlationId, eventCode, personalisation } = input; + const { patientId, recipient, correlationId, eventCode, personalisation } = input; - const patient = await this.patientDbClient.get(patientId); - const recipient: NotifyRecipient = { - nhsNumber: patient.nhsNumber, - dateOfBirth: patient.birthDate, - }; + const notifyRecipient = recipient ?? (await this.getRecipientFromPatientId(patientId)); return { correlationId, messageReference: uuidv4(), eventCode, - recipient, + recipient: notifyRecipient, personalisation, }; } + + private async getRecipientFromPatientId(patientId?: string): Promise { + if (!patientId) { + throw new Error("patientId is required when recipient is not provided"); + } + + const patient = await this.patientDbClient.get(patientId); + + return { + nhsNumber: patient.nhsNumber, + dateOfBirth: patient.birthDate, + }; + } } diff --git a/lambdas/src/order-status-lambda/notify-service.test.ts b/lambdas/src/order-status-lambda/notify-service.test.ts index c63379f3..00aff5d9 100644 --- a/lambdas/src/order-status-lambda/notify-service.test.ts +++ b/lambdas/src/order-status-lambda/notify-service.test.ts @@ -5,8 +5,6 @@ import { OrderStatusNotifyService } from "./notify-service"; describe("OrderStatusNotifyService", () => { const mockIsFirstStatusOccurrence = jest.fn(); - const mockGetPatient = jest.fn(); - const mockGetOrder = jest.fn(); const mockBuildOrderConfirmedNotifyMessage = jest.fn(); const mockBuildOrderDispatchedNotifyMessage = jest.fn(); const mockBuildOrderReceivedNotifyMessage = jest.fn(); @@ -26,13 +24,6 @@ describe("OrderStatusNotifyService", () => { jest.clearAllMocks(); mockIsFirstStatusOccurrence.mockResolvedValue(true); - mockGetPatient.mockResolvedValue({ - nhsNumber: "1234567890", - birthDate: "1990-01-02", - }); - mockGetOrder.mockResolvedValue({ - created_at: new Date("2026-08-06T08:30:00Z"), - }); mockBuildOrderConfirmedNotifyMessage.mockResolvedValue({ messageReference: "123e4567-e89b-12d3-a456-426614174090", @@ -74,12 +65,6 @@ describe("OrderStatusNotifyService", () => { notificationAuditDbClient: { insertNotificationAuditEntry: mockInsertNotificationAuditEntry, } as never, - patientDbClient: { - get: mockGetPatient, - } as never, - orderDbClient: { - getOrder: mockGetOrder, - } as never, sqsClient: { sendMessage: mockSendMessage, }, @@ -128,8 +113,6 @@ describe("OrderStatusNotifyService", () => { statusUpdate.orderId, OrderStatusCodes.CONFIRMED, ); - expect(mockGetPatient).not.toHaveBeenCalled(); - expect(mockGetOrder).not.toHaveBeenCalled(); expect(mockBuildOrderConfirmedNotifyMessage).not.toHaveBeenCalled(); expect(mockSendMessage).not.toHaveBeenCalled(); expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); @@ -150,13 +133,11 @@ describe("OrderStatusNotifyService", () => { statusUpdate.orderId, OrderStatusCodes.CONFIRMED, ); - expect(mockGetPatient).toHaveBeenCalledWith("patient-123"); - expect(mockGetOrder).toHaveBeenCalledWith(statusUpdate.orderId, "1234567890", expect.any(Date)); expect(mockBuildOrderConfirmedNotifyMessage).toHaveBeenCalledWith({ patientId: "patient-123", correlationId: statusUpdate.correlationId, orderId: statusUpdate.orderId, - orderedAt: "2026-08-06T08:30:00.000Z", + orderedAt: statusUpdate.createdAt, }); expect(mockSendMessage).toHaveBeenCalledWith( "https://example.queue.local/notify", @@ -188,8 +169,6 @@ describe("OrderStatusNotifyService", () => { orderId: statusUpdate.orderId, dispatchedAt: statusUpdate.createdAt, }); - expect(mockGetPatient).not.toHaveBeenCalled(); - expect(mockGetOrder).not.toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledWith( "https://example.queue.local/notify", expect.any(String), @@ -243,8 +222,6 @@ describe("OrderStatusNotifyService", () => { orderId: statusUpdate.orderId, receivedAt: statusUpdate.createdAt, }); - expect(mockGetPatient).not.toHaveBeenCalled(); - expect(mockGetOrder).not.toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledWith( "https://example.queue.local/notify", expect.any(String), diff --git a/lambdas/src/order-status-lambda/notify-service.ts b/lambdas/src/order-status-lambda/notify-service.ts index 4fe281e0..222111b3 100644 --- a/lambdas/src/order-status-lambda/notify-service.ts +++ b/lambdas/src/order-status-lambda/notify-service.ts @@ -3,14 +3,12 @@ import { NotificationAuditDbClient, NotificationAuditStatus, } from "../lib/db/notification-audit-db-client"; -import { OrderDbClient } from "../lib/db/order-db-client"; import { OrderStatusCode, OrderStatusCodes, OrderStatusService, OrderStatusUpdateParams, } from "../lib/db/order-status-db"; -import { PatientDbClient } from "../lib/db/patient-db-client"; import { SQSClientInterface } from "../lib/sqs/sqs-client"; import type { NotifyMessage } from "../lib/types/notify-message"; import { NotifyMessageBuilder } from "./notify-message-builder"; @@ -21,8 +19,6 @@ const name = "order-status-lambda"; export interface OrderStatusNotifyServiceDependencies { orderStatusDb: OrderStatusService; notificationAuditDbClient: NotificationAuditDbClient; - patientDbClient: PatientDbClient; - orderDbClient: OrderDbClient; sqsClient: SQSClientInterface; notifyMessageBuilder: NotifyMessageBuilder; notifyMessagesQueueUrl: string; @@ -50,18 +46,6 @@ type NotifyMessageBuilderByStatus = Partial< export class OrderStatusNotifyService { constructor(private readonly dependencies: OrderStatusNotifyServiceDependencies) {} - private async getOrderedAtValue(orderId: string, patientId: string): Promise { - const { patientDbClient, orderDbClient } = this.dependencies; - const patient = await patientDbClient.get(patientId); - const order = await orderDbClient.getOrder( - orderId, - patient.nhsNumber, - new Date(patient.birthDate), - ); - - return order?.created_at.toISOString() ?? null; - } - async handleOrderStatusUpdated( handleOrderStatusUpdatedInput: HandleOrderStatusUpdatedInput, ): Promise { @@ -117,17 +101,11 @@ export class OrderStatusNotifyService { return; } - const orderedAt = - statusCode === OrderStatusCodes.CONFIRMED - ? await this.getOrderedAtValue(orderId, patientId) - : null; - const notifyMessage = await buildNotifyMessage({ patientId, correlationId, orderId, createdAt: statusUpdate.createdAt, - ...(orderedAt ? { orderedAt } : {}), }); await sqsClient.sendMessage(notifyMessagesQueueUrl, JSON.stringify(notifyMessage));