diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md index b21ae6675..691e4db76 100644 --- a/infrastructure/terraform/components/api/README.md +++ b/infrastructure/terraform/components/api/README.md @@ -60,9 +60,9 @@ No requirements. | Name | Source | Version | | ---- | ------ | ------- | -| [amendment\_event\_transformer](#module\_amendment\_event\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | +| [amendment\_event\_transformer](#module\_amendment\_event\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | | [amendments\_queue](#module\_amendments\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.5/terraform-sqs.zip | n/a | -| [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | +| [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | | [ddb\_alarms\_idempotency](#module\_ddb\_alarms\_idempotency) | ../../modules/alarms-ddb | n/a | | [ddb\_alarms\_letter\_queue](#module\_ddb\_alarms\_letter\_queue) | ../../modules/alarms-ddb | n/a | | [ddb\_alarms\_letters](#module\_ddb\_alarms\_letters) | ../../modules/alarms-ddb | n/a | @@ -71,30 +71,30 @@ No requirements. | [domain\_truststore](#module\_domain\_truststore) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-s3bucket.zip | n/a | | [eventpub](#module\_eventpub) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/5.0.6/terraform-eventpub.zip | n/a | | [eventsub](#module\_eventsub) | ../../modules/eventsub | n/a | -| [get\_letter](#module\_get\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | -| [get\_letter\_data](#module\_get\_letter\_data) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | -| [get\_letters](#module\_get\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | -| [get\_mi](#module\_get\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | -| [get\_status](#module\_get\_status) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | +| [get\_letter](#module\_get\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [get\_letter\_data](#module\_get\_letter\_data) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [get\_letters](#module\_get\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [get\_mi](#module\_get\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [get\_status](#module\_get\_status) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | | [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-kms.zip | n/a | | [lambda\_alarms](#module\_lambda\_alarms) | ../../modules/alarms-lambda | n/a | | [letter\_status\_updates\_queue](#module\_letter\_status\_updates\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.5/terraform-sqs.zip | n/a | -| [letter\_updates\_transformer](#module\_letter\_updates\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | -| [mi\_updates\_transformer](#module\_mi\_updates\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | -| [patch\_letter](#module\_patch\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | -| [post\_letters](#module\_post\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | -| [post\_mi](#module\_post\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | +| [letter\_updates\_transformer](#module\_letter\_updates\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [mi\_updates\_transformer](#module\_mi\_updates\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [patch\_letter](#module\_patch\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [post\_letters](#module\_post\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [post\_mi](#module\_post\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | | [s3bucket\_test\_letters](#module\_s3bucket\_test\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-s3bucket.zip | n/a | | [sqs\_alarms](#module\_sqs\_alarms) | ../../modules/alarms-sqs | n/a | | [sqs\_letter\_updates](#module\_sqs\_letter\_updates) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.5/terraform-sqs.zip | n/a | | [sqs\_supplier\_allocator](#module\_sqs\_supplier\_allocator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.5/terraform-sqs.zip | n/a | | [sqs\_supplier\_config](#module\_sqs\_supplier\_config) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.5/terraform-sqs.zip | n/a | -| [supplier\_allocator](#module\_supplier\_allocator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | -| [supplier\_config\_ingress](#module\_supplier\_config\_ingress) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | -| [supplier\_mock](#module\_supplier\_mock) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.5/terraform-lambda.zip | n/a | +| [supplier\_allocator](#module\_supplier\_allocator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [supplier\_config\_ingress](#module\_supplier\_config\_ingress) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [supplier\_mock](#module\_supplier\_mock) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | | [supplier\_ssl](#module\_supplier\_ssl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-ssl.zip | n/a | -| [update\_letter\_queue](#module\_update\_letter\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | -| [upsert\_letter](#module\_upsert\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.2/terraform-lambda.zip | n/a | +| [update\_letter\_queue](#module\_update\_letter\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | +| [upsert\_letter](#module\_upsert\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/6.0.5/terraform-lambda.zip | n/a | ## Outputs | Name | Description | diff --git a/internal/events/package.json b/internal/events/package.json index ca6177c5a..28cc9ffa2 100644 --- a/internal/events/package.json +++ b/internal/events/package.json @@ -36,5 +36,5 @@ "typecheck": "tsc --noEmit" }, "types": "dist/index.d.ts", - "version": "1.0.22" + "version": "1.0.23" } diff --git a/internal/events/schemas/examples/letter.ACCEPTED.json b/internal/events/schemas/examples/letter.ACCEPTED.json index 62ea85bb9..e113c8151 100644 --- a/internal/events/schemas/examples/letter.ACCEPTED.json +++ b/internal/events/schemas/examples/letter.ACCEPTED.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/events/schemas/examples/letter.CANCELLED.json b/internal/events/schemas/examples/letter.CANCELLED.json index 95c7638d7..87a569d81 100644 --- a/internal/events/schemas/examples/letter.CANCELLED.json +++ b/internal/events/schemas/examples/letter.CANCELLED.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/events/schemas/examples/letter.DELIVERED.json b/internal/events/schemas/examples/letter.DELIVERED.json index a15fd21e9..d9acbc605 100644 --- a/internal/events/schemas/examples/letter.DELIVERED.json +++ b/internal/events/schemas/examples/letter.DELIVERED.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/events/schemas/examples/letter.DISPATCHED.json b/internal/events/schemas/examples/letter.DISPATCHED.json index 782ee5c15..da1b68edc 100644 --- a/internal/events/schemas/examples/letter.DISPATCHED.json +++ b/internal/events/schemas/examples/letter.DISPATCHED.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/events/schemas/examples/letter.ENCLOSED.json b/internal/events/schemas/examples/letter.ENCLOSED.json index 610d157da..4d9aad83b 100644 --- a/internal/events/schemas/examples/letter.ENCLOSED.json +++ b/internal/events/schemas/examples/letter.ENCLOSED.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/events/schemas/examples/letter.FAILED.json b/internal/events/schemas/examples/letter.FAILED.json index 9ef34aa14..5bdd1b841 100644 --- a/internal/events/schemas/examples/letter.FAILED.json +++ b/internal/events/schemas/examples/letter.FAILED.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/events/schemas/examples/letter.FORWARDED.json b/internal/events/schemas/examples/letter.FORWARDED.json index bc1f48cf3..2bbe69606 100644 --- a/internal/events/schemas/examples/letter.FORWARDED.json +++ b/internal/events/schemas/examples/letter.FORWARDED.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/events/schemas/examples/letter.PENDING.json b/internal/events/schemas/examples/letter.PENDING.json index c5c0fe697..798b71d23 100644 --- a/internal/events/schemas/examples/letter.PENDING.json +++ b/internal/events/schemas/examples/letter.PENDING.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/events/schemas/examples/letter.PRINTED.json b/internal/events/schemas/examples/letter.PRINTED.json index 9c8cf131d..f1c6412a3 100644 --- a/internal/events/schemas/examples/letter.PRINTED.json +++ b/internal/events/schemas/examples/letter.PRINTED.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/events/schemas/examples/letter.REJECTED.json b/internal/events/schemas/examples/letter.REJECTED.json index d8fee1b71..d608c8115 100644 --- a/internal/events/schemas/examples/letter.REJECTED.json +++ b/internal/events/schemas/examples/letter.REJECTED.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/events/schemas/examples/letter.RETURNED.json b/internal/events/schemas/examples/letter.RETURNED.json index 7b7121d8f..9396b8a36 100644 --- a/internal/events/schemas/examples/letter.RETURNED.json +++ b/internal/events/schemas/examples/letter.RETURNED.json @@ -2,7 +2,7 @@ "data": { "billingRef": "1y3q9v1zzzz", "domainId": "e9dc9ee9-b2f8-4ef2-8351-73a9a29f8e96_38U220Ghb1WXF8hCK6hL0LGya3Y_38U23YjtnKSZypvaCSb07adMOlq", - "groupId": "client_template", + "groupId": "client-campaign-template", "origin": { "domain": "letter-rendering", "event": "f47ac10b-58cc-4372-a567-0e02b2c3d479", diff --git a/internal/helpers/src/group-id.ts b/internal/helpers/src/group-id.ts index a5a6cff2a..0fc9e8d85 100644 --- a/internal/helpers/src/group-id.ts +++ b/internal/helpers/src/group-id.ts @@ -3,5 +3,5 @@ export default function formatGroupId( campaignId = "unknown", safeTemplateId = "unknown", ): string { - return `${clientId}_${campaignId}_${safeTemplateId}`; + return `${clientId}-${campaignId}-${safeTemplateId}`; } diff --git a/internal/helpers/src/metrics.ts b/internal/helpers/src/metrics.ts index 1b3929b4e..74ed3622a 100644 --- a/internal/helpers/src/metrics.ts +++ b/internal/helpers/src/metrics.ts @@ -35,15 +35,13 @@ export function buildEMFObject( ) { const namespace = process.env.AWS_LAMBDA_FUNCTION_NAME || functionName; return { - LogGroup: namespace, - ServiceName: namespace, ...dimensions, _aws: { Timestamp: Date.now(), CloudWatchMetrics: [ { Namespace: namespace, - Dimensions: [[...Object.keys(dimensions), "ServiceName", "LogGroup"]], + Dimensions: [Object.keys(dimensions)], Metrics: [ { Name: metric.key, Value: metric.value, Unit: metric.unit }, ], diff --git a/lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts b/lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts index 1634efe21..de56c12a5 100644 --- a/lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts +++ b/lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts @@ -13,7 +13,9 @@ import { } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events"; import { MetricStatus, buildEMFObject } from "@internal/helpers"; import { Unit } from "aws-embedded-metrics"; -import createUpsertLetterHandler from "../upsert-handler"; +import createUpsertLetterHandler, { + extractDimensionsFromLetterEvent, +} from "../upsert-handler"; import { Deps } from "../../config/deps"; import { EnvVars } from "../../config/env"; import packageJson from "../../../package.json"; @@ -281,7 +283,7 @@ describe("createUpsertLetterHandler", () => { expect(insertedV2Letter.billingRef).toBe("spec1"); expect(insertedV2Letter.url).toBe("s3://letterDataBucket/letter1.pdf"); expect(insertedV2Letter.status).toBe("PENDING"); - expect(insertedV2Letter.groupId).toBe("client1_campaign1_template1"); + expect(insertedV2Letter.groupId).toBe("client1-campaign1-template1"); expect(insertedV2Letter.source).toBe("/data-plane/letter-rendering/test"); expect(insertedV2Letter.specificationBillingId).toBe("billing1"); expect(insertedV2Letter.priority).toBe(10); @@ -294,7 +296,7 @@ describe("createUpsertLetterHandler", () => { expect(insertedV1Letter.billingRef).toBe("spec2"); expect(insertedV1Letter.url).toBe("s3://letterDataBucket/letter1.pdf"); expect(insertedV1Letter.status).toBe("PENDING"); - expect(insertedV1Letter.groupId).toBe("client1_campaign1_template1"); + expect(insertedV1Letter.groupId).toBe("client1-campaign1-template1"); expect(insertedV1Letter.source).toBe("/data-plane/letter-rendering/test"); expect(insertedV1Letter.specificationBillingId).toBe("billing2"); expect(insertedV1Letter.priority).toBe(10); @@ -311,7 +313,11 @@ describe("createUpsertLetterHandler", () => { "upsertLetter", { Supplier: "supplier1", - GroupId: "client1_campaign1_template1", + GroupId: "client1-campaign1-template1", + CampaignId: "campaign1", + ClientId: "client1", + Status: "PENDING", + TemplateId: "template1", }, expect.objectContaining({ key: MetricStatus.Success, @@ -388,7 +394,7 @@ describe("createUpsertLetterHandler", () => { expect(insertedV2Letter.reasonText).toBe( "No suppliers available for allocation of V2", ); - expect(insertedV2Letter.groupId).toBe("client1_campaign1_template1"); + expect(insertedV2Letter.groupId).toBe("client1-campaign1-template1"); expect(insertedV2Letter.source).toBe("/data-plane/letter-rendering/test"); expect(insertedV2Letter.specificationBillingId).toBe("unknown"); expect(insertedV2Letter.priority).toBe(0); @@ -405,7 +411,7 @@ describe("createUpsertLetterHandler", () => { expect(insertedV1Letter.reasonText).toBe( "No suppliers available for allocation of V1", ); - expect(insertedV1Letter.groupId).toBe("client1_campaign1_template1"); + expect(insertedV1Letter.groupId).toBe("client1-campaign1-template1"); expect(insertedV1Letter.source).toBe("/data-plane/letter-rendering/test"); expect(insertedV1Letter.specificationBillingId).toBe("unknown"); expect(insertedV1Letter.priority).toBe(0); @@ -488,7 +494,11 @@ describe("createUpsertLetterHandler", () => { "upsertLetter", { Supplier: "unknown", - GroupId: "unknown", + Status: "RETURNED", + ClientId: "client_template", + CampaignId: "unknown", + GroupId: "client_template", + TemplateId: "unknown", }, expect.objectContaining({ key: MetricStatus.Success, @@ -498,6 +508,126 @@ describe("createUpsertLetterHandler", () => { ); }); + test("extracts insert dimensions without allocation details", () => { + const letterEvent = createPreparedV2Event(); + const allocationDetails = { + supplierSpec: { + supplierId: "", + specId: "spec1", + priority: 10, + billingId: "billing1", + }, + allocationStatus: { + status: "PENDING", + }, + }; + + expect( + extractDimensionsFromLetterEvent(letterEvent, allocationDetails), + ).toEqual({ + Supplier: "unknown", + Status: "PENDING", + ClientId: "client1", + CampaignId: "campaign1", + GroupId: "client1-campaign1-template1", + TemplateId: "template1", + }); + }); + + test("uses unknown update dimensions when group ID is empty or partial", async () => { + const emptyGroupEvent = createSupplierStatusChangeEvent(); + emptyGroupEvent.data.groupId = ""; + const partialGroupEvent = createSupplierStatusChangeEvent({ + id: "23f1f09c-a555-4d9b-8405-0b33490bc921", + }); + partialGroupEvent.data.groupId = "-campaign-template"; + const evt: SQSEvent = createSQSEvent([ + createSqsRecord("empty-group", JSON.stringify(emptyGroupEvent)), + createSqsRecord("partial-group", JSON.stringify(partialGroupEvent)), + ]); + + const result = await createUpsertLetterHandler(mockedDeps)( + evt, + {} as any, + {} as any, + ); + + expect(result!.batchItemFailures).toEqual([]); + expect(buildEMFObject as jest.Mock).toHaveBeenCalledWith( + "upsertLetter", + { + Supplier: "supplier1", + Status: "RETURNED", + ClientId: "unknown", + CampaignId: "unknown", + GroupId: "unknown", + TemplateId: "unknown", + }, + expect.objectContaining({ key: MetricStatus.Success }), + ); + expect(buildEMFObject as jest.Mock).toHaveBeenCalledWith( + "upsertLetter", + { + Supplier: "supplier1", + Status: "RETURNED", + ClientId: "unknown", + CampaignId: "campaign", + GroupId: "-campaign-template", + TemplateId: "template", + }, + expect.objectContaining({ key: MetricStatus.Success }), + ); + }); + + test("insert with missing client, campaign and template has metric emitted with 'unknown' dimensions", async () => { + const letterEvent = createPreparedV2Event(); + letterEvent.data.clientId = ""; + delete letterEvent.data.campaignId; + delete letterEvent.data.templateId; + const message = { + letterEvent, + allocationDetails: { + supplierSpec: { + supplierId: "supplier1", + specId: "spec1", + priority: 10, + billingId: "billing1", + }, + allocationStatus: { + status: "PENDING", + }, + }, + }; + + const evt: SQSEvent = createSQSEvent([ + createSqsRecord("missing-fields", JSON.stringify(message)), + ]); + + const result = await createUpsertLetterHandler(mockedDeps)( + evt, + {} as any, + {} as any, + ); + + expect(result!.batchItemFailures).toEqual([]); + expect(buildEMFObject as jest.Mock).toHaveBeenCalledWith( + "upsertLetter", + { + ClientId: "unknown", + CampaignId: "unknown", + TemplateId: "unknown", + Supplier: "supplier1", + GroupId: "-unknown-unknown", + Status: "PENDING", + }, + expect.objectContaining({ + key: MetricStatus.Failure, + value: 1, + unit: Unit.Count, + }), + ); + }); + test("invalid JSON produces batch failure and logs error", async () => { const evt: SQSEvent = createSQSEvent([ createSqsRecord("bad-json", "this-is-not-json"), @@ -525,7 +655,11 @@ describe("createUpsertLetterHandler", () => { "upsertLetter", { Supplier: "unknown", + Status: "unknown", + ClientId: "unknown", + CampaignId: "unknown", GroupId: "unknown", + TemplateId: "unknown", }, expect.objectContaining({ key: MetricStatus.Failure, @@ -624,7 +758,11 @@ describe("createUpsertLetterHandler", () => { "upsertLetter", { Supplier: "unknown", + Status: "unknown", + ClientId: "unknown", + CampaignId: "unknown", GroupId: "unknown", + TemplateId: "unknown", }, expect.objectContaining({ key: MetricStatus.Failure, diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index 6b5199b92..06c232987 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -31,6 +31,15 @@ import { UpsertOperation, } from "./schemas"; +interface UpsertDimensions extends Record { + Supplier: string; + Status: string; + ClientId: string; + CampaignId: string; + GroupId: string; + TemplateId: string; +} + const idempotencyConfig = new IdempotencyConfig({ eventKeyJmesPath: "id", }); @@ -48,6 +57,10 @@ function getOperationFromType(type: string): UpsertOperation { preparedRequest, allocationDetails, ); + const dimensions: UpsertDimensions = extractDimensionsFromLetterEvent( + request as LetterStatusChangeEvent | PreparedEvents, + allocationDetails, + ); try { await deps.letterRepo.putLetter(letterToInsert); @@ -60,26 +73,15 @@ function getOperationFromType(type: string): UpsertOperation { letterToInsert, }); // emit success metric - emitIndividualMetric( - deps.logger, - letterToInsert.supplierId, - MetricStatus.Success, - letterToInsert.groupId, - ); + emitIndividualMetric(deps.logger, MetricStatus.Success, dimensions); } catch (error) { - // emit failure metric - emitIndividualMetric( - deps.logger, - letterToInsert.supplierId, - MetricStatus.Failure, - letterToInsert.groupId, - ); if (error instanceof LetterAlreadyExistsError) { deps.logger.warn({ description: "Letter already exists", supplierId: letterToInsert.supplierId, letterId: letterToInsert.id, }); + emitIndividualMetric(deps.logger, MetricStatus.Failure, dimensions); } else { throw error; } @@ -94,6 +96,10 @@ function getOperationFromType(type: string): UpsertOperation { handler: async (request, allocationDetails, deps) => { const supplierEvent = request as LetterStatusChangeEvent; const letterToUpdate: UpdateLetter = mapToUpdateLetter(supplierEvent); + const dimensions: UpsertDimensions = extractDimensionsFromLetterEvent( + request as LetterStatusChangeEvent | PreparedEvents, + allocationDetails, + ); await deps.letterRepo.updateLetterStatus(letterToUpdate); deps.logger.info({ @@ -104,15 +110,51 @@ function getOperationFromType(type: string): UpsertOperation { letterUpdateRequest: request, letterToUpdate, }); - emitIndividualMetric( - deps.logger, - letterToUpdate.supplierId, - MetricStatus.Success, - ); + emitIndividualMetric(deps.logger, MetricStatus.Success, dimensions); }, }; } +export function extractDimensionsFromLetterEvent( + letterEvent: LetterStatusChangeEvent | PreparedEvents, + allocationDetails: AllocationDetails | undefined, +): UpsertDimensions { + const { type } = letterEvent; + // Insert type + if ( + type.startsWith("uk.nhs.notify.letter-rendering.letter-request.prepared") + ) { + const preparedRequest = letterEvent as PreparedEvents; + const { campaignId, clientId, templateId } = preparedRequest.data; + const supplierId = allocationDetails?.supplierSpec.supplierId; + const status = + allocationDetails?.allocationStatus.status === "REJECTED" + ? "REJECTED" + : "PENDING"; + const groupId = formatGroupId(clientId, campaignId, templateId); + return { + Supplier: supplierId || "unknown", + Status: status, + ClientId: clientId || "unknown", + CampaignId: campaignId || "unknown", + GroupId: groupId, + TemplateId: templateId || "unknown", + }; + } + // Update type + const changeEvent = letterEvent as LetterStatusChangeEvent; + const groupId = changeEvent.data.groupId || "unknown"; + const [clientId, campaignId, templateId] = groupId.split("-"); + return { + Supplier: changeEvent.data.supplierId || "unknown", + Status: changeEvent.data.status, + ClientId: clientId || "unknown", + CampaignId: campaignId || "unknown", + GroupId: groupId, + TemplateId: templateId || "unknown", + }; +} + function mapToInsertLetter( upsertRequest: PreparedEvents, allocationDetails: AllocationDetails, @@ -176,15 +218,10 @@ async function runUpsert( async function emitIndividualMetric( logger: Logger, - supplier: string, metricKey: MetricStatus, - groupId?: string, + dimensions: UpsertDimensions, ) { const namespace = process.env.AWS_LAMBDA_FUNCTION_NAME || "upsertLetter"; - const dimensions: Record = { - Supplier: supplier || "unknown", - GroupId: groupId || "unknown", - }; const metric: MetricEntry = { key: metricKey, @@ -223,6 +260,14 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { return async (event: SQSEvent, context: Context) => { const batchItemFailures: SQSBatchItemFailure[] = []; + let dimensions: UpsertDimensions = { + Supplier: "unknown", + Status: "unknown", + ClientId: "unknown", + CampaignId: "unknown", + GroupId: "unknown", + TemplateId: "unknown", + }; const tasks = event.Records.map(async (record) => { try { deps.logger.info({ @@ -253,6 +298,10 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { }); idempotencyConfig.registerLambdaContext(context); + dimensions = extractDimensionsFromLetterEvent( + letterEvent, + allocationDetails, + ); await processRecordIdempotently(letterEvent, allocationDetails, deps); } catch (error) { deps.logger.error({ @@ -263,8 +312,8 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { }); await emitIndividualMetric( deps.logger, - "unknown", MetricStatus.Failure, + dimensions, ); batchItemFailures.push({ itemIdentifier: record.messageId }); } diff --git a/tests/pact-tests/consumer/letter-request-prepared.consumer.pact.test.ts b/tests/pact-tests/consumer/letter-request-prepared.consumer.pact.test.ts index 0430bd129..82f1093ed 100644 --- a/tests/pact-tests/consumer/letter-request-prepared.consumer.pact.test.ts +++ b/tests/pact-tests/consumer/letter-request-prepared.consumer.pact.test.ts @@ -1,4 +1,4 @@ -/* eslint-disable security/detect-unsafe-regex, sonarjs/slow-regex */ +/* eslint-disable security/detect-unsafe-regex */ import path from "node:path"; import { MatchersV3,