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,