Skip to content

Commit ce798b8

Browse files
calculate allocation factors
1 parent 7512a20 commit ce798b8

12 files changed

Lines changed: 363 additions & 4 deletions

File tree

internal/datastore/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from "./mi-repository";
33
export * from "./letter-repository";
44
export * from "./supplier-repository";
55
export * from "./supplier-config-repository";
6+
export * from "./supplier-quotas-repository";
67
export { default as LetterQueueRepository } from "./letter-queue-repository";
78
export { default as DBHealthcheck } from "./healthcheck";
89
export { default as LetterAlreadyExistsError } from "./errors/letter-already-exists-error";

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export class SupplierConfigRepository {
133133
const result = await this.ddbClient.send(
134134
new GetCommand({
135135
TableName: this.config.supplierConfigTableName,
136-
Key: { pk: "ENTITY#pack_specification", sk: `ID#${packSpecId}` },
136+
Key: { pk: "ENTITY#pack-specification", sk: `ID#${packSpecId}` },
137137
}),
138138
);
139139
if (!result.Item) {
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {
2+
DynamoDBDocumentClient,
3+
GetCommand,
4+
PutCommand,
5+
UpdateCommand,
6+
} from "@aws-sdk/lib-dynamodb";
7+
import {
8+
$DailyAllocation,
9+
$OverallAllocation,
10+
DailyAllocation,
11+
OverallAllocation,
12+
} from "./types";
13+
14+
export type SupplierQuotasRepositoryConfig = {
15+
supplierQuotasTableName: string;
16+
};
17+
18+
function ItemForRecord(
19+
entity: string,
20+
id: string,
21+
record: Record<string, any>,
22+
): Record<string, any> {
23+
return {
24+
pk: `ENTITY#${entity}`,
25+
sk: `ID#${id}`,
26+
...record,
27+
};
28+
}
29+
30+
export class SupplierQuotasRepository {
31+
constructor(
32+
readonly ddbClient: DynamoDBDocumentClient,
33+
readonly config: SupplierQuotasRepositoryConfig,
34+
) {}
35+
36+
async getOverallAllocation(groupId: string): Promise<OverallAllocation> {
37+
const result = await this.ddbClient.send(
38+
new GetCommand({
39+
TableName: this.config.supplierQuotasTableName,
40+
Key: { pk: "ENTITY#overall-allocation", sk: `ID#${groupId}` },
41+
}),
42+
);
43+
if (!result.Item) {
44+
throw new Error(
45+
`No overall allocation found for volume group id ${groupId}`,
46+
);
47+
}
48+
return $OverallAllocation.parse(result.Item);
49+
}
50+
51+
async putOverallAllocation(allocation: OverallAllocation): Promise<void> {
52+
await this.ddbClient.send(
53+
new PutCommand({
54+
TableName: this.config.supplierQuotasTableName,
55+
Item: ItemForRecord(
56+
"overall-allocation",
57+
allocation.id,
58+
$OverallAllocation.parse(allocation),
59+
),
60+
}),
61+
);
62+
}
63+
64+
// Update the overallAllocation table updating the allocations array for a given volume group
65+
// or adding the value if the supplier is not present //
66+
async updateOverallAllocation(
67+
groupId: string,
68+
supplierId: string,
69+
newAllocation: number,
70+
): Promise<void> {
71+
const overallAllocation = await this.getOverallAllocation(groupId);
72+
const currentAllocation = overallAllocation.allocations[supplierId] ?? 0;
73+
const updatedAllocation = currentAllocation + newAllocation;
74+
75+
await this.ddbClient.send(
76+
new UpdateCommand({
77+
TableName: this.config.supplierQuotasTableName,
78+
Key: { pk: "ENTITY#overall-allocation", sk: `ID#${groupId}` },
79+
UpdateExpression:
80+
"SET allocations.#supplierId = :updatedAllocation, updatedAt = :updatedAt",
81+
ExpressionAttributeNames: {
82+
"#supplierId": supplierId,
83+
},
84+
ExpressionAttributeValues: {
85+
":updatedAllocation": updatedAllocation,
86+
":updatedAt": new Date().toISOString(),
87+
},
88+
}),
89+
);
90+
}
91+
92+
async getDailyAllocation(
93+
groupId: string,
94+
date: string,
95+
): Promise<DailyAllocation> {
96+
const result = await this.ddbClient.send(
97+
new GetCommand({
98+
TableName: this.config.supplierQuotasTableName,
99+
Key: {
100+
pk: "ENTITY#daily-allocation",
101+
sk: `ID#${groupId}#DATE#${date}`,
102+
},
103+
}),
104+
);
105+
if (!result.Item) {
106+
throw new Error(
107+
`No daily allocation found for volume group id ${groupId} and date ${date}`,
108+
);
109+
}
110+
return $DailyAllocation.parse(result.Item);
111+
}
112+
113+
async putDailyAllocation(allocation: DailyAllocation): Promise<void> {
114+
await this.ddbClient.send(
115+
new PutCommand({
116+
TableName: this.config.supplierQuotasTableName,
117+
Item: ItemForRecord(
118+
"daily-allocation",
119+
`${allocation.volumeGroup}#DATE#${allocation.date}`,
120+
$DailyAllocation.parse(allocation),
121+
),
122+
}),
123+
);
124+
}
125+
126+
async updateDailyAllocation(
127+
groupId: string,
128+
date: string,
129+
supplierId: string,
130+
newAllocation: number,
131+
): Promise<void> {
132+
const dailyAllocation = await this.getDailyAllocation(groupId, date);
133+
const currentAllocation = dailyAllocation.allocations[supplierId] ?? 0;
134+
const updatedAllocation = currentAllocation + newAllocation;
135+
136+
await this.ddbClient.send(
137+
new UpdateCommand({
138+
TableName: this.config.supplierQuotasTableName,
139+
Key: {
140+
pk: "ENTITY#daily-allocation",
141+
sk: `ID#${groupId}#DATE#${date}`,
142+
},
143+
UpdateExpression:
144+
"SET allocations.#supplierId = :updatedAllocation, updatedAt = :updatedAt",
145+
ExpressionAttributeNames: {
146+
"#supplierId": supplierId,
147+
},
148+
ExpressionAttributeValues: {
149+
":updatedAllocation": updatedAllocation,
150+
":updatedAt": new Date().toISOString(),
151+
},
152+
}),
153+
);
154+
}
155+
}

internal/datastore/src/types.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { z } from "zod";
22
import { idRef } from "@internal/helpers";
3+
import {
4+
$Supplier,
5+
$VolumeGroup,
6+
} from "@nhsdigital/nhs-notify-event-schemas-supplier-config";
37

48
export const SupplierStatus = z.enum(["ENABLED", "DISABLED"]);
59

@@ -120,3 +124,38 @@ export const MISchema = MISchemaBase.extend({
120124

121125
export type MI = z.infer<typeof MISchema>;
122126
export type MIBase = z.infer<typeof MISchemaBase>;
127+
128+
export const $OverallAllocation = z
129+
.object({
130+
id: z.string(),
131+
volumeGroup: idRef($VolumeGroup, "id"),
132+
allocations: z.record(
133+
idRef($Supplier, "id"),
134+
z.number().int().nonnegative(),
135+
),
136+
})
137+
.meta({
138+
title: "OverallAllocation",
139+
description:
140+
"The overall allocation for a volume group, including all suppliers",
141+
});
142+
143+
export type OverallAllocation = z.infer<typeof $OverallAllocation>;
144+
145+
export const $DailyAllocation = z
146+
.object({
147+
id: z.string(),
148+
date: z.ZodISODate,
149+
volumeGroup: idRef($VolumeGroup, "id"),
150+
allocations: z.record(
151+
idRef($Supplier, "id"),
152+
z.number().int().nonnegative(),
153+
),
154+
})
155+
.meta({
156+
title: "DailyAllocation",
157+
description:
158+
"The daily allocation for a volume group, including all suppliers",
159+
});
160+
161+
export type DailyAllocation = z.infer<typeof $DailyAllocation>;

lambdas/supplier-allocator/jest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const baseJestConfig = {
1414
clearMocks: true,
1515

1616
// Indicates whether the coverage information should be collected while executing the test
17-
collectCoverage: true,
17+
collectCoverage: false,
1818

1919
// The directory where Jest should output its coverage files
2020
coverageDirectory: "./.reports/unit/coverage",

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Deps } from "lambdas/supplier-allocator/src/config/deps";
33
describe("createDependenciesContainer", () => {
44
const env = {
55
SUPPLIER_CONFIG_TABLE_NAME: "SupplierConfigTable",
6+
SUPPLIER_QUOTAS_TABLE_NAME: "SupplierQuotasTable",
67
VARIANT_MAP: {
78
lv1: {
89
supplierId: "supplier1",
@@ -30,6 +31,7 @@ describe("createDependenciesContainer", () => {
3031
// Repo client
3132
jest.mock("@internal/datastore", () => ({
3233
SupplierConfigRepository: jest.fn(),
34+
SupplierQuotasRepository: jest.fn(),
3335
}));
3436

3537
// Env
@@ -42,6 +44,9 @@ describe("createDependenciesContainer", () => {
4244
const { SupplierConfigRepository } = jest.requireMock(
4345
"@internal/datastore",
4446
);
47+
const { SupplierQuotasRepository } = jest.requireMock(
48+
"@internal/datastore",
49+
);
4550
// eslint-disable-next-line @typescript-eslint/no-require-imports
4651
const { createDependenciesContainer } = require("../deps");
4752
const deps: Deps = createDependenciesContainer();
@@ -51,6 +56,11 @@ describe("createDependenciesContainer", () => {
5156
expect(supplierConfigRepoCtorArgs[1]).toEqual({
5257
supplierConfigTableName: "SupplierConfigTable",
5358
});
59+
expect(SupplierQuotasRepository).toHaveBeenCalledTimes(1);
60+
const supplierQuotasRepoCtorArgs = SupplierQuotasRepository.mock.calls[0];
61+
expect(supplierQuotasRepoCtorArgs[1]).toEqual({
62+
supplierQuotasTableName: "SupplierQuotasTable",
63+
});
5464
expect(deps.env).toEqual(env);
5565
});
5666
});

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe("lambdaEnv", () => {
1616

1717
it("should load all environment variables successfully", () => {
1818
process.env.SUPPLIER_CONFIG_TABLE_NAME = "SupplierConfigTable";
19+
process.env.SUPPLIER_QUOTAS_TABLE_NAME = "SupplierQuotasTable";
1920
process.env.VARIANT_MAP = `{
2021
"lv1": {
2122
"supplierId": "supplier1",
@@ -29,6 +30,7 @@ describe("lambdaEnv", () => {
2930

3031
expect(envVars).toEqual({
3132
SUPPLIER_CONFIG_TABLE_NAME: "SupplierConfigTable",
33+
SUPPLIER_QUOTAS_TABLE_NAME: "SupplierQuotasTable",
3234
VARIANT_MAP: {
3335
lv1: {
3436
supplierId: "supplier1",

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
33
import { SQSClient } from "@aws-sdk/client-sqs";
44
import { Logger } from "pino";
55
import { createLogger } from "@internal/helpers";
6-
import { SupplierConfigRepository } from "@internal/datastore";
6+
import {
7+
SupplierConfigRepository,
8+
SupplierQuotasRepository,
9+
} from "@internal/datastore";
710
import { EnvVars, envVars } from "./env";
811

912
export type Deps = {
1013
supplierConfigRepo: SupplierConfigRepository;
14+
supplierQuotasRepo: SupplierQuotasRepository;
1115
logger: Logger;
1216
env: EnvVars;
1317
sqsClient: SQSClient;
@@ -30,11 +34,24 @@ function createSupplierConfigRepository(
3034
return new SupplierConfigRepository(createDocumentClient(), config);
3135
}
3236

37+
function createSupplierQuotasRepository(
38+
log: Logger,
39+
// eslint-disable-next-line @typescript-eslint/no-shadow
40+
envVars: EnvVars,
41+
): SupplierQuotasRepository {
42+
const config = {
43+
supplierQuotasTableName: envVars.SUPPLIER_QUOTAS_TABLE_NAME,
44+
};
45+
46+
return new SupplierQuotasRepository(createDocumentClient(), config);
47+
}
48+
3349
export function createDependenciesContainer(): Deps {
3450
const log = createLogger({ logLevel: envVars.PINO_LOG_LEVEL });
3551

3652
return {
3753
supplierConfigRepo: createSupplierConfigRepository(log, envVars),
54+
supplierQuotasRepo: createSupplierQuotasRepository(log, envVars),
3855
logger: log,
3956
env: envVars,
4057
sqsClient: new SQSClient({}),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type LetterVariant = z.infer<typeof LetterVariantSchema>;
1313

1414
const EnvVarsSchema = z.object({
1515
SUPPLIER_CONFIG_TABLE_NAME: z.string(),
16+
SUPPLIER_QUOTAS_TABLE_NAME: z.string(),
1617
PINO_LOG_LEVEL: z.coerce.string().optional(),
1718
VARIANT_MAP: z.string().transform((str, _) => {
1819
const parsed = JSON.parse(str);

0 commit comments

Comments
 (0)