From af13cd444831ffd07590e7d1764d7f7a4e9bc59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Rejterada?= Date: Fri, 3 Apr 2026 12:27:19 +0200 Subject: [PATCH] handle received status as part of notify message --- lambdas/src/lib/types/notify-message.ts | 1 + .../notify-message-builder.test.ts | 17 ++++ .../notify-message-builder.ts | 49 ++++++++++- .../notify-service.test.ts | 88 ++++++++++++++++++- .../src/order-status-lambda/notify-service.ts | 82 +++++++++++------ 5 files changed, 207 insertions(+), 30 deletions(-) diff --git a/lambdas/src/lib/types/notify-message.ts b/lambdas/src/lib/types/notify-message.ts index 5622e8ad..cf568405 100644 --- a/lambdas/src/lib/types/notify-message.ts +++ b/lambdas/src/lib/types/notify-message.ts @@ -14,4 +14,5 @@ export interface NotifyRecipient { export enum NotifyEventCode { OrderConfirmed = "ORDER_CONFIRMED", OrderDispatched = "ORDER_DISPATCHED", + OrderReceived = "ORDER_RECEIVED", } 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 bb3f8f5b..ecb034ed 100644 --- a/lambdas/src/order-status-lambda/notify-message-builder.test.ts +++ b/lambdas/src/order-status-lambda/notify-message-builder.test.ts @@ -78,4 +78,21 @@ describe("NotifyMessageBuilder", () => { expect(mockGetPatient).toHaveBeenCalledWith("550e8400-e29b-41d4-a716-446655440111"); }); + + it("should build received notify message with receivedDate in personalisation", async () => { + const result = await builder.buildOrderReceivedNotifyMessage({ + patientId: "550e8400-e29b-41d4-a716-446655440111", + correlationId: "123e4567-e89b-12d3-a456-426614174000", + orderId: "550e8400-e29b-41d4-a716-446655440000", + receivedAt: "2026-08-06T10:00:00Z", + }); + + expect(result.correlationId).toBe("123e4567-e89b-12d3-a456-426614174000"); + expect(result.eventCode).toBe(NotifyEventCode.OrderReceived); + expect(result.personalisation).toEqual({ + receivedDate: "6 August 2026", + statusLink: + "[View kit order update and see more information](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 3990e3d5..79886fb8 100644 --- a/lambdas/src/order-status-lambda/notify-message-builder.ts +++ b/lambdas/src/order-status-lambda/notify-message-builder.ts @@ -10,9 +10,16 @@ export interface BuildOrderDispatchedNotifyMessageInput { dispatchedAt: string; } +export interface BuildOrderReceivedNotifyMessageInput { + patientId: string; + correlationId: string; + orderId: string; + receivedAt: string; +} + const ORDER_TRACKING_LINK_TEXT = "View kit order update and see more information"; -const formatDispatchedDate = (isoDateTime: string): string => +const formatStatusDate = (isoDateTime: string): string => new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "long", @@ -35,6 +42,42 @@ export class NotifyMessageBuilder { ): Promise { const { patientId, correlationId, orderId, dispatchedAt } = input; + return this.buildOrderStatusNotifyMessage({ + patientId, + correlationId, + orderId, + eventCode: NotifyEventCode.OrderDispatched, + personalisation: { + dispatchedDate: formatStatusDate(dispatchedAt), + }, + }); + } + + async buildOrderReceivedNotifyMessage( + input: BuildOrderReceivedNotifyMessageInput, + ): Promise { + const { patientId, correlationId, orderId, receivedAt } = input; + + return this.buildOrderStatusNotifyMessage({ + patientId, + correlationId, + orderId, + eventCode: NotifyEventCode.OrderReceived, + personalisation: { + receivedDate: formatStatusDate(receivedAt), + }, + }); + } + + private async buildOrderStatusNotifyMessage(input: { + patientId: string; + correlationId: string; + orderId: string; + eventCode: NotifyEventCode; + personalisation: Record; + }): Promise { + const { patientId, correlationId, orderId, eventCode, personalisation } = input; + const patient = await this.patientDbClient.get(patientId); const recipient: NotifyRecipient = { nhsNumber: patient.nhsNumber, @@ -46,10 +89,10 @@ export class NotifyMessageBuilder { return { correlationId, messageReference: uuidv4(), - eventCode: NotifyEventCode.OrderDispatched, + eventCode, recipient, personalisation: { - dispatchedDate: formatDispatchedDate(dispatchedAt), + ...personalisation, statusLink: `[${ORDER_TRACKING_LINK_TEXT}](${trackingUrl})`, }, }; diff --git a/lambdas/src/order-status-lambda/notify-service.test.ts b/lambdas/src/order-status-lambda/notify-service.test.ts index b22d239b..a5aedb77 100644 --- a/lambdas/src/order-status-lambda/notify-service.test.ts +++ b/lambdas/src/order-status-lambda/notify-service.test.ts @@ -6,6 +6,7 @@ import { OrderStatusNotifyService } from "./notify-service"; describe("OrderStatusNotifyService", () => { const mockIsFirstStatusOccurrence = jest.fn, [string, string]>(); const mockBuildOrderDispatchedNotifyMessage = jest.fn(); + const mockBuildOrderReceivedNotifyMessage = jest.fn(); const mockSendMessage = jest.fn(); const mockInsertNotificationAuditEntry = jest.fn(); @@ -32,6 +33,16 @@ describe("OrderStatusNotifyService", () => { }, personalisation: {}, }); + mockBuildOrderReceivedNotifyMessage.mockResolvedValue({ + messageReference: "123e4567-e89b-12d3-a456-426614174199", + eventCode: NotifyEventCode.OrderReceived, + correlationId: statusUpdate.correlationId, + recipient: { + nhsNumber: "1234567890", + dateOfBirth: "1990-01-02", + }, + personalisation: {}, + }); mockSendMessage.mockResolvedValue({ messageId: "message-id" }); mockInsertNotificationAuditEntry.mockResolvedValue(undefined); @@ -47,6 +58,7 @@ describe("OrderStatusNotifyService", () => { }, notifyMessageBuilder: { buildOrderDispatchedNotifyMessage: mockBuildOrderDispatchedNotifyMessage, + buildOrderReceivedNotifyMessage: mockBuildOrderReceivedNotifyMessage, } as never, notifyMessagesQueueUrl: "https://example.queue.local/notify", }); @@ -59,12 +71,13 @@ describe("OrderStatusNotifyService", () => { correlationId: statusUpdate.correlationId, statusUpdate: { ...statusUpdate, - statusCode: OrderStatusCodes.RECEIVED, + statusCode: OrderStatusCodes.COMPLETE, }, }); expect(mockIsFirstStatusOccurrence).not.toHaveBeenCalled(); expect(mockBuildOrderDispatchedNotifyMessage).not.toHaveBeenCalled(); + expect(mockBuildOrderReceivedNotifyMessage).not.toHaveBeenCalled(); expect(mockSendMessage).not.toHaveBeenCalled(); expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); }); @@ -84,6 +97,7 @@ describe("OrderStatusNotifyService", () => { OrderStatusCodes.DISPATCHED, ); expect(mockBuildOrderDispatchedNotifyMessage).not.toHaveBeenCalled(); + expect(mockBuildOrderReceivedNotifyMessage).not.toHaveBeenCalled(); expect(mockSendMessage).not.toHaveBeenCalled(); expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); }); @@ -114,6 +128,57 @@ 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 () => { + await service.handleOrderStatusUpdated({ + orderId: statusUpdate.orderId, + patientId: "patient-123", + correlationId: statusUpdate.correlationId, + statusUpdate: { + ...statusUpdate, + statusCode: OrderStatusCodes.RECEIVED, + }, + }); + + expect(mockBuildOrderReceivedNotifyMessage).toHaveBeenCalledWith({ + patientId: "patient-123", + correlationId: statusUpdate.correlationId, + orderId: statusUpdate.orderId, + receivedAt: statusUpdate.createdAt, + }); + expect(mockSendMessage).toHaveBeenCalledWith( + "https://example.queue.local/notify", + expect.any(String), + ); + expect(mockInsertNotificationAuditEntry).toHaveBeenCalledWith({ + messageReference: "123e4567-e89b-12d3-a456-426614174199", + eventCode: NotifyEventCode.OrderReceived, + correlationId: statusUpdate.correlationId, + status: NotificationAuditStatus.QUEUED, + }); + }); + it("should swallow errors when building the notify message fails", async () => { mockBuildOrderDispatchedNotifyMessage.mockRejectedValueOnce( new Error("Notify payload build failed"), @@ -146,4 +211,25 @@ describe("OrderStatusNotifyService", () => { expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); }); + + it("should swallow errors when building the received notify message fails", async () => { + mockBuildOrderReceivedNotifyMessage.mockRejectedValueOnce( + new Error("Notify payload build failed"), + ); + + await expect( + service.handleOrderStatusUpdated({ + orderId: statusUpdate.orderId, + patientId: "patient-123", + correlationId: statusUpdate.correlationId, + statusUpdate: { + ...statusUpdate, + statusCode: OrderStatusCodes.RECEIVED, + }, + }), + ).resolves.toBeUndefined(); + + expect(mockSendMessage).not.toHaveBeenCalled(); + expect(mockInsertNotificationAuditEntry).not.toHaveBeenCalled(); + }); }); diff --git a/lambdas/src/order-status-lambda/notify-service.ts b/lambdas/src/order-status-lambda/notify-service.ts index 16c78461..4a42b5b0 100644 --- a/lambdas/src/order-status-lambda/notify-service.ts +++ b/lambdas/src/order-status-lambda/notify-service.ts @@ -4,11 +4,13 @@ import { NotificationAuditStatus, } from "../lib/db/notification-audit-db-client"; import { + OrderStatusCode, OrderStatusCodes, OrderStatusService, OrderStatusUpdateParams, } from "../lib/db/order-status-db"; import { SQSClientInterface } from "../lib/sqs/sqs-client"; +import type { NotifyMessage } from "../lib/types/notify-message"; import { NotifyMessageBuilder } from "./notify-message-builder"; const commons = new ConsoleCommons(); @@ -29,46 +31,73 @@ export interface HandleOrderStatusUpdatedInput { statusUpdate: OrderStatusUpdateParams; } +interface BuildNotifyMessageInput { + orderId: string; + patientId: string; + correlationId: string; + createdAt: string; +} + +type NotifyMessageBuilderByStatus = Partial< + Record Promise> +>; + export class OrderStatusNotifyService { constructor(private readonly dependencies: OrderStatusNotifyServiceDependencies) {} - async handleOrderStatusUpdated(input: HandleOrderStatusUpdatedInput): Promise { - const { statusUpdate } = input; + async handleOrderStatusUpdated( + handleOrderStatusUpdatedInput: HandleOrderStatusUpdatedInput, + ): Promise { + const { statusUpdate } = handleOrderStatusUpdatedInput; + const { notifyMessageBuilder } = this.dependencies; - switch (statusUpdate.statusCode) { - case OrderStatusCodes.DISPATCHED: - await this.handleDispatchedStatusUpdated(input); - return; - default: - return; + const buildNotifyMessageByStatus: NotifyMessageBuilderByStatus = { + [OrderStatusCodes.DISPATCHED]: ({ patientId, correlationId, orderId, createdAt }) => + notifyMessageBuilder.buildOrderDispatchedNotifyMessage({ + patientId, + correlationId, + orderId, + dispatchedAt: createdAt, + }), + [OrderStatusCodes.RECEIVED]: ({ patientId, correlationId, orderId, createdAt }) => + notifyMessageBuilder.buildOrderReceivedNotifyMessage({ + patientId, + correlationId, + orderId, + receivedAt: createdAt, + }), + }; + + const buildNotifyMessageFunc = buildNotifyMessageByStatus[statusUpdate.statusCode]; + + if (!buildNotifyMessageFunc) { + return; } + + await this.handleStatusUpdated(handleOrderStatusUpdatedInput, buildNotifyMessageFunc); } - private async handleDispatchedStatusUpdated(input: HandleOrderStatusUpdatedInput): Promise { + private async handleStatusUpdated( + input: HandleOrderStatusUpdatedInput, + buildNotifyMessage: (input: BuildNotifyMessageInput) => Promise, + ): Promise { const { orderId, patientId, correlationId, statusUpdate } = input; - const { - orderStatusDb, - notificationAuditDbClient, - sqsClient, - notifyMessageBuilder, - notifyMessagesQueueUrl, - } = this.dependencies; - - const isFirstDispatched = await orderStatusDb.isFirstStatusOccurrence( - orderId, - OrderStatusCodes.DISPATCHED, - ); - - if (!isFirstDispatched) { + const { statusCode } = statusUpdate; + const { orderStatusDb, notificationAuditDbClient, sqsClient, notifyMessagesQueueUrl } = + this.dependencies; + + const isFirstOccurrence = await orderStatusDb.isFirstStatusOccurrence(orderId, statusCode); + + if (!isFirstOccurrence) { return; } try { - const notifyMessage = await notifyMessageBuilder.buildOrderDispatchedNotifyMessage({ + const notifyMessage = await buildNotifyMessage({ patientId, correlationId, orderId, - dispatchedAt: statusUpdate.createdAt, + createdAt: statusUpdate.createdAt, }); await sqsClient.sendMessage(notifyMessagesQueueUrl, JSON.stringify(notifyMessage)); @@ -80,9 +109,10 @@ export class OrderStatusNotifyService { status: NotificationAuditStatus.QUEUED, }); } catch (error) { - commons.logError(name, "Failed to send dispatched notification", { + commons.logError(name, "Failed to send status notification", { correlationId, orderId, + statusCode, error, }); }