Skip to content

Commit dd1812d

Browse files
authored
CCM-15185 List Letters uses new queue (#508)
* CCM-15185 List Letters uses new queue * Don't swallow asdf install terraform failure * Prettier fixes * Type fix * type fix * type cleanup * Address co-pilot review comments * Correct visibility timeout to match ticket * Remove duplication
1 parent 9b1e49c commit dd1812d

16 files changed

Lines changed: 421 additions & 439 deletions

File tree

infrastructure/terraform/components/api/locals.tf

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,19 @@ locals {
2222
destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"
2323

2424
common_lambda_env_vars = {
25-
APIM_CORRELATION_HEADER = "nhsd-correlation-id",
26-
DOWNLOAD_URL_TTL_SECONDS = 60
27-
EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters"
28-
LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours
29-
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
30-
MI_TABLE_NAME = aws_dynamodb_table.mi.name,
31-
MI_TTL_HOURS = 2160 # 90 days * 24 hours
32-
SNS_TOPIC_ARN = "${module.eventsub.sns_topic.arn}",
33-
SUPPLIER_CONFIG_TABLE_NAME = aws_dynamodb_table.supplier-configuration.name
34-
SUPPLIER_ID_HEADER = "nhsd-supplier-id",
25+
APIM_CORRELATION_HEADER = "nhsd-correlation-id",
26+
DOWNLOAD_URL_TTL_SECONDS = 60
27+
EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters"
28+
LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours
29+
LETTER_QUEUE_TABLE_NAME = aws_dynamodb_table.letter_queue.name,
30+
LETTER_QUEUE_TTL_HOURS = 168 # 7 days * 24 hours
31+
LETTER_QUEUE_VISIBILITY_TIMEOUT = 300, # 5 minutes * 60 seconds
32+
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
33+
MI_TABLE_NAME = aws_dynamodb_table.mi.name,
34+
MI_TTL_HOURS = 2160 # 90 days * 24 hours
35+
SNS_TOPIC_ARN = "${module.eventsub.sns_topic.arn}",
36+
SUPPLIER_CONFIG_TABLE_NAME = aws_dynamodb_table.supplier-configuration.name
37+
SUPPLIER_ID_HEADER = "nhsd-supplier-id",
3538
}
3639

3740
core_pdf_bucket_arn = "arn:aws:s3:::comms-${var.core_account_id}-eu-west-2-${var.core_environment}-api-stg-pdf-pipeline"

infrastructure/terraform/components/api/module_lambda_get_letters.tf

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,12 @@ data "aws_iam_policy_document" "get_letters_lambda" {
6363
"dynamodb:GetItem",
6464
"dynamodb:Query",
6565
"dynamodb:Scan",
66+
"dynamodb:UpdateItem",
6667
]
6768

6869
resources = [
69-
aws_dynamodb_table.letters.arn,
70-
"${aws_dynamodb_table.letters.arn}/index/supplierStatus-index"
70+
aws_dynamodb_table.letter_queue.arn,
71+
"${aws_dynamodb_table.letter_queue.arn}/index/queueSortOrder-index"
7172
]
7273
}
7374
}

infrastructure/terraform/components/api/module_lambda_update_letter_queue.tf

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,7 @@ module "update_letter_queue" {
3434
log_destination_arn = local.destination_arn
3535
log_subscription_role_arn = local.acct.log_subscription_role_arn
3636

37-
lambda_env_vars = merge(local.common_lambda_env_vars, {
38-
LETTER_QUEUE_TABLE_NAME = "${local.csi}-letter-queue",
39-
LETTER_QUEUE_TTL_HOURS = 168 # 7 days
40-
})
37+
lambda_env_vars = merge(local.common_lambda_env_vars, {})
4138
}
4239

4340
data "aws_iam_policy_document" "update_letter_queue_lambda" {

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

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe("LetterQueueRepository", () => {
5151
afterEach(async () => {
5252
await deleteTables(db);
5353
jest.useRealTimers();
54+
jest.restoreAllMocks();
5455
});
5556

5657
afterAll(async () => {
@@ -149,6 +150,196 @@ describe("LetterQueueRepository", () => {
149150
).rejects.toThrow("Cannot do operations on a non-existent table");
150151
});
151152
});
153+
154+
describe("getLetters", () => {
155+
it("filters by supplierId", async () => {
156+
await letterQueueRepository.putLetter(createLetter());
157+
158+
const letters = await letterQueueRepository.getLetters("supplier2", 1);
159+
160+
expect(letters).toHaveLength(0);
161+
});
162+
163+
it("filters by visibilityTimestamp", async () => {
164+
const pendingLetter = createLetter();
165+
await letterQueueRepository.putLetter(createLetter());
166+
await letterQueueRepository.updateVisibilityTimestamp(
167+
pendingLetter,
168+
new Date(Date.now() + 600_000),
169+
);
170+
171+
const letters = await letterQueueRepository.getLetters("supplier1", 1);
172+
173+
expect(letters).toHaveLength(0);
174+
});
175+
176+
it("returns letters in timestamp order", async () => {
177+
jest.useFakeTimers().setSystemTime(new Date());
178+
await letterQueueRepository.putLetter(
179+
createLetter({ letterId: "first-letter" }),
180+
);
181+
jest.advanceTimersByTime(1);
182+
await letterQueueRepository.putLetter(
183+
createLetter({ letterId: "second-letter" }),
184+
);
185+
jest.advanceTimersByTime(1);
186+
await letterQueueRepository.putLetter(
187+
createLetter({ letterId: "third-letter" }),
188+
);
189+
jest.advanceTimersByTime(1);
190+
await letterQueueRepository.putLetter(
191+
createLetter({ letterId: "fourth-letter" }),
192+
);
193+
jest.advanceTimersByTime(1);
194+
await letterQueueRepository.putLetter(
195+
createLetter({ letterId: "fifth-letter" }),
196+
);
197+
jest.advanceTimersByTime(1);
198+
199+
const letters = await letterQueueRepository.getLetters("supplier1", 5);
200+
201+
expect(letters[0].letterId).toBe("first-letter");
202+
expect(letters[1].letterId).toBe("second-letter");
203+
expect(letters[2].letterId).toBe("third-letter");
204+
expect(letters[3].letterId).toBe("fourth-letter");
205+
expect(letters[4].letterId).toBe("fifth-letter");
206+
});
207+
208+
it("limits results to the supplied number", async () => {
209+
await letterQueueRepository.putLetter(
210+
createLetter({ letterId: "first-letter" }),
211+
);
212+
await letterQueueRepository.putLetter(
213+
createLetter({ letterId: "second-letter" }),
214+
);
215+
await letterQueueRepository.putLetter(
216+
createLetter({ letterId: "third-letter" }),
217+
);
218+
await letterQueueRepository.putLetter(
219+
createLetter({ letterId: "fourth-letter" }),
220+
);
221+
222+
const letters = await letterQueueRepository.getLetters("supplier1", 3);
223+
224+
expect(letters).toHaveLength(3);
225+
expect(letters[2].letterId).toBe("third-letter");
226+
});
227+
228+
it("applies the limit after filtering on supplier", async () => {
229+
await letterQueueRepository.putLetter(
230+
createLetter({ letterId: "first-letter" }),
231+
);
232+
await letterQueueRepository.putLetter(
233+
createLetter({ letterId: "second-letter", supplierId: "supplier2" }),
234+
);
235+
await letterQueueRepository.putLetter(
236+
createLetter({ letterId: "third-letter" }),
237+
);
238+
await letterQueueRepository.putLetter(
239+
createLetter({ letterId: "fourth-letter" }),
240+
);
241+
242+
const letters = await letterQueueRepository.getLetters("supplier1", 3);
243+
244+
expect(letters).toHaveLength(3);
245+
expect(letters[2].letterId).toBe("fourth-letter");
246+
});
247+
248+
it("applies the limit after filtering on visibilityTimestamp", async () => {
249+
await letterQueueRepository.putLetter(
250+
createLetter({ letterId: "first-letter" }),
251+
);
252+
await letterQueueRepository.putLetter(
253+
createLetter({ letterId: "second-letter" }),
254+
);
255+
await letterQueueRepository.putLetter(
256+
createLetter({ letterId: "third-letter" }),
257+
);
258+
await letterQueueRepository.putLetter(
259+
createLetter({ letterId: "fourth-letter" }),
260+
);
261+
await letterQueueRepository.updateVisibilityTimestamp(
262+
createLetter({ letterId: "second-letter" }),
263+
new Date(Date.now() + 600_000),
264+
);
265+
266+
const letters = await letterQueueRepository.getLetters("supplier1", 3);
267+
268+
expect(letters).toHaveLength(3);
269+
expect(letters[2].letterId).toBe("fourth-letter");
270+
});
271+
272+
it("paginates through multiple DynamoDB pages to reach the limit", async () => {
273+
await letterQueueRepository.putLetter(
274+
createLetter({ letterId: "first-letter" }),
275+
);
276+
await letterQueueRepository.putLetter(
277+
createLetter({ letterId: "second-letter" }),
278+
);
279+
await letterQueueRepository.putLetter(
280+
createLetter({ letterId: "third-letter" }),
281+
);
282+
283+
const pagedRepository = new LetterQueueRepository(db.docClient, logger, {
284+
...db.config,
285+
queryPageSize: 1,
286+
});
287+
288+
const letters = await pagedRepository.getLetters("supplier1", 3);
289+
290+
expect(letters).toHaveLength(3);
291+
expect(letters[0].letterId).toBe("first-letter");
292+
expect(letters[1].letterId).toBe("second-letter");
293+
expect(letters[2].letterId).toBe("third-letter");
294+
});
295+
296+
it("returns an empty array if no items found", async () => {
297+
const letters = await letterQueueRepository.getLetters("supplier1", 3);
298+
299+
expect(letters).toHaveLength(0);
300+
});
301+
});
302+
303+
describe("updateVisibilityTimestamp", () => {
304+
it("updates the visibilityTimestamp on an existing letter", async () => {
305+
const pendingLetter =
306+
await letterQueueRepository.putLetter(createLetter());
307+
308+
await letterQueueRepository.updateVisibilityTimestamp(
309+
pendingLetter,
310+
new Date("2026-03-04T13:15:45.000Z"),
311+
);
312+
313+
const letter = await getLetter(db, "supplier1", "letter1");
314+
expect(letter?.visibilityTimestamp).toBe("2026-03-04T13:15:45.000Z");
315+
});
316+
317+
it("does nothing when the letter does not exist", async () => {
318+
await letterQueueRepository.updateVisibilityTimestamp(
319+
createLetter(),
320+
new Date(),
321+
);
322+
323+
expect(await letterExists(db, "supplier1", "letter1")).toBe(false);
324+
});
325+
326+
it("rethrows errors from DynamoDB when updating the letter", async () => {
327+
const misconfiguredRepository = new LetterQueueRepository(
328+
db.docClient,
329+
logger,
330+
{
331+
...db.config,
332+
letterQueueTableName: "nonexistent-table",
333+
},
334+
);
335+
await expect(
336+
misconfiguredRepository.updateVisibilityTimestamp(
337+
createLetter(),
338+
new Date(),
339+
),
340+
).rejects.toThrow("Cannot do operations on a non-existent table");
341+
});
342+
});
152343
});
153344

154345
async function getLetter(db: DBContext, supplierId: string, letterId: string) {

0 commit comments

Comments
 (0)