Skip to content

Commit 3208f3d

Browse files
CCM-13372 - Select Preferred Pack
1 parent 65d727a commit 3208f3d

9 files changed

Lines changed: 292 additions & 7 deletions

File tree

infrastructure/terraform/components/api/ddb_table_supplier_configuration.tf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ resource "aws_dynamodb_table" "supplier-configuration" {
3030
type = "S"
3131
}
3232

33+
attribute {
34+
name = "packSpecificationId"
35+
type = "S"
36+
}
37+
3338
// The type-index GSI allows us to query for all supplier configurations of a given type (e.g. all letter supplier configurations)
3439
global_secondary_index {
3540
name = "EntityTypeIndex"
@@ -45,6 +50,13 @@ resource "aws_dynamodb_table" "supplier-configuration" {
4550
projection_type = "ALL"
4651
}
4752

53+
global_secondary_index {
54+
name = "packSpecificationId-index"
55+
hash_key = "PK"
56+
range_key = "packSpecificationId"
57+
projection_type = "ALL"
58+
}
59+
4860
point_in_time_recovery {
4961
enabled = true
5062
}

infrastructure/terraform/components/api/module_lambda_supplier_allocator.tf

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,7 @@ data "aws_iam_policy_document" "supplier_allocator_lambda" {
9494

9595
resources = [
9696
aws_dynamodb_table.supplier-configuration.arn,
97-
"${aws_dynamodb_table.supplier-configuration.arn}/index/volumeGroup-index"
98-
97+
"${aws_dynamodb_table.supplier-configuration.arn}/index/*"
9998
]
10099
}
101100
}

internal/datastore/src/__test__/db.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,22 @@ const createSupplierConfigTableCommand = new CreateTableCommand({
165165
ProjectionType: "ALL",
166166
},
167167
},
168+
{
169+
IndexName: "packSpecificationId-index",
170+
KeySchema: [
171+
{ AttributeName: "PK", KeyType: "HASH" }, // Partition key for GSI
172+
{ AttributeName: "packSpecificationId", KeyType: "RANGE" }, // Sort key for GSI
173+
],
174+
Projection: {
175+
ProjectionType: "ALL",
176+
},
177+
},
168178
],
169179
AttributeDefinitions: [
170180
{ AttributeName: "PK", AttributeType: "S" },
171181
{ AttributeName: "SK", AttributeType: "S" },
172182
{ AttributeName: "volumeGroup", AttributeType: "S" },
183+
{ AttributeName: "packSpecificationId", AttributeType: "S" },
173184
],
174185
});
175186

internal/datastore/src/__test__/supplier-config-repository.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,89 @@ describe("SupplierConfigRepository", () => {
263263
`Supplier with id ${supplierId} not found`,
264264
);
265265
});
266+
267+
test("getSupplierPacksForPackSpecification returns correct supplier packs", async () => {
268+
const packSpecId = "pack-spec-123";
269+
const supplierId = "supplier-123";
270+
const supplierPackId = "supplier-pack-123";
271+
272+
await dbContext.docClient.send(
273+
new PutCommand({
274+
TableName: dbContext.config.supplierConfigTableName,
275+
Item: {
276+
PK: "SUPPLIER_PACK",
277+
SK: supplierPackId,
278+
id: supplierPackId,
279+
packSpecificationId: packSpecId,
280+
supplierId,
281+
status: "PROD",
282+
approval: "APPROVED",
283+
},
284+
}),
285+
);
286+
287+
const result =
288+
await repository.getSupplierPacksForPackSpecification(packSpecId);
289+
expect(result).toEqual([
290+
{
291+
approval: "APPROVED",
292+
id: supplierPackId,
293+
packSpecificationId: packSpecId,
294+
supplierId,
295+
status: "PROD",
296+
},
297+
]);
298+
});
299+
300+
test("getSupplierPacksForPackSpecification throws error for non-existent pack specification", async () => {
301+
const packSpecId = "non-existent-pack-spec";
302+
303+
await expect(
304+
repository.getSupplierPacksForPackSpecification(packSpecId),
305+
).rejects.toThrow(
306+
`No supplier packs found for pack specification id ${packSpecId}`,
307+
);
308+
});
309+
310+
test("getPackSpecification returns correct pack specification details", async () => {
311+
const packSpecId = "pack-spec-123";
312+
313+
await dbContext.docClient.send(
314+
new PutCommand({
315+
TableName: dbContext.config.supplierConfigTableName,
316+
Item: {
317+
PK: "PACK_SPECIFICATION",
318+
SK: packSpecId,
319+
id: packSpecId,
320+
name: `Pack Specification ${packSpecId}`,
321+
createdAt: new Date().toISOString(),
322+
updatedAt: new Date().toISOString(),
323+
version: 1,
324+
billingId: `billing-${packSpecId}`,
325+
postage: { id: "postageId", size: "STANDARD" },
326+
status: "PROD",
327+
},
328+
}),
329+
);
330+
331+
const result = await repository.getPackSpecification(packSpecId);
332+
expect(result).toEqual({
333+
billingId: `billing-${packSpecId}`,
334+
createdAt: expect.any(String),
335+
id: packSpecId,
336+
name: `Pack Specification ${packSpecId}`,
337+
postage: { id: "postageId", size: "STANDARD" },
338+
updatedAt: expect.any(String),
339+
version: 1,
340+
status: "PROD",
341+
});
342+
});
343+
344+
test("getPackSpecification throws error for non-existent pack specification", async () => {
345+
const packSpecId = "non-existent-pack-spec";
346+
347+
await expect(repository.getPackSpecification(packSpecId)).rejects.toThrow(
348+
`No pack specification found for id ${packSpecId}`,
349+
);
350+
});
266351
});

internal/datastore/src/supplier-config-repository.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import {
55
} from "@aws-sdk/lib-dynamodb";
66
import {
77
$LetterVariant,
8+
$PackSpecification,
89
$Supplier,
910
$SupplierAllocation,
11+
$SupplierPack,
1012
$VolumeGroup,
1113
LetterVariant,
14+
PackSpecification,
1215
Supplier,
1316
SupplierAllocation,
17+
SupplierPack,
1418
VolumeGroup,
1519
} from "@nhsdigital/nhs-notify-event-schemas-supplier-config";
1620

@@ -97,4 +101,44 @@ export class SupplierConfigRepository {
97101
}
98102
return suppliers;
99103
}
104+
105+
async getSupplierPacksForPackSpecification(
106+
packSpecId: string,
107+
): Promise<SupplierPack[]> {
108+
const result = await this.ddbClient.send(
109+
new QueryCommand({
110+
TableName: this.config.supplierConfigTableName,
111+
IndexName: "packSpecificationId-index",
112+
KeyConditionExpression: "#pk = :pk AND #packSpecId = :packSpecId",
113+
ExpressionAttributeNames: {
114+
"#pk": "PK",
115+
"#packSpecId": "packSpecificationId",
116+
},
117+
ExpressionAttributeValues: {
118+
":pk": "SUPPLIER_PACK",
119+
":packSpecId": packSpecId,
120+
},
121+
}),
122+
);
123+
if (!result.Items || result.Items.length === 0) {
124+
throw new Error(
125+
`No supplier packs found for pack specification id ${packSpecId}`,
126+
);
127+
}
128+
129+
return $SupplierPack.array().parse(result.Items);
130+
}
131+
132+
async getPackSpecification(packSpecId: string): Promise<PackSpecification> {
133+
const result = await this.ddbClient.send(
134+
new GetCommand({
135+
TableName: this.config.supplierConfigTableName,
136+
Key: { PK: "PACK_SPECIFICATION", SK: packSpecId },
137+
}),
138+
);
139+
if (!result.Item) {
140+
throw new Error(`No pack specification found for id ${packSpecId}`);
141+
}
142+
return $PackSpecification.parse(result.Item);
143+
}
100144
}

lambdas/supplier-allocator/src/handler/__tests__/allocate-handler.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
12
import { SQSEvent, SQSRecord } from "aws-lambda";
23
import pino from "pino";
3-
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
44
import { LetterRequestPreparedEventV2 } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering";
55
import { LetterRequestPreparedEvent } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1";
66
import {
@@ -153,6 +153,17 @@ function setupDefaultMocks() {
153153
priority: 1,
154154
billingId: "billing-1",
155155
});
156+
(supplierConfig.getPreferredSupplierPacks as jest.Mock).mockResolvedValue([
157+
{
158+
packSpecificationId: "pack-spec-1",
159+
},
160+
]);
161+
(supplierConfig.getPackSpecification as jest.Mock).mockResolvedValue({
162+
id: "pack-spec-1",
163+
type: "A4",
164+
colour: false,
165+
duplex: false,
166+
});
156167
}
157168

158169
describe("createSupplierAllocatorHandler", () => {

lambdas/supplier-allocator/src/handler/allocate-handler.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ import { SendMessageCommand } from "@aws-sdk/client-sqs";
33
import { LetterRequestPreparedEvent } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1";
44
import {
55
LetterVariant,
6+
PackSpecification,
67
Supplier,
78
SupplierAllocation,
9+
SupplierPack,
810
VolumeGroup,
911
} from "@nhsdigital/nhs-notify-event-schemas-supplier-config";
1012
import { LetterRequestPreparedEventV2 } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering";
1113
import z from "zod";
1214
import { Unit } from "aws-embedded-metrics";
1315
import { MetricEntry, MetricStatus, buildEMFObject } from "@internal/helpers";
1416
import {
17+
getPackSpecification,
18+
getPreferredSupplierPacks,
1519
getSupplierAllocationsForVolumeGroup,
1620
getSupplierDetails,
1721
getVariantDetails,
@@ -83,19 +87,34 @@ async function getSupplierFromConfig(letterEvent: PreparedEvents, deps: Deps) {
8387
variantDetails.supplierId,
8488
);
8589

86-
const supplierDetails: Supplier[] = await getSupplierDetails(
90+
const allocatedSuppliers: Supplier[] = await getSupplierDetails(
8791
supplierAllocations,
8892
deps,
8993
);
94+
95+
const preferredSupplierPacks: SupplierPack[] =
96+
await getPreferredSupplierPacks(
97+
variantDetails.packSpecificationIds,
98+
allocatedSuppliers,
99+
deps,
100+
);
101+
102+
const preferredPack: PackSpecification = await getPackSpecification(
103+
preferredSupplierPacks[0].packSpecificationId,
104+
deps,
105+
);
106+
90107
deps.logger.info({
91108
description: "Fetched supplier details for supplier allocations",
92109
variantId: letterEvent.data.letterVariantId,
93110
volumeGroupId: volumeGroupDetails.id,
94111
supplierAllocationIds: supplierAllocations.map((a) => a.id),
95-
supplierDetails,
112+
allocatedSuppliers,
113+
preferredSupplierPacks,
114+
preferredPack,
96115
});
97116

98-
return supplierDetails;
117+
return allocatedSuppliers;
99118
} catch (error) {
100119
deps.logger.error({
101120
description: "Error fetching supplier from config",

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

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {
2+
getPackSpecification,
3+
getPreferredSupplierPacks,
24
getSupplierAllocationsForVolumeGroup,
35
getSupplierDetails,
46
getVariantDetails,
@@ -31,7 +33,7 @@ describe("supplier-config service", () => {
3133
afterEach(() => jest.resetAllMocks());
3234

3335
describe("getVariantDetails", () => {
34-
it("returns variant details", async () => {
36+
it("returns variant details for valid id", async () => {
3537
const variant = { id: "v1", volumeGroupId: "g1" } as any;
3638
const deps = makeDeps();
3739
deps.supplierConfigRepo.getLetterVariant = jest
@@ -336,4 +338,67 @@ describe("supplier-config service", () => {
336338
expect(result).toEqual([suppliers[0], suppliers[2]]);
337339
expect(result.every((s) => s.status === "PROD")).toBe(true);
338340
});
341+
describe("getPreferredSupplierPacks", () => {
342+
it("returns preferred supplier packs when found", async () => {
343+
const suppliers = [
344+
{ id: "s1", name: "Supplier 1", status: "PROD" },
345+
{ id: "s2", name: "Supplier 2", status: "PROD" },
346+
] as any[];
347+
const supplierPacks = [
348+
{ id: "p1", supplierId: "s1", packSpecificationId: "spec1" },
349+
{ id: "p2", supplierId: "s2", packSpecificationId: "spec1" },
350+
{ id: "p3", supplierId: "s3", packSpecificationId: "spec1" },
351+
] as any[];
352+
const deps = makeDeps();
353+
deps.supplierConfigRepo.getSupplierPacksForPackSpecification = jest
354+
.fn()
355+
.mockResolvedValue(supplierPacks);
356+
357+
const result = await getPreferredSupplierPacks(
358+
["spec1"],
359+
suppliers,
360+
deps,
361+
);
362+
363+
expect(result).toEqual([
364+
{ id: "p1", supplierId: "s1", packSpecificationId: "spec1" },
365+
{ id: "p2", supplierId: "s2", packSpecificationId: "spec1" },
366+
]);
367+
});
368+
369+
it("throws when no preferred supplier packs found", async () => {
370+
const suppliers = [
371+
{ id: "s1", name: "Supplier 1", status: "PROD" },
372+
{ id: "s2", name: "Supplier 2", status: "PROD" },
373+
] as any[];
374+
const deps = makeDeps();
375+
deps.supplierConfigRepo.getSupplierPacksForPackSpecification = jest
376+
.fn()
377+
.mockResolvedValue([]);
378+
379+
await expect(
380+
getPreferredSupplierPacks(["spec1"], suppliers, deps),
381+
).rejects.toThrow(/No preferred supplier packs found/);
382+
expect(deps.logger.error).toHaveBeenCalledWith(
383+
expect.objectContaining({
384+
description:
385+
"No preferred supplier packs found for pack specification ids and suppliers",
386+
}),
387+
);
388+
});
389+
});
390+
391+
describe("getPackSpecification", () => {
392+
it("returns pack specification when found", async () => {
393+
const packSpec = { id: "spec1", name: "Pack Spec 1" } as any;
394+
const deps = makeDeps();
395+
deps.supplierConfigRepo.getPackSpecification = jest
396+
.fn()
397+
.mockResolvedValue(packSpec);
398+
399+
const result = await getPackSpecification("spec1", deps);
400+
401+
expect(result).toBe(packSpec);
402+
});
403+
});
339404
});

0 commit comments

Comments
 (0)