Skip to content

Commit 8a74c03

Browse files
Add notification parsing logic
1 parent 65481fd commit 8a74c03

File tree

6 files changed

+109
-25
lines changed

6 files changed

+109
-25
lines changed

infrastructure/terraform/components/api/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ No requirements.
3030
| <a name="input_project"></a> [project](#input\_project) | The name of the tfscaffold project | `string` | n/a | yes |
3131
| <a name="input_region"></a> [region](#input\_region) | The AWS Region | `string` | n/a | yes |
3232
| <a name="input_shared_infra_account_id"></a> [shared\_infra\_account\_id](#input\_shared\_infra\_account\_id) | The AWS Account ID of the shared infrastructure account | `string` | `"000000000000"` | no |
33-
| <a name="input_variant_map"></a> [variant\_map](#input\_variant\_map) | n/a | `map(object({ supplier_id = string, spec_id = string }))` | <pre>{<br/> "lv1": {<br/> "spec_id": "spec1",<br/> "supplier_id": "supplier1"<br/> },<br/> "lv2": {<br/> "spec_id": "spec2",<br/> "supplier_id": "supplier1"<br/> },<br/> "lv3": {<br/> "spec_id": "spec3",<br/> "supplier_id": "supplier2"<br/> }<br/>}</pre> | no |
33+
| <a name="input_variant_map"></a> [variant\_map](#input\_variant\_map) | n/a | `map(object({ supplier_id = string, spec_id = string }))` | <pre>{<br/> "lv1": {<br/> "specId": "spec1",<br/> "supplierId": "supplier1"<br/> },<br/> "lv2": {<br/> "specId": "spec2",<br/> "supplierId": "supplier1"<br/> },<br/> "lv3": {<br/> "specId": "spec3",<br/> "supplierId": "supplier2"<br/> }<br/>}</pre> | no |
3434
## Modules
3535

3636
| Name | Source | Version |

infrastructure/terraform/components/api/module_lambda_upsert_letter.tf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ module "upsert_letter" {
4343
variable "variant_map" {
4444
type = map(object({ supplier_id = string, spec_id = string }))
4545
default = {
46-
"lv1" = { supplier_id = "supplier1", spec_id = "spec1" },
47-
"lv2" = { supplier_id = "supplier1", spec_id = "spec2" },
48-
"lv3" = { supplier_id = "supplier2", spec_id = "spec3" }
46+
"lv1" = { supplierId = "supplier1", specId = "spec1" },
47+
"lv2" = { supplierId = "supplier1", specId = "spec2" },
48+
"lv3" = { supplierId = "supplier2", specId = "spec3" }
4949
}
5050
}
5151

lambdas/upsert-letter/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"@internal/datastore": "*",
66
"@nhsdigital/nhs-notify-event-schemas-letter-rendering": "^1.1.5",
77
"@types/aws-lambda": "^8.10.148",
8+
"aws-lambda": "^1.0.7",
89
"esbuild": "^0.24.0",
910
"pino": "^9.7.0",
1011
"zod": "^4.1.11"

lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
1-
import { SQSEvent } from "aws-lambda";
1+
import { SNSMessage, SQSEvent } from "aws-lambda";
22
import pino from "pino";
33
import { LetterRepository } from "internal/datastore/src";
44
import { LetterRequestPreparedEvent } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering";
55
import createUpsertLetterHandler from "../upsert-handler";
66
import { Deps } from "../../config/deps";
77
import { EnvVars } from "../../config/env";
88

9+
function createNotification(
10+
event: LetterRequestPreparedEvent,
11+
): Partial<SNSMessage> {
12+
return {
13+
SignatureVersion: "",
14+
Timestamp: "",
15+
Signature: "",
16+
SigningCertUrl: "",
17+
MessageId: "",
18+
Message: JSON.stringify(event),
19+
MessageAttributes: {},
20+
Type: "Notification",
21+
UnsubscribeUrl: "",
22+
TopicArn: "",
23+
Subject: "",
24+
Token: "",
25+
};
26+
}
27+
928
function createValidEvent(
1029
overrides: Partial<any> = {},
1130
): LetterRequestPreparedEvent {
@@ -74,7 +93,7 @@ describe("createUpsertLetterHandler", () => {
7493
{
7594
messageId: "msg1",
7695
receiptHandle: "rh1",
77-
body: JSON.stringify(createValidEvent()),
96+
body: JSON.stringify(createNotification(createValidEvent())),
7897
attributes: {
7998
ApproximateReceiveCount: "",
8099
SentTimestamp: "",
@@ -91,11 +110,13 @@ describe("createUpsertLetterHandler", () => {
91110
messageId: "msg2",
92111
receiptHandle: "rh2",
93112
body: JSON.stringify(
94-
createValidEvent({
95-
id: "7b9a03ca-342a-4150-b56b-989109c45614",
96-
domainId: "letter2",
97-
url: "s3://letterDataBucket/letter2.pdf",
98-
}),
113+
createNotification(
114+
createValidEvent({
115+
id: "7b9a03ca-342a-4150-b56b-989109c45614",
116+
domainId: "letter2",
117+
url: "s3://letterDataBucket/letter2.pdf",
118+
}),
119+
),
99120
),
100121
attributes: {
101122
ApproximateReceiveCount: "",
@@ -179,11 +200,13 @@ describe("createUpsertLetterHandler", () => {
179200
expect(result.batchItemFailures).toHaveLength(1);
180201
expect(result.batchItemFailures[0].itemIdentifier).toBe("bad-json");
181202

182-
expect(mockedDeps.logger.error).toHaveBeenCalled();
203+
expect((mockedDeps.logger.error as jest.Mock).mock.calls[0][1]).toBe(
204+
"Error processing upsert",
205+
);
183206
expect(mockedDeps.letterRepo.upsertLetter).not.toHaveBeenCalled();
184207
});
185208

186-
test("invalid schema produces batch failure and logs error", async () => {
209+
test("invalid notification schema produces batch failure and logs error", async () => {
187210
const evt: SQSEvent = {
188211
Records: [
189212
{
@@ -216,7 +239,51 @@ describe("createUpsertLetterHandler", () => {
216239
expect(result.batchItemFailures).toHaveLength(1);
217240
expect(result.batchItemFailures[0].itemIdentifier).toBe("bad-schema");
218241

219-
expect(mockedDeps.logger.error).toHaveBeenCalled();
242+
expect((mockedDeps.logger.error as jest.Mock).mock.calls[0][1]).toBe(
243+
"Error processing upsert",
244+
);
245+
expect(mockedDeps.letterRepo.upsertLetter).not.toHaveBeenCalled();
246+
});
247+
248+
test("invalid event schema produces batch failure and logs error", async () => {
249+
const evt: SQSEvent = {
250+
Records: [
251+
{
252+
messageId: "bad-schema",
253+
receiptHandle: "rh1",
254+
body: JSON.stringify({
255+
Type: "Notification",
256+
Message: JSON.stringify({ bad: "shape" }),
257+
}),
258+
attributes: {
259+
ApproximateReceiveCount: "",
260+
SentTimestamp: "",
261+
SenderId: "",
262+
ApproximateFirstReceiveTimestamp: "",
263+
},
264+
messageAttributes: {},
265+
md5OfBody: "",
266+
eventSource: "",
267+
eventSourceARN: "",
268+
awsRegion: "",
269+
},
270+
],
271+
};
272+
273+
const result = await createUpsertLetterHandler(mockedDeps)(
274+
evt,
275+
{} as any,
276+
{} as any,
277+
);
278+
279+
expect(result).toBeDefined();
280+
if (!result) throw new Error("expected BatchResponse, got void");
281+
expect(result.batchItemFailures).toHaveLength(1);
282+
expect(result.batchItemFailures[0].itemIdentifier).toBe("bad-schema");
283+
284+
expect((mockedDeps.logger.error as jest.Mock).mock.calls[0][1]).toBe(
285+
"Error parsing letter event in upsert",
286+
);
220287
expect(mockedDeps.letterRepo.upsertLetter).not.toHaveBeenCalled();
221288
});
222289

@@ -231,10 +298,12 @@ describe("createUpsertLetterHandler", () => {
231298
messageId: "ok-msg",
232299
receiptHandle: "rh1",
233300
body: JSON.stringify(
234-
createValidEvent({
235-
id: "7b9a03ca-342a-4150-b56b-989109c45615",
236-
data: { domainId: "ok" },
237-
}),
301+
createNotification(
302+
createValidEvent({
303+
id: "7b9a03ca-342a-4150-b56b-989109c45615",
304+
data: { domainId: "ok" },
305+
}),
306+
),
238307
),
239308
attributes: {
240309
ApproximateReceiveCount: "",
@@ -252,10 +321,12 @@ describe("createUpsertLetterHandler", () => {
252321
messageId: "fail-msg",
253322
receiptHandle: "rh2",
254323
body: JSON.stringify(
255-
createValidEvent({
256-
id: "7b9a03ca-342a-4150-b56b-989109c45616",
257-
data: { domainId: "ok" },
258-
}),
324+
createNotification(
325+
createValidEvent({
326+
id: "7b9a03ca-342a-4150-b56b-989109c45616",
327+
data: { domainId: "ok" },
328+
}),
329+
),
259330
),
260331
attributes: {
261332
ApproximateReceiveCount: "",

lambdas/upsert-letter/src/handler/upsert-handler.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { SQSBatchItemFailure, SQSEvent, SQSHandler } from "aws-lambda";
1+
import { SQSBatchItemFailure, SQSEvent, SQSHandler, SNSMessage } from "aws-lambda";
22
import { UpsertLetter } from "@internal/datastore";
33
import {
44
$LetterRequestPreparedEvent,
55
LetterRequestPreparedEvent,
66
} from "@nhsdigital/nhs-notify-event-schemas-letter-rendering";
77
import { ZodError } from "zod";
88
import { Deps } from "../config/deps";
9+
import { no } from "zod/v4/locales";
910

1011
type SupplierSpec = { supplierId: string; specId: string };
1112

@@ -41,8 +42,18 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler {
4142

4243
const tasks = event.Records.map(async (record) => {
4344
try {
45+
const notification = JSON.parse(record.body) as Partial<SNSMessage>;
46+
if (
47+
notification.Type !== "Notification" ||
48+
typeof notification.Message !== "string"
49+
) {
50+
throw new Error(
51+
"SQS record does not contain SNS Notification with string Message",
52+
);
53+
}
54+
4455
const upsertRequest: LetterRequestPreparedEvent =
45-
$LetterRequestPreparedEvent.parse(JSON.parse(record.body));
56+
$LetterRequestPreparedEvent.parse(JSON.parse(notification.Message));
4657

4758
const supplierSpec: SupplierSpec = resolveSupplierForVariant(
4859
upsertRequest.data.letterVariantId,
@@ -58,7 +69,7 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler {
5869
} catch (error) {
5970
if (error instanceof ZodError) {
6071
deps.logger.error(
61-
{ issues: error.issues },
72+
{ issues: error.issues, body: record.body },
6273
"Error parsing letter event in upsert",
6374
);
6475
}

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)