diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md
index dc293659f..c0dc3e1bb 100644
--- a/infrastructure/terraform/components/api/README.md
+++ b/infrastructure/terraform/components/api/README.md
@@ -30,7 +30,7 @@ No requirements.
| [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | n/a | yes |
| [kms\_deletion\_window](#input\_kms\_deletion\_window) | When a kms key is deleted, how long should it wait in the pending deletion state? | `string` | `"30"` | no |
| [letter\_table\_ttl\_hours](#input\_letter\_table\_ttl\_hours) | Number of hours to set as TTL on letters table | `number` | `24` | no |
-| [letter\_variant\_map](#input\_letter\_variant\_map) | n/a | `map(object({ supplierId = string, specId = string }))` |
{
"lv1": {
"specId": "spec1",
"supplierId": "supplier1"
},
"lv2": {
"specId": "spec2",
"supplierId": "supplier1"
},
"lv3": {
"specId": "spec3",
"supplierId": "supplier2"
}
} | no |
+| [letter\_variant\_map](#input\_letter\_variant\_map) | n/a | `map(object({ supplierId = string, specId = string, priority = number }))` | {
"lv1": {
"priority": 10,
"specId": "spec1",
"supplierId": "supplier1"
},
"lv2": {
"priority": 10,
"specId": "spec2",
"supplierId": "supplier1"
},
"lv3": {
"priority": 10,
"specId": "spec3",
"supplierId": "supplier2"
}
} | no |
| [log\_level](#input\_log\_level) | The log level to be used in lambda functions within the component. Any log with a lower severity than the configured value will not be logged: https://docs.python.org/3/library/logging.html#levels | `string` | `"INFO"` | no |
| [log\_retention\_in\_days](#input\_log\_retention\_in\_days) | The retention period in days for the Cloudwatch Logs events to be retained, default of 0 is indefinite | `number` | `0` | no |
| [manually\_configure\_mtls\_truststore](#input\_manually\_configure\_mtls\_truststore) | Manually manage the truststore used for API Gateway mTLS (e.g. for prod environment) | `bool` | `false` | no |
diff --git a/infrastructure/terraform/components/api/ddb_table_letter_queue.tf b/infrastructure/terraform/components/api/ddb_table_letter_queue.tf
index b6952ab46..0aa940121 100644
--- a/infrastructure/terraform/components/api/ddb_table_letter_queue.tf
+++ b/infrastructure/terraform/components/api/ddb_table_letter_queue.tf
@@ -12,7 +12,7 @@ resource "aws_dynamodb_table" "letter_queue" {
local_secondary_index {
name = "queueSortOrder-index"
- range_key = "queueTimestamp"
+ range_key = "queueSortOrderSk"
projection_type = "ALL"
}
@@ -27,7 +27,7 @@ resource "aws_dynamodb_table" "letter_queue" {
}
attribute {
- name = "queueTimestamp"
+ name = "queueSortOrderSk"
type = "S"
}
diff --git a/infrastructure/terraform/components/api/variables.tf b/infrastructure/terraform/components/api/variables.tf
index 4559f30b9..7f20770be 100644
--- a/infrastructure/terraform/components/api/variables.tf
+++ b/infrastructure/terraform/components/api/variables.tf
@@ -136,11 +136,11 @@ variable "eventpub_control_plane_bus_arn" {
}
variable "letter_variant_map" {
- type = map(object({ supplierId = string, specId = string }))
+ type = map(object({ supplierId = string, specId = string, priority = number }))
default = {
- "lv1" = { supplierId = "supplier1", specId = "spec1" },
- "lv2" = { supplierId = "supplier1", specId = "spec2" },
- "lv3" = { supplierId = "supplier2", specId = "spec3" }
+ "lv1" = { supplierId = "supplier1", specId = "spec1", priority = 10 },
+ "lv2" = { supplierId = "supplier1", specId = "spec2", priority = 10 },
+ "lv3" = { supplierId = "supplier2", specId = "spec3", priority = 10 }
}
}
diff --git a/internal/datastore/src/__test__/db.ts b/internal/datastore/src/__test__/db.ts
index ae652ad13..a513b74b8 100644
--- a/internal/datastore/src/__test__/db.ts
+++ b/internal/datastore/src/__test__/db.ts
@@ -22,6 +22,7 @@ export async function setupDynamoDBContainer() {
accessKeyId: "fakeMyKeyId",
secretAccessKey: "fakeSecretAccessKey",
},
+ maxAttempts: 1,
});
const docClient = DynamoDBDocumentClient.from(ddbClient);
@@ -132,7 +133,7 @@ const createLetterQueueTableCommand = new CreateTableCommand({
IndexName: "queueSortOrder-index",
KeySchema: [
{ AttributeName: "supplierId", KeyType: "HASH" }, // Partition key for LSI
- { AttributeName: "queueTimestamp", KeyType: "RANGE" }, // Sort key for LSI
+ { AttributeName: "queueSortOrderSk", KeyType: "RANGE" }, // Sort key for LSI
],
Projection: {
ProjectionType: "ALL",
@@ -142,7 +143,7 @@ const createLetterQueueTableCommand = new CreateTableCommand({
AttributeDefinitions: [
{ AttributeName: "supplierId", AttributeType: "S" },
{ AttributeName: "letterId", AttributeType: "S" },
- { AttributeName: "queueTimestamp", AttributeType: "S" },
+ { AttributeName: "queueSortOrderSk", AttributeType: "S" },
],
});
diff --git a/internal/datastore/src/__test__/letter-queue-repository.test.ts b/internal/datastore/src/__test__/letter-queue-repository.test.ts
index 04e8d57ca..629d8aefd 100644
--- a/internal/datastore/src/__test__/letter-queue-repository.test.ts
+++ b/internal/datastore/src/__test__/letter-queue-repository.test.ts
@@ -12,12 +12,16 @@ import { LetterAlreadyExistsError } from "../letter-already-exists-error";
import { createTestLogger } from "./logs";
import { LetterDoesNotExistError } from "../letter-does-not-exist-error";
-function createLetter(letterId = "letter1"): InsertPendingLetter {
+function createLetter(
+ overrides: Partial = {},
+): InsertPendingLetter {
return {
- letterId,
+ letterId: "letter1",
supplierId: "supplier1",
specificationId: "specification1",
groupId: "group1",
+ priority: 10,
+ ...overrides,
};
}
@@ -54,9 +58,11 @@ describe("LetterQueueRepository", () => {
});
describe("putLetter", () => {
- it("adds a letter to the database", async () => {
+ beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date("2026-03-04T13:15:45.000Z"));
+ });
+ it("adds a letter to the database", async () => {
const pendingLetter =
await letterQueueRepository.putLetter(createLetter());
@@ -65,9 +71,32 @@ describe("LetterQueueRepository", () => {
"2026-03-04T13:15:45.000Z",
);
expect(pendingLetter.ttl).toBe(1_772_633_745);
+ expect(pendingLetter.queueSortOrderSk).toBe(
+ "10-2026-03-04T13:15:45.000Z",
+ );
expect(await letterExists(db, "supplier1", "letter1")).toBe(true);
});
+ it("left-pads the priority with zeros in the sort key", async () => {
+ const pendingLetter = await letterQueueRepository.putLetter(
+ createLetter({ priority: 5 }),
+ );
+
+ expect(pendingLetter.queueSortOrderSk).toBe(
+ "05-2026-03-04T13:15:45.000Z",
+ );
+ });
+
+ it("defaults a missing priority to 10 in the sort key", async () => {
+ const pendingLetter = await letterQueueRepository.putLetter(
+ createLetter({ priority: undefined }),
+ );
+
+ expect(pendingLetter.queueSortOrderSk).toBe(
+ "10-2026-03-04T13:15:45.000Z",
+ );
+ });
+
it("throws LetterAlreadyExistsError when creating a letter which already exists", async () => {
await letterQueueRepository.putLetter(createLetter());
@@ -122,16 +151,21 @@ describe("LetterQueueRepository", () => {
});
});
-async function letterExists(
- db: DBContext,
- supplierId: string,
- letterId: string,
-): Promise {
+async function getLetter(db: DBContext, supplierId: string, letterId: string) {
const result = await db.docClient.send(
new GetCommand({
TableName: db.config.letterQueueTableName,
Key: { supplierId, letterId },
}),
);
- return result.Item !== undefined;
+ return result.Item;
+}
+
+async function letterExists(
+ db: DBContext,
+ supplierId: string,
+ letterId: string,
+): Promise {
+ const letter = await getLetter(db, supplierId, letterId);
+ return letter !== undefined;
}
diff --git a/internal/datastore/src/letter-queue-repository.ts b/internal/datastore/src/letter-queue-repository.ts
index 70592db25..e30eadcfc 100644
--- a/internal/datastore/src/letter-queue-repository.ts
+++ b/internal/datastore/src/letter-queue-repository.ts
@@ -24,16 +24,22 @@ export default class LetterQueueRepository {
readonly config: LetterQueueRepositoryConfig,
) {}
+ private readonly defaultPriority = 10;
+
async putLetter(
insertPendingLetter: InsertPendingLetter,
): Promise {
// needs to be an ISO timestamp as Db sorts alphabetically
const now = new Date().toISOString();
-
+ const priority = String(
+ insertPendingLetter.priority ?? this.defaultPriority,
+ );
+ const queueSortOrderSk = `${priority.padStart(2, "0")}-${now}`;
const pendingLetter: PendingLetter = {
...insertPendingLetter,
queueTimestamp: now,
visibilityTimestamp: now,
+ queueSortOrderSk,
ttl: Math.floor(
Date.now() / 1000 + 60 * 60 * this.config.letterQueueTtlHours,
),
diff --git a/internal/datastore/src/types.ts b/internal/datastore/src/types.ts
index 107a6c8b8..efeefe345 100644
--- a/internal/datastore/src/types.ts
+++ b/internal/datastore/src/types.ts
@@ -43,6 +43,7 @@ export const LetterSchemaBase = z.object({
export const LetterSchema = LetterSchemaBase.extend({
supplierId: idRef(SupplierSchema, "id"),
eventId: z.string().optional(),
+ priority: z.int().min(0).max(99).optional(), // A lower number represents a higher priority
url: z.url(),
createdAt: z.string(),
updatedAt: z.string(),
@@ -79,10 +80,12 @@ export type UpdateLetter = {
export const PendingLetterSchema = z.object({
supplierId: idRef(SupplierSchema, "id"),
letterId: idRef(LetterSchema, "id"),
- queueTimestamp: z.string().describe("Secondary index SK"),
+ queueTimestamp: z.string(),
visibilityTimestamp: z.string(),
+ queueSortOrderSk: z.string().describe("Secondary index SK"),
specificationId: z.string(),
groupId: z.string(),
+ priority: z.int().min(0).max(99).optional(),
ttl: z.int(),
});
@@ -90,7 +93,7 @@ export type PendingLetter = z.infer;
export type InsertPendingLetter = Omit<
PendingLetter,
- "ttl" | "queueTimestamp" | "visibilityTimestamp"
+ "ttl" | "queueTimestamp" | "visibilityTimestamp" | "queueSortOrderSk"
>;
export const MISchemaBase = z.object({
diff --git a/lambdas/supplier-allocator/package.json b/lambdas/supplier-allocator/package.json
index 1b21d001f..6aa1f7abd 100644
--- a/lambdas/supplier-allocator/package.json
+++ b/lambdas/supplier-allocator/package.json
@@ -9,6 +9,7 @@
"@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1": "npm:@nhsdigital/nhs-notify-event-schemas-letter-rendering@^1.1.5",
"@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.8",
"@types/aws-lambda": "^8.10.148",
+ "aws-embedded-metrics": "^4.2.1",
"aws-lambda": "^1.0.7",
"esbuild": "^0.27.2",
"pino": "^9.7.0",
diff --git a/lambdas/supplier-allocator/src/config/__tests__/env.test.ts b/lambdas/supplier-allocator/src/config/__tests__/env.test.ts
index dd013aebf..3ad3991ac 100644
--- a/lambdas/supplier-allocator/src/config/__tests__/env.test.ts
+++ b/lambdas/supplier-allocator/src/config/__tests__/env.test.ts
@@ -18,7 +18,8 @@ describe("lambdaEnv", () => {
process.env.VARIANT_MAP = `{
"lv1": {
"supplierId": "supplier1",
- "specId": "spec1"
+ "specId": "spec1",
+ "priority": 10
}
}`;
@@ -29,6 +30,7 @@ describe("lambdaEnv", () => {
lv1: {
supplierId: "supplier1",
specId: "spec1",
+ priority: 10,
},
},
});
diff --git a/lambdas/supplier-allocator/src/config/env.ts b/lambdas/supplier-allocator/src/config/env.ts
index 0adc39203..d1e07403f 100644
--- a/lambdas/supplier-allocator/src/config/env.ts
+++ b/lambdas/supplier-allocator/src/config/env.ts
@@ -5,6 +5,7 @@ const LetterVariantSchema = z.record(
z.object({
supplierId: z.string(),
specId: z.string(),
+ priority: z.int().min(0).max(99), // Lower number represents a higher priority
}),
);
export type LetterVariant = z.infer;
diff --git a/lambdas/supplier-allocator/src/handler/__tests__/allocate-handler.test.ts b/lambdas/supplier-allocator/src/handler/__tests__/allocate-handler.test.ts
index 23fc5981c..dc9524e5e 100644
--- a/lambdas/supplier-allocator/src/handler/__tests__/allocate-handler.test.ts
+++ b/lambdas/supplier-allocator/src/handler/__tests__/allocate-handler.test.ts
@@ -7,10 +7,39 @@ import {
$LetterEvent,
LetterEvent,
} from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events";
+import { MetricStatus } from "@internal/helpers";
import createSupplierAllocatorHandler from "../allocate-handler";
import { Deps } from "../../config/deps";
import { EnvVars } from "../../config/env";
+function assertMetricLogged(
+ logger: pino.Logger,
+ supplier: string,
+ priority: string,
+ status: MetricStatus,
+ count: number,
+) {
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ Supplier: supplier,
+ Priority: priority,
+ _aws: expect.objectContaining({
+ CloudWatchMetrics: expect.arrayContaining([
+ expect.objectContaining({
+ Metrics: [
+ expect.objectContaining({
+ Name: status,
+ Value: count,
+ }),
+ ],
+ }),
+ ]),
+ }),
+ [status]: count,
+ }),
+ );
+}
+
function createSQSEvent(records: SQSRecord[]): SQSEvent {
return {
Records: records,
@@ -145,6 +174,7 @@ describe("createSupplierAllocatorHandler", () => {
lv1: {
supplierId: "supplier1",
specId: "spec1",
+ priority: 10,
},
},
} as EnvVars,
@@ -179,7 +209,16 @@ describe("createSupplierAllocatorHandler", () => {
expect(messageBody.supplierSpec).toEqual({
supplierId: "supplier1",
specId: "spec1",
+ priority: 10,
});
+
+ assertMetricLogged(
+ mockedDeps.logger,
+ "supplier1",
+ "10",
+ MetricStatus.Success,
+ 1,
+ );
});
test("parses SNS notification and sends message to SQS queue for v1 event", async () => {
@@ -205,6 +244,7 @@ describe("createSupplierAllocatorHandler", () => {
expect(messageBody.supplierSpec).toEqual({
supplierId: "supplier1",
specId: "spec1",
+ priority: 10,
});
});
@@ -226,6 +266,14 @@ describe("createSupplierAllocatorHandler", () => {
expect(result.batchItemFailures).toHaveLength(1);
expect(result.batchItemFailures[0].itemIdentifier).toBe("invalid-event");
expect((mockedDeps.logger.error as jest.Mock).mock.calls).toHaveLength(1);
+
+ assertMetricLogged(
+ mockedDeps.logger,
+ "unknown",
+ "unknown",
+ MetricStatus.Failure,
+ 1,
+ );
});
test("unwraps EventBridge envelope and extracts event details", async () => {
@@ -259,8 +307,11 @@ describe("createSupplierAllocatorHandler", () => {
const sendCall = (mockSqsClient.send as jest.Mock).mock.calls[0][0];
const messageBody = JSON.parse(sendCall.input.MessageBody);
- expect(messageBody.supplierSpec.supplierId).toBe("supplier1");
- expect(messageBody.supplierSpec.specId).toBe("spec1");
+ expect(messageBody.supplierSpec).toEqual({
+ supplierId: "supplier1",
+ specId: "spec1",
+ priority: 10,
+ });
});
test("processes multiple messages in batch", async () => {
@@ -392,6 +443,14 @@ describe("createSupplierAllocatorHandler", () => {
expect(result.batchItemFailures).toHaveLength(1);
expect(result.batchItemFailures[0].itemIdentifier).toBe("msg1");
expect((mockedDeps.logger.error as jest.Mock).mock.calls).toHaveLength(1);
+
+ assertMetricLogged(
+ mockedDeps.logger,
+ "supplier1",
+ "10",
+ MetricStatus.Failure,
+ 1,
+ );
});
test("processes mixed batch with successes and failures", async () => {
@@ -417,6 +476,21 @@ describe("createSupplierAllocatorHandler", () => {
expect(result.batchItemFailures[0].itemIdentifier).toBe("fail-msg");
expect(mockSqsClient.send).toHaveBeenCalledTimes(2);
+
+ assertMetricLogged(
+ mockedDeps.logger,
+ "supplier1",
+ "10",
+ MetricStatus.Success,
+ 2,
+ );
+ assertMetricLogged(
+ mockedDeps.logger,
+ "unknown",
+ "unknown",
+ MetricStatus.Failure,
+ 1,
+ );
});
test("sends correct queue URL in SQS message command", async () => {
@@ -435,4 +509,48 @@ describe("createSupplierAllocatorHandler", () => {
const sendCall = (mockSqsClient.send as jest.Mock).mock.calls[0][0];
expect(sendCall.input.QueueUrl).toBe(queueUrl);
});
+
+ test("emits separate metrics per supplier and priority combination", async () => {
+ mockedDeps.env.VARIANT_MAP = {
+ lv1: { supplierId: "supplier1", specId: "spec1", priority: 10 },
+ lv2: { supplierId: "supplier2", specId: "spec2", priority: 5 },
+ } as any;
+
+ const eventForSupplier1 = createPreparedV2Event({ domainId: "letter1" });
+ const eventForSupplier2 = {
+ ...createPreparedV2Event({ domainId: "letter2" }),
+ data: {
+ ...createPreparedV2Event({ domainId: "letter2" }).data,
+ letterVariantId: "lv2",
+ },
+ };
+
+ const evt: SQSEvent = createSQSEvent([
+ createSqsRecord("msg1", JSON.stringify(eventForSupplier1)),
+ createSqsRecord("msg2", JSON.stringify(eventForSupplier2)),
+ ]);
+
+ process.env.UPSERT_LETTERS_QUEUE_URL = "https://sqs.test.queue";
+
+ const handler = createSupplierAllocatorHandler(mockedDeps);
+ const result = await handler(evt, {} as any, {} as any);
+ if (!result) throw new Error("expected BatchResponse, got void");
+
+ expect(result.batchItemFailures).toHaveLength(0);
+
+ assertMetricLogged(
+ mockedDeps.logger,
+ "supplier1",
+ "10",
+ MetricStatus.Success,
+ 1,
+ );
+ assertMetricLogged(
+ mockedDeps.logger,
+ "supplier2",
+ "5",
+ MetricStatus.Success,
+ 1,
+ );
+ });
});
diff --git a/lambdas/supplier-allocator/src/handler/allocate-handler.ts b/lambdas/supplier-allocator/src/handler/allocate-handler.ts
index 0402e2846..3fd4f3be5 100644
--- a/lambdas/supplier-allocator/src/handler/allocate-handler.ts
+++ b/lambdas/supplier-allocator/src/handler/allocate-handler.ts
@@ -4,9 +4,11 @@ import { LetterRequestPreparedEvent } from "@nhsdigital/nhs-notify-event-schemas
import { LetterRequestPreparedEventV2 } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering";
import z from "zod";
+import { Unit } from "aws-embedded-metrics";
+import { MetricEntry, MetricStatus, buildEMFObject } from "@internal/helpers";
import { Deps } from "../config/deps";
-type SupplierSpec = { supplierId: string; specId: string };
+type SupplierSpec = { supplierId: string; specId: string; priority: number };
type PreparedEvents = LetterRequestPreparedEventV2 | LetterRequestPreparedEvent;
// small envelope that must exist in all inputs
@@ -50,11 +52,49 @@ function getSupplier(letterEvent: PreparedEvents, deps: Deps): SupplierSpec {
return resolveSupplierForVariant(letterEvent.data.letterVariantId, deps);
}
+type AllocationMetrics = Map>;
+
+function incrementMetric(
+ map: AllocationMetrics,
+ supplier: string,
+ priority: string,
+) {
+ const byPriority = map.get(supplier) ?? new Map();
+ byPriority.set(priority, (byPriority.get(priority) ?? 0) + 1);
+ map.set(supplier, byPriority);
+}
+
+function emitMetrics(
+ metrics: AllocationMetrics,
+ status: MetricStatus,
+ deps: Deps,
+) {
+ const namespace = "supplier-allocator";
+ for (const [supplier, byPriority] of metrics) {
+ for (const [priority, count] of byPriority) {
+ const dimensions: Record = {
+ Priority: priority,
+ Supplier: supplier,
+ };
+ const metric: MetricEntry = {
+ key: status,
+ value: count,
+ unit: Unit.Count,
+ };
+ deps.logger.info(buildEMFObject(namespace, dimensions, metric));
+ }
+ }
+}
+
export default function createSupplierAllocatorHandler(deps: Deps): SQSHandler {
return async (event: SQSEvent) => {
const batchItemFailures: SQSBatchItemFailure[] = [];
+ const perAllocationSuccess: AllocationMetrics = new Map();
+ const perAllocationFailure: AllocationMetrics = new Map();
const tasks = event.Records.map(async (record) => {
+ let supplier = "unknown";
+ let priority = "unknown";
try {
const letterEvent: unknown = JSON.parse(record.body);
@@ -67,6 +107,9 @@ export default function createSupplierAllocatorHandler(deps: Deps): SQSHandler {
const supplierSpec = getSupplier(letterEvent as PreparedEvents, deps);
+ supplier = supplierSpec.supplierId;
+ priority = String(supplierSpec.priority);
+
deps.logger.info({
description: "Resolved supplier spec",
supplierSpec,
@@ -95,6 +138,8 @@ export default function createSupplierAllocatorHandler(deps: Deps): SQSHandler {
MessageBody: JSON.stringify(queueMessage),
}),
);
+
+ incrementMetric(perAllocationSuccess, supplier, priority);
} catch (error) {
deps.logger.error({
description: "Error processing allocation of record",
@@ -102,12 +147,15 @@ export default function createSupplierAllocatorHandler(deps: Deps): SQSHandler {
messageId: record.messageId,
message: record.body,
});
+ incrementMetric(perAllocationFailure, supplier, priority);
batchItemFailures.push({ itemIdentifier: record.messageId });
}
});
await Promise.all(tasks);
+ emitMetrics(perAllocationSuccess, MetricStatus.Success, deps);
+ emitMetrics(perAllocationFailure, MetricStatus.Failure, deps);
return { batchItemFailures };
};
}
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 6292de0f7..9f855d176 100644
--- a/lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts
+++ b/lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts
@@ -211,11 +211,11 @@ describe("createUpsertLetterHandler", () => {
test("processes all records successfully and returns no batch failures", async () => {
const v2message = {
letterEvent: createPreparedV2Event(),
- supplierSpec: { supplierId: "supplier1", specId: "spec1" },
+ supplierSpec: { supplierId: "supplier1", specId: "spec1", priority: 10 },
};
const v1message = {
letterEvent: createPreparedV1Event(),
- supplierSpec: { supplierId: "supplier1", specId: "spec1" },
+ supplierSpec: { supplierId: "supplier1", specId: "spec1", priority: 10 },
};
const evt: SQSEvent = createSQSEvent([
@@ -249,6 +249,7 @@ describe("createUpsertLetterHandler", () => {
expect(insertedV2Letter.status).toBe("PENDING");
expect(insertedV2Letter.groupId).toBe("client1campaign1template1");
expect(insertedV2Letter.source).toBe("/data-plane/letter-rendering/test");
+ expect(insertedV2Letter.priority).toBe(10);
const insertedV1Letter = (mockedDeps.letterRepo.putLetter as jest.Mock).mock
.calls[1][0];
@@ -260,6 +261,7 @@ describe("createUpsertLetterHandler", () => {
expect(insertedV1Letter.status).toBe("PENDING");
expect(insertedV1Letter.groupId).toBe("client1campaign1template1");
expect(insertedV1Letter.source).toBe("/data-plane/letter-rendering/test");
+ expect(insertedV1Letter.priority).toBe(10);
const updatedLetter = (
mockedDeps.letterRepo.updateLetterStatus as jest.Mock
@@ -472,14 +474,14 @@ describe("createUpsertLetterHandler", () => {
id: "7b9a03ca-342a-4150-b56b-989109c45615",
domainId: "ok",
}),
- supplierSpec: { supplierId: "supplier1", specId: "spec1" },
+ supplierSpec: { supplierId: "supplier1", specId: "spec1", priority: 10 },
};
const message2 = {
letterEvent: createPreparedV2Event({
id: "7b9a03ca-342a-4150-b56b-989109c45616",
domainId: "fail",
}),
- supplierSpec: { supplierId: "supplier1", specId: "spec1" },
+ supplierSpec: { supplierId: "supplier1", specId: "spec1", priority: 10 },
};
const evt: SQSEvent = createSQSEvent([
createSqsRecord("ok-msg", JSON.stringify(message1)),
diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts
index ada416ec2..ab9bd9877 100644
--- a/lambdas/upsert-letter/src/handler/upsert-handler.ts
+++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts
@@ -16,14 +16,16 @@ import z from "zod";
import { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics";
import { Deps } from "../config/deps";
-type SupplierSpec = { supplierId: string; specId: string };
type PreparedEvents = LetterRequestPreparedEventV2 | LetterRequestPreparedEvent;
const SupplierSpecSchema = z.object({
supplierId: z.string().min(1),
specId: z.string().min(1),
+ priority: z.int().min(0).max(99).default(10),
});
+type SupplierSpec = z.infer;
+
const PreparedEventUnionSchema = z.discriminatedUnion("type", [
$LetterRequestPreparedEventV2,
$LetterRequestPreparedEvent,
@@ -63,6 +65,7 @@ function getOperationFromType(type: string): UpsertOperation {
supplierSpec.supplierId,
supplierSpec.specId,
supplierSpec.specId, // use specId for now
+ supplierSpec.priority,
);
await deps.letterRepo.putLetter(letterToInsert);
@@ -99,6 +102,7 @@ function mapToInsertLetter(
supplier: string,
spec: string,
billingRef: string,
+ priority: number,
): InsertLetter {
const now = new Date().toISOString();
return {
@@ -107,6 +111,7 @@ function mapToInsertLetter(
supplierId: supplier,
status: "PENDING",
specificationId: spec,
+ priority,
groupId:
upsertRequest.data.clientId +
upsertRequest.data.campaignId +
@@ -235,7 +240,11 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler {
await runUpsert(
operation,
letterEvent,
- supplierSpec ?? { supplierId: "unknown", specId: "unknown" },
+ supplierSpec ?? {
+ supplierId: "unknown",
+ specId: "unknown",
+ priority: 10,
+ },
deps,
);
diff --git a/package-lock.json b/package-lock.json
index 84ca65626..dda49061b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -295,6 +295,7 @@
"@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1": "npm:@nhsdigital/nhs-notify-event-schemas-letter-rendering@^1.1.5",
"@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.8",
"@types/aws-lambda": "^8.10.148",
+ "aws-embedded-metrics": "^4.2.1",
"aws-lambda": "^1.0.7",
"esbuild": "^0.27.2",
"pino": "^9.7.0",