Skip to content

Commit 64c80b0

Browse files
refactor for clarity
1 parent 227dd44 commit 64c80b0

8 files changed

Lines changed: 210 additions & 131 deletions

File tree

internal/datastore/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"@aws-sdk/client-dynamodb": "^3.984.0",
44
"@aws-sdk/lib-dynamodb": "^3.1008.0",
55
"@internal/helpers": "*",
6-
"@nhsdigital/nhs-notify-event-schemas-supplier-config": "^1.0.1",
6+
"@nhsdigital/nhs-notify-event-schemas-supplier-config": "^1.1.0",
77
"pino": "^10.3.0",
88
"zod": "^4.1.11",
99
"zod-mermaid": "^1.0.9"

lambdas/supplier-allocator/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"@nhsdigital/nhs-notify-event-schemas-letter-rendering": "2.0.1",
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",
11-
"@nhsdigital/nhs-notify-event-schemas-supplier-config": "^1.0.1",
11+
"@nhsdigital/nhs-notify-event-schemas-supplier-config": "^1.1.0",
1212
"@types/aws-lambda": "^8.10.148",
1313
"aws-embedded-metrics": "^4.2.1",
1414
"aws-lambda": "^1.0.7",

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

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import createSupplierAllocatorHandler from "../allocate-handler";
1515
import * as supplierConfig from "../../services/supplier-config";
1616
import * as supplierQuotas from "../../services/supplier-quotas";
17+
import * as allocationConfig from "../allocation-config";
1718

1819
import { Deps } from "../../config/deps";
1920
import { EnvVars } from "../../config/env";
@@ -26,6 +27,7 @@ const renderingSchemaVersion: string =
2627

2728
jest.mock("../../services/supplier-config");
2829
jest.mock("../../services/supplier-quotas");
30+
jest.mock("../allocation-config");
2931

3032
function createSQSEvent(records: SQSRecord[]): SQSEvent {
3133
return {
@@ -154,26 +156,27 @@ function setupDefaultMocks() {
154156
id: "g1",
155157
status: "PROD",
156158
});
157-
(
158-
supplierConfig.getSupplierAllocationsForVolumeGroup as jest.Mock
159-
).mockResolvedValue([{ supplier: "s1" }]);
160-
(supplierConfig.getSupplierDetails as jest.Mock).mockResolvedValue({
161-
supplierId: "supplier-1",
162-
specId: "spec-1",
163-
priority: 1,
164-
billingId: "billing-1",
159+
(allocationConfig.eligibleSuppliers as jest.Mock).mockResolvedValue({
160+
supplierAllocations: [{ supplier: "s1", variantId: "v1" }],
161+
suppliers: [{ id: "s1", name: "Supplier 1", status: "PROD" }],
165162
});
166-
(supplierConfig.getPreferredSupplierPacks as jest.Mock).mockResolvedValue([
167-
{
168-
packSpecificationId: "pack-spec-1",
169-
},
170-
]);
171-
(supplierConfig.getPackSpecification as jest.Mock).mockResolvedValue({
163+
(allocationConfig.preferredSupplierPack as jest.Mock).mockResolvedValue({
172164
id: "pack-spec-1",
173165
type: "A4",
174166
colour: false,
175167
duplex: false,
176168
});
169+
(allocationConfig.filterSuppliersWithCapacity as jest.Mock).mockResolvedValue(
170+
[{ id: "s1", name: "Supplier 1", status: "PROD" }],
171+
);
172+
(allocationConfig.selectSupplierByFactor as jest.Mock).mockResolvedValue({
173+
id: "s1",
174+
name: "Supplier 1",
175+
status: "PROD",
176+
});
177+
(allocationConfig.suppliersWithValidPack as jest.Mock).mockResolvedValue([
178+
{ id: "s1", name: "Supplier 1", status: "PROD" },
179+
]);
177180
(
178181
supplierQuotas.calculateSupplierAllocatedFactor as jest.Mock
179182
).mockResolvedValue({

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

Lines changed: 43 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,25 @@ import {
55
LetterVariant,
66
PackSpecification,
77
Supplier,
8-
SupplierAllocation,
9-
SupplierPack,
108
VolumeGroup,
119
} from "@nhsdigital/nhs-notify-event-schemas-supplier-config";
1210
import { LetterRequestPreparedEventV2 } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering";
1311
import z from "zod";
1412
import { Unit } from "aws-embedded-metrics";
1513
import { MetricEntry, MetricStatus, buildEMFObject } from "@internal/helpers";
1614
import {
17-
filterPacksForLetter,
18-
getPackSpecification,
19-
getPreferredSupplierPacks,
20-
getSupplierAllocationsForVolumeGroup,
21-
getSupplierDetails,
22-
getSuppliersWithValidPack,
2315
getVariantDetails,
2416
getVolumeGroupDetails,
2517
} from "../services/supplier-config";
18+
import { updateSupplierAllocation } from "../services/supplier-quotas";
2619
import {
27-
calculateSupplierAllocatedFactor,
28-
updateSupplierAllocation,
29-
} from "../services/supplier-quotas";
20+
eligibleSuppliers,
21+
filterSuppliersWithCapacity,
22+
preferredSupplierPack,
23+
selectSupplierByFactor,
24+
suppliersWithValidPack,
25+
} from "./allocation-config";
26+
3027
import { Deps } from "../config/deps";
3128

3229
type SupplierSpec = {
@@ -85,87 +82,68 @@ async function getSupplierFromConfig(
8582
deps: Deps,
8683
): Promise<SupplierDetails | undefined> {
8784
try {
88-
const variantDetails: LetterVariant = await getVariantDetails(
85+
const letterVariant: LetterVariant = await getVariantDetails(
8986
letterEvent.data.letterVariantId,
9087
deps,
9188
);
9289

93-
const volumeGroupDetails: VolumeGroup = await getVolumeGroupDetails(
94-
variantDetails.volumeGroupId,
90+
const volumeGroup: VolumeGroup = await getVolumeGroupDetails(
91+
letterVariant.volumeGroupId,
9592
deps,
9693
);
9794

98-
const supplierAllocations: SupplierAllocation[] =
99-
await getSupplierAllocationsForVolumeGroup(
100-
variantDetails.volumeGroupId,
101-
deps,
102-
variantDetails.supplierId,
103-
);
95+
const { supplierAllocations, suppliers: allocatedSuppliers } =
96+
await eligibleSuppliers(volumeGroup, deps);
10497

105-
const supplierIds = supplierAllocations.map((alloc) => alloc.supplier);
106-
107-
const allocatedSuppliers: Supplier[] = await getSupplierDetails(
108-
supplierIds,
109-
deps,
110-
);
111-
112-
const eligiblePacks: string[] = await filterPacksForLetter(
98+
const preferredPack: PackSpecification = await preferredSupplierPack(
11399
letterEvent,
114-
variantDetails.packSpecificationIds,
115-
deps,
116-
);
117-
118-
const preferredSupplierPacks: SupplierPack[] =
119-
await getPreferredSupplierPacks(eligiblePacks, allocatedSuppliers, deps);
120-
121-
const preferredPack: PackSpecification = await getPackSpecification(
122-
preferredSupplierPacks[0].packSpecificationId,
100+
allocatedSuppliers,
101+
letterVariant.packSpecificationIds,
123102
deps,
124103
);
125104

126-
const suppliersForPack: Supplier[] = await getSuppliersWithValidPack(
105+
const allSuppliersForPack: Supplier[] = await suppliersWithValidPack(
127106
allocatedSuppliers,
128107
preferredPack.id,
129108
deps,
130109
);
131110

132-
let supplierAllocationsForPack: SupplierAllocation[] = [];
133-
let supplierFactors: { supplierId: string; factor: number }[] = [];
134-
let selectedSupplierId = "unknown"; // Default to first supplier if no allocations or factors can be calculated
135-
if (suppliersForPack && suppliersForPack.length > 0) {
136-
supplierAllocationsForPack = supplierAllocations.filter((alloc) =>
137-
suppliersForPack.some((supplier) => supplier.id === alloc.supplier),
111+
const suppliersForPackWithCapacity: Supplier[] =
112+
await filterSuppliersWithCapacity(
113+
allSuppliersForPack,
114+
volumeGroup.id,
115+
deps,
138116
);
139117

140-
supplierFactors = await calculateSupplierAllocatedFactor(
141-
supplierAllocationsForPack,
118+
// selected supplier id is determined by first calling selectSupplierByFactor for suppliers with capacity and if nothing is returned tryong again with all suppliers for pack
119+
const selectedSupplierId =
120+
(await selectSupplierByFactor(
121+
suppliersForPackWithCapacity,
122+
supplierAllocations,
142123
deps,
143-
);
124+
)) ??
125+
(await selectSupplierByFactor(
126+
allSuppliersForPack,
127+
supplierAllocations,
128+
deps,
129+
));
144130

145-
// Get the supplierid with the lowest factor
146-
selectedSupplierId = supplierFactors[0].supplierId;
147-
let lowestFactor = supplierFactors[0].factor;
148-
for (const supplierFactor of supplierFactors) {
149-
if (supplierFactor.factor < lowestFactor) {
150-
lowestFactor = supplierFactor.factor;
151-
selectedSupplierId = supplierFactor.supplierId;
152-
}
153-
}
131+
if (!selectedSupplierId) {
132+
throw new Error(
133+
"No suppliers found with capacity or valid allocation factor for preferred pack",
134+
);
154135
}
155136

156137
deps.logger.info({
157138
description: "Fetched supplier details for supplier allocations",
158139
variantId: letterEvent.data.letterVariantId,
159-
volumeGroupId: volumeGroupDetails.id,
140+
volumeGroupId: volumeGroup.id,
160141
supplierAllocationIds: supplierAllocations.map((a) => a.id),
161142
allocatedSuppliers,
162-
variantPacks: variantDetails.packSpecificationIds,
163-
eligiblePacks,
164-
preferredSupplierPacks,
165-
preferredPack,
166-
suppliersForPack,
167-
supplierAllocationsForPack,
168-
supplierFactors,
143+
allSuppliersForPack: allSuppliersForPack.map((s) => s.id),
144+
suppliersForPackWithCapacity: suppliersForPackWithCapacity.map(
145+
(s) => s.id,
146+
),
169147
selectedSupplierId,
170148
});
171149

@@ -176,7 +154,7 @@ async function getSupplierFromConfig(
176154
priority: 0,
177155
billingId: preferredPack.billingId,
178156
},
179-
volumeGroupId: volumeGroupDetails.id,
157+
volumeGroupId: volumeGroup.id,
180158
};
181159
deps.logger.info({
182160
description: "Resolved supplier details for letter event",
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { LetterRequestPreparedEvent } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1";
2+
import { LetterRequestPreparedEventV2 } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering";
3+
import {
4+
PackSpecification,
5+
Supplier,
6+
SupplierAllocation,
7+
SupplierPack,
8+
VolumeGroup,
9+
} from "@nhsdigital/nhs-notify-event-schemas-supplier-config";
10+
import {
11+
filterPacksForLetter,
12+
getPackSpecification,
13+
getPreferredSupplierPacks,
14+
getSupplierAllocationsForVolumeGroup,
15+
getSupplierDetails,
16+
getSupplierPacks,
17+
} from "../services/supplier-config";
18+
import { calculateSupplierAllocatedFactor } from "../services/supplier-quotas";
19+
import { Deps } from "../config/deps";
20+
21+
type PreparedEvents = LetterRequestPreparedEventV2 | LetterRequestPreparedEvent;
22+
23+
export async function eligibleSuppliers(
24+
volumeGroup: VolumeGroup,
25+
deps: Deps,
26+
): Promise<{
27+
supplierAllocations: SupplierAllocation[];
28+
suppliers: Supplier[];
29+
}> {
30+
const supplierAllocations = await getSupplierAllocationsForVolumeGroup(
31+
volumeGroup.id,
32+
deps,
33+
);
34+
const supplierIds = supplierAllocations.map((alloc) => alloc.supplier);
35+
36+
const suppliers = await getSupplierDetails(supplierIds, deps);
37+
return { supplierAllocations, suppliers };
38+
}
39+
40+
export async function preferredSupplierPack(
41+
letterEvent: PreparedEvents,
42+
suppliers: Supplier[],
43+
packSpecificationIds: string[],
44+
deps: Deps,
45+
): Promise<PackSpecification> {
46+
const eligiblePacks: string[] = await filterPacksForLetter(
47+
letterEvent,
48+
packSpecificationIds,
49+
deps,
50+
);
51+
const preferredSupplierPacks: SupplierPack[] =
52+
await getPreferredSupplierPacks(eligiblePacks, suppliers, deps);
53+
const preferredPack: PackSpecification = await getPackSpecification(
54+
preferredSupplierPacks[0].packSpecificationId,
55+
deps,
56+
);
57+
return preferredPack;
58+
}
59+
60+
// This function is used to filter the allocated suppliers based on those that support the supplied pack specification
61+
export async function suppliersWithValidPack(
62+
suppliers: Supplier[],
63+
packSpecificationId: string,
64+
deps: Deps,
65+
): Promise<Supplier[]> {
66+
const validSuppliers: Supplier[] = [];
67+
const supplierPacks = await getSupplierPacks(packSpecificationId, deps);
68+
69+
for (const supplier of suppliers) {
70+
const hasValidPack = supplierPacks.some(
71+
(pack) => pack.supplierId === supplier.id,
72+
);
73+
if (hasValidPack) {
74+
validSuppliers.push(supplier);
75+
}
76+
}
77+
78+
return validSuppliers;
79+
}
80+
81+
export async function filterSuppliersWithCapacity(
82+
suppliers: Supplier[],
83+
volumeGroupId: string,
84+
deps: Deps,
85+
): Promise<Supplier[]> {
86+
const dailyAllocationDate = new Date().toISOString().split("T")[0]; // Get current date in YYYY-MM-DD format
87+
const dailyAllocation = await deps.supplierQuotasRepo.getDailyAllocation(
88+
volumeGroupId,
89+
dailyAllocationDate,
90+
);
91+
if (dailyAllocation) {
92+
const suppliersWithCapacity = suppliers.filter((supplier) => {
93+
const allocated = dailyAllocation.allocations[supplier.id] ?? 0;
94+
return allocated < supplier.dailyCapacity;
95+
});
96+
return suppliersWithCapacity;
97+
}
98+
return suppliers; // If no daily allocation exists, assume all suppliers have capacity
99+
}
100+
101+
export async function selectSupplierByFactor(
102+
suppliers: Supplier[],
103+
supplierAllocations: SupplierAllocation[],
104+
deps: Deps,
105+
): Promise<string> {
106+
const supplierAllocationsForPack = supplierAllocations.filter((alloc) =>
107+
suppliers.some((supplier) => supplier.id === alloc.supplier),
108+
);
109+
const supplierFactors: { supplierId: string; factor: number }[] =
110+
await calculateSupplierAllocatedFactor(supplierAllocationsForPack, deps);
111+
112+
deps.logger.info({
113+
description: "Calculated supplier factors for allocation",
114+
supplierFactors,
115+
});
116+
let selectedSupplierId = supplierFactors[0].supplierId;
117+
let lowestFactor = supplierFactors[0].factor;
118+
for (const supplierFactor of supplierFactors) {
119+
if (supplierFactor.factor < lowestFactor) {
120+
lowestFactor = supplierFactor.factor;
121+
selectedSupplierId = supplierFactor.supplierId;
122+
}
123+
}
124+
return selectedSupplierId;
125+
}

0 commit comments

Comments
 (0)