From 3f77980d887113ae20362eaef26bb35162ffa9c0 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 4 Jun 2026 11:05:29 +0100 Subject: [PATCH 1/9] inital changes for sha256 --- internal/datastore/src/types.ts | 1 + lambdas/api-handler/src/contracts/letters.ts | 1 + lambdas/api-handler/src/mappers/letter-mapper.ts | 1 + lambdas/upsert-letter/src/handler/upsert-handler.ts | 1 + .../letter-test-data/src/__test__/helpers/s3-helpers.test.ts | 4 ++++ scripts/utilities/letter-test-data/src/helpers/s3-helpers.ts | 5 +++++ 6 files changed, 13 insertions(+) diff --git a/internal/datastore/src/types.ts b/internal/datastore/src/types.ts index 53708cedf..39ece3492 100644 --- a/internal/datastore/src/types.ts +++ b/internal/datastore/src/types.ts @@ -42,6 +42,7 @@ export const LetterSchemaBase = z.object({ groupId: z.string(), reasonCode: z.string().optional(), reasonText: z.string().optional(), + sha256Hash: z.string().optional(), }); export const LetterSchema = LetterSchemaBase.extend({ diff --git a/lambdas/api-handler/src/contracts/letters.ts b/lambdas/api-handler/src/contracts/letters.ts index 0c3d14182..ad906b6f2 100644 --- a/lambdas/api-handler/src/contracts/letters.ts +++ b/lambdas/api-handler/src/contracts/letters.ts @@ -52,6 +52,7 @@ export const GetLetterResponseResourceSchema = z groupId: z.string().optional(), reasonCode: z.string().optional(), reasonText: z.string().optional(), + sha256Hash: z.string().optional(), }) .strict(), }) diff --git a/lambdas/api-handler/src/mappers/letter-mapper.ts b/lambdas/api-handler/src/mappers/letter-mapper.ts index c31d61b34..0db9bcca6 100644 --- a/lambdas/api-handler/src/mappers/letter-mapper.ts +++ b/lambdas/api-handler/src/mappers/letter-mapper.ts @@ -22,6 +22,7 @@ function letterToResourceResponse(letter: LetterBase) { groupId: letter.groupId, ...(letter.reasonCode != null && { reasonCode: letter.reasonCode }), ...(letter.reasonText != null && { reasonText: letter.reasonText }), + ...(letter.sha256Hash != null && { sha256Hash: letter.sha256Hash }), }, }; } diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index a38bbf013..fa49adb05 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -132,6 +132,7 @@ function mapToInsertLetter( upsertRequest.data.templateId, ), url: upsertRequest.data.url, + sha256Hash: upsertRequest.data.sha256Hash, source: upsertRequest.source, subject: upsertRequest.subject, createdAt: now, diff --git a/scripts/utilities/letter-test-data/src/__test__/helpers/s3-helpers.test.ts b/scripts/utilities/letter-test-data/src/__test__/helpers/s3-helpers.test.ts index 22e56cd54..93e996fbf 100644 --- a/scripts/utilities/letter-test-data/src/__test__/helpers/s3-helpers.test.ts +++ b/scripts/utilities/letter-test-data/src/__test__/helpers/s3-helpers.test.ts @@ -64,6 +64,10 @@ describe("uploadFile", () => { Key: `${supplierId}/${targetFilename}`, Body: Buffer.from("fake-pdf-bytes"), ContentType: "application/pdf", + Metadata: { + sha256Hash: + "50af8d443ccf8b2777b72a9169cd0665ef4be5335b8f53543556fa0d320b135b", + }, }); }); diff --git a/scripts/utilities/letter-test-data/src/helpers/s3-helpers.ts b/scripts/utilities/letter-test-data/src/helpers/s3-helpers.ts index 3f25a5c79..58024ba76 100644 --- a/scripts/utilities/letter-test-data/src/helpers/s3-helpers.ts +++ b/scripts/utilities/letter-test-data/src/helpers/s3-helpers.ts @@ -1,4 +1,5 @@ import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; +import { createHash } from "crypto"; import { readFileSync } from "node:fs"; import path from "node:path"; @@ -12,12 +13,16 @@ export default async function uploadFile( const s3 = new S3Client(); const filePath = path.join(__dirname, "..", "test-letters", sourceFilename); const fileContent = readFileSync(filePath); + const hash = createHash("sha256").update(fileContent).digest("hex"); const uploadParams = { Bucket: bucketName, Key: `${folder}/${targetFilename}`, Body: fileContent, ContentType: "application/pdf", + Metadata: { + sha256Hash: hash, + }, }; const command = new PutObjectCommand(uploadParams); From b95e224bbb06303a1d8ac6203726b6ce53131187 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 4 Jun 2026 12:22:05 +0100 Subject: [PATCH 2/9] use node:crypto instead of crypto --- scripts/utilities/letter-test-data/src/helpers/s3-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utilities/letter-test-data/src/helpers/s3-helpers.ts b/scripts/utilities/letter-test-data/src/helpers/s3-helpers.ts index 58024ba76..33f7104e1 100644 --- a/scripts/utilities/letter-test-data/src/helpers/s3-helpers.ts +++ b/scripts/utilities/letter-test-data/src/helpers/s3-helpers.ts @@ -1,5 +1,5 @@ import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; -import { createHash } from "crypto"; +import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import path from "node:path"; From 6e0b2efaf7915fc08dd56c29f991efa182e9afe9 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 4 Jun 2026 12:35:30 +0100 Subject: [PATCH 3/9] remove blank line in object --- internal/event-builders/src/letter-mapper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/event-builders/src/letter-mapper.ts b/internal/event-builders/src/letter-mapper.ts index 27083b1b3..2367045c7 100644 --- a/internal/event-builders/src/letter-mapper.ts +++ b/internal/event-builders/src/letter-mapper.ts @@ -19,7 +19,6 @@ export function mapLetterToCloudEvent( dataschemaversion, source, subject: `letter-origin/letter-rendering/letter/${letter.id}`, - data: { domainId: letter.id as LetterStatusChangeEvent["data"]["domainId"], status: letter.status, From 1a2361baf989072b557b84c899ee39646d288e34 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 4 Jun 2026 12:44:59 +0100 Subject: [PATCH 4/9] temporarily see if Omit fixes test --- internal/event-builders/src/letter-mapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/event-builders/src/letter-mapper.ts b/internal/event-builders/src/letter-mapper.ts index 2367045c7..5925863b7 100644 --- a/internal/event-builders/src/letter-mapper.ts +++ b/internal/event-builders/src/letter-mapper.ts @@ -5,7 +5,7 @@ import { LetterStatusChangeEvent } from "@nhsdigital/nhs-notify-event-schemas-su // eslint-disable-next-line import-x/prefer-default-export export function mapLetterToCloudEvent( - letter: Letter, + letter: Omit, // check to see if need to change schema outside of this repo source: string, ): LetterStatusChangeEvent { const eventId = randomUUID(); From d353991045bd7d10b6da32aecb6f66728c3dbd7f Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 4 Jun 2026 13:50:24 +0100 Subject: [PATCH 5/9] removed temporary omit --- internal/event-builders/src/letter-mapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/event-builders/src/letter-mapper.ts b/internal/event-builders/src/letter-mapper.ts index 5925863b7..2367045c7 100644 --- a/internal/event-builders/src/letter-mapper.ts +++ b/internal/event-builders/src/letter-mapper.ts @@ -5,7 +5,7 @@ import { LetterStatusChangeEvent } from "@nhsdigital/nhs-notify-event-schemas-su // eslint-disable-next-line import-x/prefer-default-export export function mapLetterToCloudEvent( - letter: Omit, // check to see if need to change schema outside of this repo + letter: Letter, source: string, ): LetterStatusChangeEvent { const eventId = randomUUID(); From a76506a3dc6318f280a8ca9d0f133110db68bb36 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Fri, 5 Jun 2026 09:50:21 +0100 Subject: [PATCH 6/9] add sha256 test --- .../mappers/__tests__/letter-mapper.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts index d6440eee5..a71841313 100644 --- a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts +++ b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts @@ -120,6 +120,41 @@ describe("letter-mapper", () => { }); }); + it("maps an internal Letter to a GetLetterResponse with sha256Hash when present", () => { + const date = new Date().toISOString(); + const letter: Letter = { + id: "abc123", + status: "PENDING", + supplierId: "supplier1", + specificationId: "spec123", + billingRef: "spec123", + groupId: "group123", + url: "https://example.com/letter/abc123", + createdAt: date, + updatedAt: date, + supplierStatus: "supplier1#PENDING", + supplierStatusSk: date, + ttl: 123, + source: "/data-plane/letter-rendering/pdf", + subject: "letter-rendering/source/letter/letter-id", + specificationBillingId: "billing123", + }; + + const result: GetLetterResponse = mapToGetLetterResponse(letter); + + expect(result).toEqual({ + data: { + id: "abc123", + type: "Letter", + attributes: { + specificationId: "spec123", + status: "PENDING", + groupId: "group123", + }, + }, + }); + }); + it("maps an internal Letter to a GetLetterResponse with reasonCode and reasonText when present", () => { const date = new Date().toISOString(); const letter: Letter = { From 9253cab0bd337361d11efe8c7885008990c2930a Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Fri, 5 Jun 2026 10:14:23 +0100 Subject: [PATCH 7/9] add sha256hash to test --- lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts index a71841313..4be82c53e 100644 --- a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts +++ b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts @@ -135,6 +135,7 @@ describe("letter-mapper", () => { supplierStatus: "supplier1#PENDING", supplierStatusSk: date, ttl: 123, + sha256Hash: "abc123hash", source: "/data-plane/letter-rendering/pdf", subject: "letter-rendering/source/letter/letter-id", specificationBillingId: "billing123", @@ -150,6 +151,7 @@ describe("letter-mapper", () => { specificationId: "spec123", status: "PENDING", groupId: "group123", + sha256Hash: "abc123hash", }, }, }); From 27c745fc61bef18f3c26dadc17376e40c4a7bf3e Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Fri, 5 Jun 2026 11:38:08 +0100 Subject: [PATCH 8/9] add sha256Hash to the getLetters endpoint --- lambdas/api-handler/src/contracts/letters.ts | 1 + .../mappers/__tests__/letter-mapper.test.ts | 53 +++++++++++++++++++ .../api-handler/src/mappers/letter-mapper.ts | 1 + 3 files changed, 55 insertions(+) diff --git a/lambdas/api-handler/src/contracts/letters.ts b/lambdas/api-handler/src/contracts/letters.ts index ad906b6f2..e53dd935f 100644 --- a/lambdas/api-handler/src/contracts/letters.ts +++ b/lambdas/api-handler/src/contracts/letters.ts @@ -67,6 +67,7 @@ export const GetLettersResponseResourceSchema = z status: LetterStatusSchema, specificationId: z.string(), groupId: z.string().optional(), + sha256Hash: z.string().optional(), }) .strict(), }) diff --git a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts index 4be82c53e..758e039d0 100644 --- a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts +++ b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts @@ -246,4 +246,57 @@ describe("letter-mapper", () => { ], }); }); + it("maps an internal Letter collection to a GetLettersResponse with sha256Hash when present", () => { + const date = new Date().toISOString(); + const letter: Letter = { + id: "abc123", + status: "PENDING", + supplierId: "supplier1", + specificationId: "spec123", + billingRef: "spec123", + groupId: "group123", + url: "https://example.com/letter/abc123", + createdAt: date, + updatedAt: date, + supplierStatus: "supplier1#PENDING", + supplierStatusSk: date, + ttl: 123, + reasonCode: "R01", + reasonText: "Reason text", + sha256Hash: "abc123hash", + source: "/data-plane/letter-rendering/pdf", + subject: "letter-rendering/source/letter/letter-id", + specificationBillingId: "billing123", + }; + + const result: GetLettersResponse = mapToGetLettersResponse([ + letter, + letter, + ]); + + expect(result).toEqual({ + data: [ + { + id: "abc123", + type: "Letter", + attributes: { + specificationId: "spec123", + status: "PENDING", + groupId: "group123", + sha256Hash: "abc123hash", + }, + }, + { + id: "abc123", + type: "Letter", + attributes: { + specificationId: "spec123", + status: "PENDING", + groupId: "group123", + sha256Hash: "abc123hash", + }, + }, + ], + }); + }); }); diff --git a/lambdas/api-handler/src/mappers/letter-mapper.ts b/lambdas/api-handler/src/mappers/letter-mapper.ts index 0db9bcca6..81403d6dd 100644 --- a/lambdas/api-handler/src/mappers/letter-mapper.ts +++ b/lambdas/api-handler/src/mappers/letter-mapper.ts @@ -35,6 +35,7 @@ function letterToGetLettersResourceResponse(letter: LetterBase) { status: letter.status, specificationId: letter.specificationId, groupId: letter.groupId, + ...(letter.sha256Hash != null && { sha256Hash: letter.sha256Hash }), }, }; } From b4729556cc4fc77db0181f81540ecb9704a2874e Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Fri, 5 Jun 2026 12:07:10 +0100 Subject: [PATCH 9/9] make sure sha256 passed across --- internal/datastore/src/types.ts | 1 + lambdas/api-handler/src/services/letter-operations.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/datastore/src/types.ts b/internal/datastore/src/types.ts index 39ece3492..26739c6c7 100644 --- a/internal/datastore/src/types.ts +++ b/internal/datastore/src/types.ts @@ -88,6 +88,7 @@ export const PendingLetterSchemaBase = z.object({ letterId: idRef(LetterSchema, "id"), specificationId: z.string(), groupId: z.string(), + sha256Hash: z.string().optional(), }); export const PendingLetterSchema = PendingLetterSchemaBase.extend({ diff --git a/lambdas/api-handler/src/services/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts index a29017f65..322bd463e 100644 --- a/lambdas/api-handler/src/services/letter-operations.ts +++ b/lambdas/api-handler/src/services/letter-operations.ts @@ -36,6 +36,7 @@ function mapPendingLetterToLetterBase(pending: PendingLetterBase): LetterBase { status: "PENDING", specificationId: pending.specificationId, groupId: pending.groupId, + sha256Hash: pending.sha256Hash, }; }