Skip to content

Commit c861af7

Browse files
stevebuxmasl2
andauthored
Feature/ccm 13906 urgent letter priority (#453)
* CCM-13906 Urgent Letter Priority * Fix mapping or priority to letter queue * remove/disable trivy --------- Co-authored-by: Mark Slowey <mark.slowey1@nhs.net> Co-authored-by: Mark Slowey <113013138+masl2@users.noreply.github.com>
1 parent 023e23b commit c861af7

File tree

17 files changed

+260
-31
lines changed

17 files changed

+260
-31
lines changed

infrastructure/terraform/components/api/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ No requirements.
3535
| <a name="input_kms_deletion_window"></a> [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 |
3636
| <a name="input_letter_event_source"></a> [letter\_event\_source](#input\_letter\_event\_source) | Source value to use for the letter status event updates | `string` | `"/data-plane/supplier-api/nhs-supplier-api-prod/main/update-status"` | no |
3737
| <a name="input_letter_table_ttl_hours"></a> [letter\_table\_ttl\_hours](#input\_letter\_table\_ttl\_hours) | Number of hours to set as TTL on letters table | `number` | `24` | no |
38-
| <a name="input_letter_variant_map"></a> [letter\_variant\_map](#input\_letter\_variant\_map) | n/a | `map(object({ supplierId = string, specId = string }))` | <pre>{<br/> "lv1": {<br/> "specId": "spec1",<br/> "supplierId": "supplier1"<br/> },<br/> "lv2": {<br/> "specId": "spec2",<br/> "supplierId": "supplier1"<br/> },<br/> "lv3": {<br/> "specId": "spec3",<br/> "supplierId": "supplier2"<br/> }<br/>}</pre> | no |
38+
| <a name="input_letter_variant_map"></a> [letter\_variant\_map](#input\_letter\_variant\_map) | n/a | `map(object({ supplierId = string, specId = string, priority = number }))` | <pre>{<br/> "lv1": {<br/> "priority": 10,<br/> "specId": "spec1",<br/> "supplierId": "supplier1"<br/> },<br/> "lv2": {<br/> "priority": 10,<br/> "specId": "spec2",<br/> "supplierId": "supplier1"<br/> },<br/> "lv3": {<br/> "priority": 10,<br/> "specId": "spec3",<br/> "supplierId": "supplier2"<br/> }<br/>}</pre> | no |
3939
| <a name="input_log_level"></a> [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 |
4040
| <a name="input_log_retention_in_days"></a> [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 |
4141
| <a name="input_manually_configure_mtls_truststore"></a> [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 |

infrastructure/terraform/components/api/ddb_table_letter_queue.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ resource "aws_dynamodb_table" "letter_queue" {
1212

1313
local_secondary_index {
1414
name = "queueSortOrder-index"
15-
range_key = "queueTimestamp"
15+
range_key = "queueSortOrderSk"
1616
projection_type = "ALL"
1717
}
1818

@@ -27,7 +27,7 @@ resource "aws_dynamodb_table" "letter_queue" {
2727
}
2828

2929
attribute {
30-
name = "queueTimestamp"
30+
name = "queueSortOrderSk"
3131
type = "S"
3232
}
3333

infrastructure/terraform/components/api/variables.tf

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,11 @@ variable "eventpub_control_plane_bus_arn" {
136136
}
137137

138138
variable "letter_variant_map" {
139-
type = map(object({ supplierId = string, specId = string }))
139+
type = map(object({ supplierId = string, specId = string, priority = number }))
140140
default = {
141-
"lv1" = { supplierId = "supplier1", specId = "spec1" },
142-
"lv2" = { supplierId = "supplier1", specId = "spec2" },
143-
"lv3" = { supplierId = "supplier2", specId = "spec3" }
141+
"lv1" = { supplierId = "supplier1", specId = "spec1", priority = 10 },
142+
"lv2" = { supplierId = "supplier1", specId = "spec2", priority = 10 },
143+
"lv3" = { supplierId = "supplier2", specId = "spec3", priority = 10 }
144144
}
145145
}
146146

internal/datastore/src/__test__/db.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export async function setupDynamoDBContainer() {
2222
accessKeyId: "fakeMyKeyId",
2323
secretAccessKey: "fakeSecretAccessKey",
2424
},
25+
maxAttempts: 1,
2526
});
2627

2728
const docClient = DynamoDBDocumentClient.from(ddbClient);
@@ -132,7 +133,7 @@ const createLetterQueueTableCommand = new CreateTableCommand({
132133
IndexName: "queueSortOrder-index",
133134
KeySchema: [
134135
{ AttributeName: "supplierId", KeyType: "HASH" }, // Partition key for LSI
135-
{ AttributeName: "queueTimestamp", KeyType: "RANGE" }, // Sort key for LSI
136+
{ AttributeName: "queueSortOrderSk", KeyType: "RANGE" }, // Sort key for LSI
136137
],
137138
Projection: {
138139
ProjectionType: "ALL",
@@ -142,7 +143,7 @@ const createLetterQueueTableCommand = new CreateTableCommand({
142143
AttributeDefinitions: [
143144
{ AttributeName: "supplierId", AttributeType: "S" },
144145
{ AttributeName: "letterId", AttributeType: "S" },
145-
{ AttributeName: "queueTimestamp", AttributeType: "S" },
146+
{ AttributeName: "queueSortOrderSk", AttributeType: "S" },
146147
],
147148
});
148149

internal/datastore/src/__test__/letter-queue-repository.test.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ import { LetterAlreadyExistsError } from "../letter-already-exists-error";
1212
import { createTestLogger } from "./logs";
1313
import { LetterDoesNotExistError } from "../letter-does-not-exist-error";
1414

15-
function createLetter(letterId = "letter1"): InsertPendingLetter {
15+
function createLetter(
16+
overrides: Partial<InsertPendingLetter> = {},
17+
): InsertPendingLetter {
1618
return {
17-
letterId,
19+
letterId: "letter1",
1820
supplierId: "supplier1",
1921
specificationId: "specification1",
2022
groupId: "group1",
23+
priority: 10,
24+
...overrides,
2125
};
2226
}
2327

@@ -54,9 +58,11 @@ describe("LetterQueueRepository", () => {
5458
});
5559

5660
describe("putLetter", () => {
57-
it("adds a letter to the database", async () => {
61+
beforeEach(() => {
5862
jest.useFakeTimers().setSystemTime(new Date("2026-03-04T13:15:45.000Z"));
63+
});
5964

65+
it("adds a letter to the database", async () => {
6066
const pendingLetter =
6167
await letterQueueRepository.putLetter(createLetter());
6268

@@ -65,9 +71,32 @@ describe("LetterQueueRepository", () => {
6571
"2026-03-04T13:15:45.000Z",
6672
);
6773
expect(pendingLetter.ttl).toBe(1_772_633_745);
74+
expect(pendingLetter.queueSortOrderSk).toBe(
75+
"10-2026-03-04T13:15:45.000Z",
76+
);
6877
expect(await letterExists(db, "supplier1", "letter1")).toBe(true);
6978
});
7079

80+
it("left-pads the priority with zeros in the sort key", async () => {
81+
const pendingLetter = await letterQueueRepository.putLetter(
82+
createLetter({ priority: 5 }),
83+
);
84+
85+
expect(pendingLetter.queueSortOrderSk).toBe(
86+
"05-2026-03-04T13:15:45.000Z",
87+
);
88+
});
89+
90+
it("defaults a missing priority to 10 in the sort key", async () => {
91+
const pendingLetter = await letterQueueRepository.putLetter(
92+
createLetter({ priority: undefined }),
93+
);
94+
95+
expect(pendingLetter.queueSortOrderSk).toBe(
96+
"10-2026-03-04T13:15:45.000Z",
97+
);
98+
});
99+
71100
it("throws LetterAlreadyExistsError when creating a letter which already exists", async () => {
72101
await letterQueueRepository.putLetter(createLetter());
73102

@@ -122,16 +151,21 @@ describe("LetterQueueRepository", () => {
122151
});
123152
});
124153

125-
async function letterExists(
126-
db: DBContext,
127-
supplierId: string,
128-
letterId: string,
129-
): Promise<boolean> {
154+
async function getLetter(db: DBContext, supplierId: string, letterId: string) {
130155
const result = await db.docClient.send(
131156
new GetCommand({
132157
TableName: db.config.letterQueueTableName,
133158
Key: { supplierId, letterId },
134159
}),
135160
);
136-
return result.Item !== undefined;
161+
return result.Item;
162+
}
163+
164+
async function letterExists(
165+
db: DBContext,
166+
supplierId: string,
167+
letterId: string,
168+
): Promise<boolean> {
169+
const letter = await getLetter(db, supplierId, letterId);
170+
return letter !== undefined;
137171
}

internal/datastore/src/letter-queue-repository.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,22 @@ export default class LetterQueueRepository {
2424
readonly config: LetterQueueRepositoryConfig,
2525
) {}
2626

27+
private readonly defaultPriority = 10;
28+
2729
async putLetter(
2830
insertPendingLetter: InsertPendingLetter,
2931
): Promise<PendingLetter> {
3032
// needs to be an ISO timestamp as Db sorts alphabetically
3133
const now = new Date().toISOString();
32-
34+
const priority = String(
35+
insertPendingLetter.priority ?? this.defaultPriority,
36+
);
37+
const queueSortOrderSk = `${priority.padStart(2, "0")}-${now}`;
3338
const pendingLetter: PendingLetter = {
3439
...insertPendingLetter,
3540
queueTimestamp: now,
3641
visibilityTimestamp: now,
42+
queueSortOrderSk,
3743
ttl: Math.floor(
3844
Date.now() / 1000 + 60 * 60 * this.config.letterQueueTtlHours,
3945
),

internal/datastore/src/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const LetterSchemaBase = z.object({
4343
export const LetterSchema = LetterSchemaBase.extend({
4444
supplierId: idRef(SupplierSchema, "id"),
4545
eventId: z.string().optional(),
46+
priority: z.int().min(0).max(99).optional(), // A lower number represents a higher priority
4647
url: z.url(),
4748
createdAt: z.string(),
4849
updatedAt: z.string(),
@@ -79,18 +80,20 @@ export type UpdateLetter = {
7980
export const PendingLetterSchema = z.object({
8081
supplierId: idRef(SupplierSchema, "id"),
8182
letterId: idRef(LetterSchema, "id"),
82-
queueTimestamp: z.string().describe("Secondary index SK"),
83+
queueTimestamp: z.string(),
8384
visibilityTimestamp: z.string(),
85+
queueSortOrderSk: z.string().describe("Secondary index SK"),
8486
specificationId: z.string(),
8587
groupId: z.string(),
88+
priority: z.int().min(0).max(99).optional(),
8689
ttl: z.int(),
8790
});
8891

8992
export type PendingLetter = z.infer<typeof PendingLetterSchema>;
9093

9194
export type InsertPendingLetter = Omit<
9295
PendingLetter,
93-
"ttl" | "queueTimestamp" | "visibilityTimestamp"
96+
"ttl" | "queueTimestamp" | "visibilityTimestamp" | "queueSortOrderSk"
9497
>;
9598

9699
export const MISchemaBase = z.object({

lambdas/supplier-allocator/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1": "npm:@nhsdigital/nhs-notify-event-schemas-letter-rendering@^1.1.5",
1010
"@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.8",
1111
"@types/aws-lambda": "^8.10.148",
12+
"aws-embedded-metrics": "^4.2.1",
1213
"aws-lambda": "^1.0.7",
1314
"esbuild": "^0.27.2",
1415
"pino": "^9.7.0",

lambdas/supplier-allocator/src/config/__tests__/env.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ describe("lambdaEnv", () => {
1818
process.env.VARIANT_MAP = `{
1919
"lv1": {
2020
"supplierId": "supplier1",
21-
"specId": "spec1"
21+
"specId": "spec1",
22+
"priority": 10
2223
}
2324
}`;
2425

@@ -29,6 +30,7 @@ describe("lambdaEnv", () => {
2930
lv1: {
3031
supplierId: "supplier1",
3132
specId: "spec1",
33+
priority: 10,
3234
},
3335
},
3436
});

lambdas/supplier-allocator/src/config/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const LetterVariantSchema = z.record(
55
z.object({
66
supplierId: z.string(),
77
specId: z.string(),
8+
priority: z.int().min(0).max(99), // Lower number represents a higher priority
89
}),
910
);
1011
export type LetterVariant = z.infer<typeof LetterVariantSchema>;

0 commit comments

Comments
 (0)