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",