diff --git a/internal/datastore/src/__test__/letter-queue-repository.test.ts b/internal/datastore/src/__test__/letter-queue-repository.test.ts index 74812b010..bcac9c20b 100644 --- a/internal/datastore/src/__test__/letter-queue-repository.test.ts +++ b/internal/datastore/src/__test__/letter-queue-repository.test.ts @@ -21,6 +21,7 @@ function createLetter( specificationId: "specification1", groupId: "group1", priority: 10, + sha256Hash: "hash1", ...overrides, }; } diff --git a/internal/datastore/src/types.ts b/internal/datastore/src/types.ts index d6a4b0be9..310196d1b 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({ @@ -87,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/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, diff --git a/lambdas/api-handler/src/contracts/letters.ts b/lambdas/api-handler/src/contracts/letters.ts index 0c3d14182..e53dd935f 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(), }) @@ -66,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 d6440eee5..758e039d0 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,43 @@ 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, + sha256Hash: "abc123hash", + 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", + sha256Hash: "abc123hash", + }, + }, + }); + }); + it("maps an internal Letter to a GetLetterResponse with reasonCode and reasonText when present", () => { const date = new Date().toISOString(); const letter: Letter = { @@ -209,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 c31d61b34..81403d6dd 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 }), }, }; } @@ -34,6 +35,7 @@ function letterToGetLettersResourceResponse(letter: LetterBase) { status: letter.status, specificationId: letter.specificationId, groupId: letter.groupId, + ...(letter.sha256Hash != null && { sha256Hash: letter.sha256Hash }), }, }; } 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, }; } diff --git a/lambdas/update-letter-queue/src/update-letter-queue.ts b/lambdas/update-letter-queue/src/update-letter-queue.ts index 04ed963ff..cc91a0079 100644 --- a/lambdas/update-letter-queue/src/update-letter-queue.ts +++ b/lambdas/update-letter-queue/src/update-letter-queue.ts @@ -191,6 +191,7 @@ function mapLetterToPendingLetter(letter: Letter): InsertPendingLetter { specificationId: letter.specificationId, groupId: letter.groupId, priority: letter.priority, + 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/sandbox/data/examples/getLetter/responses/getLetter-24L5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getLetter/responses/getLetter-24L5eYSWGzCHlGmzNxuqVusPxDg.json index f72687b47..92bb25f1f 100644 --- a/sandbox/data/examples/getLetter/responses/getLetter-24L5eYSWGzCHlGmzNxuqVusPxDg.json +++ b/sandbox/data/examples/getLetter/responses/getLetter-24L5eYSWGzCHlGmzNxuqVusPxDg.json @@ -2,6 +2,7 @@ "data": { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "PENDING" }, diff --git a/sandbox/data/examples/getLetter/responses/getLetter-2AL5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getLetter/responses/getLetter-2AL5eYSWGzCHlGmzNxuqVusPxDg.json index 905df7328..bbc41e7b9 100644 --- a/sandbox/data/examples/getLetter/responses/getLetter-2AL5eYSWGzCHlGmzNxuqVusPxDg.json +++ b/sandbox/data/examples/getLetter/responses/getLetter-2AL5eYSWGzCHlGmzNxuqVusPxDg.json @@ -2,6 +2,7 @@ "data": { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "ACCEPTED" }, diff --git a/sandbox/data/examples/getLetter/responses/getLetter-2BL5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getLetter/responses/getLetter-2BL5eYSWGzCHlGmzNxuqVusPxDg.json index ae4f8d46f..ae3d1356b 100644 --- a/sandbox/data/examples/getLetter/responses/getLetter-2BL5eYSWGzCHlGmzNxuqVusPxDg.json +++ b/sandbox/data/examples/getLetter/responses/getLetter-2BL5eYSWGzCHlGmzNxuqVusPxDg.json @@ -2,6 +2,7 @@ "data": { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "PRINTED" }, diff --git a/sandbox/data/examples/getLetter/responses/getLetter-2CL5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getLetter/responses/getLetter-2CL5eYSWGzCHlGmzNxuqVusPxDg.json index afa79b734..401df416a 100644 --- a/sandbox/data/examples/getLetter/responses/getLetter-2CL5eYSWGzCHlGmzNxuqVusPxDg.json +++ b/sandbox/data/examples/getLetter/responses/getLetter-2CL5eYSWGzCHlGmzNxuqVusPxDg.json @@ -2,6 +2,7 @@ "data": { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "ENCLOSED" }, diff --git a/sandbox/data/examples/getLetter/responses/getLetter-2DL5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getLetter/responses/getLetter-2DL5eYSWGzCHlGmzNxuqVusPxDg.json index 9f5223d70..bdd71096d 100644 --- a/sandbox/data/examples/getLetter/responses/getLetter-2DL5eYSWGzCHlGmzNxuqVusPxDg.json +++ b/sandbox/data/examples/getLetter/responses/getLetter-2DL5eYSWGzCHlGmzNxuqVusPxDg.json @@ -2,6 +2,7 @@ "data": { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "DISPATCHED" }, diff --git a/sandbox/data/examples/getLetter/responses/getLetter-2EL5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getLetter/responses/getLetter-2EL5eYSWGzCHlGmzNxuqVusPxDg.json index ded23e7a1..672e13cad 100644 --- a/sandbox/data/examples/getLetter/responses/getLetter-2EL5eYSWGzCHlGmzNxuqVusPxDg.json +++ b/sandbox/data/examples/getLetter/responses/getLetter-2EL5eYSWGzCHlGmzNxuqVusPxDg.json @@ -2,6 +2,7 @@ "data": { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "DELIVERED" }, diff --git a/sandbox/data/examples/getLetter/responses/getLetter-2WL5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getLetter/responses/getLetter-2WL5eYSWGzCHlGmzNxuqVusPxDg.json index ec685a98b..c9184b3cd 100644 --- a/sandbox/data/examples/getLetter/responses/getLetter-2WL5eYSWGzCHlGmzNxuqVusPxDg.json +++ b/sandbox/data/examples/getLetter/responses/getLetter-2WL5eYSWGzCHlGmzNxuqVusPxDg.json @@ -4,6 +4,7 @@ "groupId": "c5d93f917f5546d08beccf770a915d96", "reasonCode": "R01", "reasonText": "failed validation", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "REJECTED" }, diff --git a/sandbox/data/examples/getLetter/responses/getLetter-2XL5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getLetter/responses/getLetter-2XL5eYSWGzCHlGmzNxuqVusPxDg.json index d37729d62..461303361 100644 --- a/sandbox/data/examples/getLetter/responses/getLetter-2XL5eYSWGzCHlGmzNxuqVusPxDg.json +++ b/sandbox/data/examples/getLetter/responses/getLetter-2XL5eYSWGzCHlGmzNxuqVusPxDg.json @@ -3,6 +3,7 @@ "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", "reasonCode": "R01", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "CANCELLED" }, diff --git a/sandbox/data/examples/getLetter/responses/getLetter-2YL5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getLetter/responses/getLetter-2YL5eYSWGzCHlGmzNxuqVusPxDg.json index 945e8270b..631871f95 100644 --- a/sandbox/data/examples/getLetter/responses/getLetter-2YL5eYSWGzCHlGmzNxuqVusPxDg.json +++ b/sandbox/data/examples/getLetter/responses/getLetter-2YL5eYSWGzCHlGmzNxuqVusPxDg.json @@ -4,6 +4,7 @@ "groupId": "c5d93f917f5546d08beccf770a915d96", "reasonCode": "R01", "reasonText": "failed validation", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "FAILED" }, diff --git a/sandbox/data/examples/getLetter/responses/getLetter-2ZL5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getLetter/responses/getLetter-2ZL5eYSWGzCHlGmzNxuqVusPxDg.json index fff8bf1dd..d12605718 100644 --- a/sandbox/data/examples/getLetter/responses/getLetter-2ZL5eYSWGzCHlGmzNxuqVusPxDg.json +++ b/sandbox/data/examples/getLetter/responses/getLetter-2ZL5eYSWGzCHlGmzNxuqVusPxDg.json @@ -4,6 +4,7 @@ "groupId": "c5d93f917f5546d08beccf770a915d96", "reasonCode": "R01", "reasonText": "failed validation", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "RETURNED" }, diff --git a/sandbox/data/examples/getLetters/responses/getLetters_pending.json b/sandbox/data/examples/getLetters/responses/getLetters_pending.json index c3e72d3b6..20a3ee480 100644 --- a/sandbox/data/examples/getLetters/responses/getLetters_pending.json +++ b/sandbox/data/examples/getLetters/responses/getLetters_pending.json @@ -3,6 +3,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "PENDING" }, @@ -12,6 +13,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "PENDING" }, @@ -21,6 +23,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "PENDING" }, @@ -30,6 +33,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "PENDING" }, @@ -39,6 +43,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "PENDING" }, @@ -48,6 +53,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "PENDING" }, @@ -57,6 +63,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", "status": "PENDING" }, @@ -66,6 +73,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "853817f0-d797-4eb2-bf40-2879a9b58117", "status": "PENDING" }, @@ -75,6 +83,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "9e5d5da7-5991-4ac9-9cee-559769912ca5", "status": "PENDING" }, @@ -84,6 +93,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -93,6 +103,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -102,6 +113,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -111,6 +123,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -120,6 +133,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -129,6 +143,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -138,6 +153,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -147,6 +163,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -156,6 +173,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -165,6 +183,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -174,6 +193,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -183,6 +203,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -192,6 +213,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, @@ -201,6 +223,7 @@ { "attributes": { "groupId": "c5d93f917f5546d08beccf770a915d96", + "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "specificationId": "634bcc87-fcc3-41fe-b6f9-c6a2125758e6", "status": "PENDING" }, diff --git a/scripts/utilities/letter-test-data/src/__test__/helpers/create-letter-helpers.test.ts b/scripts/utilities/letter-test-data/src/__test__/helpers/create-letter-helpers.test.ts index 66af9c01a..66bedc1b9 100644 --- a/scripts/utilities/letter-test-data/src/__test__/helpers/create-letter-helpers.test.ts +++ b/scripts/utilities/letter-test-data/src/__test__/helpers/create-letter-helpers.test.ts @@ -33,6 +33,8 @@ describe("Create letter helpers", () => { const status = "PENDING" as LetterStatusType; const testLetter = "test-letter-standard"; + (uploadFile as jest.Mock).mockResolvedValue({ hash: "abc123" }); + await createLetter({ letterId, bucketName, @@ -65,6 +67,7 @@ describe("Create letter helpers", () => { subject: "supplier-api/letter-test-data/letterId", billingRef: "specificationId", specificationBillingId: "billingId", + sha256Hash: "abc123", }); }); @@ -116,6 +119,7 @@ describe("Create letter helpers", () => { source: "/data-plane/letter-rendering/letter-test-data", subject: "supplier-api/letter-test-data/letterId", specificationBillingId: "billingId", + sha256Hash: undefined, }); }); 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/cli/index.ts b/scripts/utilities/letter-test-data/src/cli/index.ts index 0b6c32371..0d8f01910 100644 --- a/scripts/utilities/letter-test-data/src/cli/index.ts +++ b/scripts/utilities/letter-test-data/src/cli/index.ts @@ -192,21 +192,22 @@ async function main() { const letterRepository = createLetterRepository(environment, ttlHours); const { count } = argv; const { testLetter } = argv; - // Setup file attributes const bucketName = `nhs-${argv.awsAccountId}-eu-west-2-${argv.environment}-supapi-test-letters`; const targetFilename = `${batchId}-${status}.pdf`; const folder = `${supplierId}/${batchId}`; const url = `s3://${bucketName}/${folder}/${targetFilename}`; + let hash: string | undefined; // Upload a test file for this batch if it is not an 'none' batch if (testLetter !== "none") { - await uploadFile( + const result = await uploadFile( bucketName, folder, `${testLetter}.pdf`, targetFilename, ); + hash = result.hash; } // Create letter DTOs @@ -221,6 +222,7 @@ async function main() { billingId, status: status as LetterStatusType, url, + sha256Hash: hash, }), ); } diff --git a/scripts/utilities/letter-test-data/src/helpers/create-letter-helpers.ts b/scripts/utilities/letter-test-data/src/helpers/create-letter-helpers.ts index 6278dbbe7..c078052de 100644 --- a/scripts/utilities/letter-test-data/src/helpers/create-letter-helpers.ts +++ b/scripts/utilities/letter-test-data/src/helpers/create-letter-helpers.ts @@ -17,6 +17,7 @@ export async function createLetter(params: { letterRepository: LetterRepository; testLetter: string; }) { + let hash: string | undefined; const { billingId, bucketName, @@ -31,12 +32,13 @@ export async function createLetter(params: { } = params; if (testLetter !== "none") { - await uploadFile( + const result = await uploadFile( bucketName, supplierId, `${testLetter}.pdf`, targetFilename, ); + hash = result.hash; } const letter: Omit = { @@ -52,6 +54,7 @@ export async function createLetter(params: { subject: `supplier-api/letter-test-data/${letterId}`, billingRef: specificationId, specificationBillingId: billingId, + sha256Hash: hash, }; const letterRecord = await letterRepository.putLetter(letter); @@ -66,11 +69,13 @@ export function createLetterDto(params: { groupId: string; status: LetterStatusType; url: string; + sha256Hash?: string; }) { const { billingId, groupId, letterId, + sha256Hash, specificationId, status, supplierId, @@ -83,6 +88,7 @@ export function createLetterDto(params: { specificationId, groupId, url, + sha256Hash, status, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), 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..735d47e55 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 "node:crypto"; import { readFileSync } from "node:fs"; import path from "node:path"; @@ -12,16 +13,21 @@ 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); - return await s3.send(command); + const commandResult = await s3.send(command); + return { commandResult, hash }; } catch (error) { console.error("Error uploading file:", error); throw error; diff --git a/specification/api/components/schemas/letterItem.yml b/specification/api/components/schemas/letterItem.yml index a62bd6fdb..a58037b6c 100644 --- a/specification/api/components/schemas/letterItem.yml +++ b/specification/api/components/schemas/letterItem.yml @@ -24,3 +24,5 @@ properties: $ref: "./reasonCode.yml" reasonText: $ref: "./reasonText.yml" + sha256Hash: + $ref: "./sha256Hash.yml" diff --git a/specification/api/components/schemas/sha256Hash.yml b/specification/api/components/schemas/sha256Hash.yml new file mode 100644 index 000000000..a70c5e3c8 --- /dev/null +++ b/specification/api/components/schemas/sha256Hash.yml @@ -0,0 +1,3 @@ +type: string +description: Sha 256 hash of the letter content +example: 2WL5eYSWGzCHlGmzNxuqVusPxDg diff --git a/tests/component-tests/apiGateway-tests/get-letter-status.spec.ts b/tests/component-tests/apiGateway-tests/get-letter-status.spec.ts index 1e893ef24..33faf7eed 100644 --- a/tests/component-tests/apiGateway-tests/get-letter-status.spec.ts +++ b/tests/component-tests/apiGateway-tests/get-letter-status.spec.ts @@ -39,6 +39,7 @@ test.describe("API Gateway Tests to Verify Get Letter Status Endpoint", () => { expect(responseBody).toMatchObject({ data: { attributes: { + sha256Hash: createdLetter.sha256Hash, status: "PENDING", specificationId: createdLetter.specificationId, groupId: createdLetter.groupId, diff --git a/tests/component-tests/apiGateway-tests/get-letters.spec.ts b/tests/component-tests/apiGateway-tests/get-letters.spec.ts index b48544916..bcdd695fc 100644 --- a/tests/component-tests/apiGateway-tests/get-letters.spec.ts +++ b/tests/component-tests/apiGateway-tests/get-letters.spec.ts @@ -10,6 +10,7 @@ import { isErrorResponse, isGetLettersResponse, } from "../../helpers/generate-fetch-test-data"; +import { SUPPLIER_LETTERS } from "../../constants/api-constants"; let baseUrl: string; @@ -34,6 +35,39 @@ test.describe("API Gateway Tests To Get List Of Pending Letters", () => { throw new Error("Expected GetLettersResponse body for 200 status"); } expect(responseBody.data.length).toBeGreaterThanOrEqual(1); + + for (const letter of responseBody.data) { + expect(letter.attributes.sha256Hash).toBeDefined(); + expect(letter.attributes.sha256Hash).not.toBeNull(); + } + }); + + test("GET /letters and GET /letter{id} sha256 match", async ({ request }) => { + const headers = createValidRequestHeaders(); + const { responseBody, statusCode } = await getLettersWithRetry( + request, + baseUrl, + headers, + { + lettersLimit: "2", + }, + ); + + expect(statusCode).toBe(200); + if (!isGetLettersResponse(responseBody)) { + throw new Error("Expected GetLettersResponse body for 200 status"); + } + expect(responseBody.data.length).toBeGreaterThanOrEqual(1); + const getLettersSha = responseBody.data[0].attributes.sha256Hash; + const response = await request.get( + `${baseUrl}/${SUPPLIER_LETTERS}/${responseBody.data[0].id}`, + { + headers, + }, + ); + const letterResponseBody = await response.json(); + expect(response.status()).toBe(200); + expect(letterResponseBody.data.attributes.sha256Hash).toBe(getLettersSha); }); test("GET /letters with invalid authentication should return 403", async ({ diff --git a/tests/e2e-tests/api/letters/test_get_letter_status.py b/tests/e2e-tests/api/letters/test_get_letter_status.py index 18e9d97ae..d3e0a72f8 100644 --- a/tests/e2e-tests/api/letters/test_get_letter_status.py +++ b/tests/e2e-tests/api/letters/test_get_letter_status.py @@ -1,3 +1,4 @@ +import hashlib import requests import pytest from lib.fixtures import * # NOSONAR diff --git a/tests/helpers/generate-fetch-test-data.ts b/tests/helpers/generate-fetch-test-data.ts index 6ef919c3f..412149732 100644 --- a/tests/helpers/generate-fetch-test-data.ts +++ b/tests/helpers/generate-fetch-test-data.ts @@ -225,7 +225,7 @@ export async function waitForLetterStatus( TableName: LETTERSTABLENAME, Key: { id, supplierId }, ProjectionExpression: - "id, #status, supplierId, specificationId, groupId, reasonCode, reasonText", + "id, #status, supplierId, specificationId, groupId, reasonCode, reasonText, sha256Hash", ExpressionAttributeNames: { "#status": "status", },