From 07968b91a401730399499fa8e79d583734938551 Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Thu, 14 May 2026 11:45:15 +0000 Subject: [PATCH 01/12] tests --- .../letter-allocation.spec.ts | 92 +++++++++++++++++++ tests/helpers/allocation-helper.ts | 49 ++++++++++ 2 files changed, 141 insertions(+) create mode 100644 tests/component-tests/allocation-tests/letter-allocation.spec.ts create mode 100644 tests/helpers/allocation-helper.ts diff --git a/tests/component-tests/allocation-tests/letter-allocation.spec.ts b/tests/component-tests/allocation-tests/letter-allocation.spec.ts new file mode 100644 index 000000000..5a7264466 --- /dev/null +++ b/tests/component-tests/allocation-tests/letter-allocation.spec.ts @@ -0,0 +1,92 @@ +import { expect, test } from "@playwright/test"; +import { sendSnsEvent } from "tests/helpers/send-sns-event"; +import { createPreparedV1Event } from "tests/helpers/event-fixtures"; +import { randomUUID } from "node:crypto"; +import { logger } from "tests/helpers/pino-logger"; +import { + getAllocationLogForDomainId, + getVariantsForAllocation, +} from "tests/helpers/allocation-helper"; + +test.describe("Allocator Lambda Tests", () => { + test.setTimeout(180_000); // 3 minutes for long running polling + + test(`Verify that allocator successfully allocates a letter and emits PENDING event`, async () => { + const domainId = randomUUID(); + logger.info(`Testing event subscription with domainId: ${domainId}`); + + const letterVariant = getVariantsForAllocation(1); + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, + }); + + const response = await sendSnsEvent(preparedEvent); + + expect(response.MessageId).toBeTruthy(); + + const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); + const supplierId = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; + const specId = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.specId; + const billingId = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.billingId; + const allocationStatus = + supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.status; + + if (!supplierId) { + throw new Error("supplierId was not found in supplier allocator log"); + } + + expect(specId).toBeTruthy(); + expect(billingId).toBeTruthy(); + expect(allocationStatus).toBe("PENDING"); + }); + + test("Verify that unknown letter variant is marked as rejected allocation", async () => { + const domainId = randomUUID(); + logger.info(`Testing rejected allocation with domainId: ${domainId}`); + + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: `unknown-variant-${domainId}`, + }); + + const response = await sendSnsEvent(preparedEvent); + + expect(response.MessageId).toBeTruthy(); + + const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); + const supplierId = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; + const allocationStatus = + supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.status; + const reasonCode = + supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.reasonCode; + + expect(supplierId).toBe("unknown"); + expect(allocationStatus).toBe("REJECTED"); + expect(reasonCode).toBe("NO_SUPPLIERS_AVAILABLE"); + }); + + test("Verify that first eligible supplier is selected", async () => { + const letterVariant = getVariantsForAllocation(1); + const domainId = randomUUID(); + + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, + pageCount: 6, // pagecount that makes supplier1 ineligible and supplier2 eligible based on our config + }); + + const response = await sendSnsEvent(preparedEvent); + expect(response.MessageId).toBeTruthy(); + + const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); + const supplierId = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; + + expect(supplierId).toBe("supplier2"); + }); +}); diff --git a/tests/helpers/allocation-helper.ts b/tests/helpers/allocation-helper.ts new file mode 100644 index 000000000..0a6a03898 --- /dev/null +++ b/tests/helpers/allocation-helper.ts @@ -0,0 +1,49 @@ +import { pollSupplierAllocatorLogForResolvedSpec } from "./aws-cloudwatch-helper"; +import { logger } from "./pino-logger"; + +export const AllocationTestVariantMap: Record = { + "notify-standard-test1": 1, + "client1-campaign1": 2, +}; + +export function getVariantsForAllocation(testCase: number) { + const variants = Object.keys(AllocationTestVariantMap).filter( + // safe as comes from map's keys which are controlled by us + // eslint-disable-next-line security/detect-object-injection + (variant) => AllocationTestVariantMap[variant] === testCase, + ); + if (variants.length === 0) { + throw new Error(`No variants found with testCase ${testCase}`); + } + return variants[0]; +} + +export type SupplierAllocatorLog = { + msg?: { + allocationDetails?: { + supplierSpec?: { + supplierId?: string; + specId?: string; + billingId?: string; + }; + allocationStatus?: { + status?: "PENDING" | "REJECTED"; + reasonCode?: string; + }; + }; + }; +}; + +export async function getAllocationLogForDomainId( + domainId: string, +): Promise { + const message = await pollSupplierAllocatorLogForResolvedSpec(domainId); + const supplierAllocatorLog = JSON.parse(message) as SupplierAllocatorLog; + + logger.info({ + description: "Received supplier allocator log message", + message: supplierAllocatorLog, + }); + + return supplierAllocatorLog; +} From 8c9b688bde7f652f1ebfcfa9b8ae733343dec33c Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Thu, 14 May 2026 11:59:26 +0000 Subject: [PATCH 02/12] data --- config/suppliers/letter-variant/notify-standard-test1.json | 3 ++- .../supplier-allocation/supplier1-volumeGroup-test3.json | 2 +- .../supplier-allocation/supplier2-volumeGroup-test3.json | 7 +++++++ .../supplier-pack/supplier2-client1-campaign1.json | 7 +++++++ 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 config/suppliers/supplier-allocation/supplier2-volumeGroup-test3.json create mode 100644 config/suppliers/supplier-pack/supplier2-client1-campaign1.json diff --git a/config/suppliers/letter-variant/notify-standard-test1.json b/config/suppliers/letter-variant/notify-standard-test1.json index f69b59360..762356f1a 100644 --- a/config/suppliers/letter-variant/notify-standard-test1.json +++ b/config/suppliers/letter-variant/notify-standard-test1.json @@ -21,7 +21,8 @@ "id": "notify-standard-test1", "name": "Dev Happy Path", "packSpecificationIds": [ - "notify-c5" + "notify-c5", + "notify-c4" ], "priority": 50, "status": "PROD", diff --git a/config/suppliers/supplier-allocation/supplier1-volumeGroup-test3.json b/config/suppliers/supplier-allocation/supplier1-volumeGroup-test3.json index f08467c9f..299dc86c7 100644 --- a/config/suppliers/supplier-allocation/supplier1-volumeGroup-test3.json +++ b/config/suppliers/supplier-allocation/supplier1-volumeGroup-test3.json @@ -1,5 +1,5 @@ { - "allocationPercentage": 100, + "allocationPercentage": 50, "id": "supplier1-volumeGroup-test3", "status": "PROD", "supplier": "supplier1", diff --git a/config/suppliers/supplier-allocation/supplier2-volumeGroup-test3.json b/config/suppliers/supplier-allocation/supplier2-volumeGroup-test3.json new file mode 100644 index 000000000..3d8fa19cd --- /dev/null +++ b/config/suppliers/supplier-allocation/supplier2-volumeGroup-test3.json @@ -0,0 +1,7 @@ +{ + "allocationPercentage": 50, + "id": "supplier2-volumeGroup-test3", + "status": "PROD", + "supplier": "supplier2", + "volumeGroup": "volumeGroup-test3" +} diff --git a/config/suppliers/supplier-pack/supplier2-client1-campaign1.json b/config/suppliers/supplier-pack/supplier2-client1-campaign1.json new file mode 100644 index 000000000..fad6dec27 --- /dev/null +++ b/config/suppliers/supplier-pack/supplier2-client1-campaign1.json @@ -0,0 +1,7 @@ +{ + "approval": "APPROVED", + "id": "supplier2-client1-campaign1", + "packSpecificationId": "client1-campaign1", + "status": "PROD", + "supplierId": "supplier2" +} From 7870d9b6878419b711bbcfa8d90199aa2209fb89 Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Mon, 18 May 2026 09:22:39 +0000 Subject: [PATCH 03/12] tests --- .../letter-allocation.spec.ts | 76 ++++++- tests/helpers/allocation-helper.ts | 188 +++++++++++++++++- tests/helpers/aws-cloudwatch-helper.ts | 19 ++ 3 files changed, 270 insertions(+), 13 deletions(-) diff --git a/tests/component-tests/allocation-tests/letter-allocation.spec.ts b/tests/component-tests/allocation-tests/letter-allocation.spec.ts index 5a7264466..c994d1fa5 100644 --- a/tests/component-tests/allocation-tests/letter-allocation.spec.ts +++ b/tests/component-tests/allocation-tests/letter-allocation.spec.ts @@ -5,13 +5,17 @@ import { randomUUID } from "node:crypto"; import { logger } from "tests/helpers/pino-logger"; import { getAllocationLogForDomainId, + getAllocationPackSpecLog, + getLetterDailyAllocationFromDb, + getLetterVariantConfigFromDb, getVariantsForAllocation, + updateSupplierDailyAllocation, } from "tests/helpers/allocation-helper"; test.describe("Allocator Lambda Tests", () => { test.setTimeout(180_000); // 3 minutes for long running polling - test(`Verify that allocator successfully allocates a letter and emits PENDING event`, async () => { + /* test(`Verify that allocator successfully allocates a letter and emits PENDING event`, async () => { const domainId = randomUUID(); logger.info(`Testing event subscription with domainId: ${domainId}`); @@ -74,19 +78,79 @@ test.describe("Allocator Lambda Tests", () => { const letterVariant = getVariantsForAllocation(1); const domainId = randomUUID(); + const letterVariantConfig = + await getLetterVariantConfigFromDb(letterVariant); + expect(letterVariantConfig.packSpecificationIds).toEqual( + expect.arrayContaining(["notify-c4", "notify-c5"]), + ); + const preparedEvent = createPreparedV1Event({ domainId, letterVariantId: letterVariant, - pageCount: 6, // pagecount that makes supplier1 ineligible and supplier2 eligible based on our config + pageCount: 6, // pagecount that makes notify-c5 ineligible and notify-c4 eligible based on their pack configs }); const response = await sendSnsEvent(preparedEvent); expect(response.MessageId).toBeTruthy(); - const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); - const supplierId = - supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; + const supplierAllocatorLog = await getAllocationPackSpecLog( + "Pack specification filtered out based on constraints", + ); + const filteredPackSpecId = supplierAllocatorLog.packSpecId; + logger.info(`Pack spec filtered out ${filteredPackSpecId}`); + expect(filteredPackSpecId).toBe("notify-c5"); + expect(letterVariantConfig.packSpecificationIds).toContain( + filteredPackSpecId as string, + ); + + const allocationLog = await getAllocationLogForDomainId(domainId); + const allocatedPackSpecId = + allocationLog.msg?.allocationDetails?.supplierSpec?.specId; + expect(allocatedPackSpecId).toBeTruthy(); + expect(letterVariantConfig.packSpecificationIds).toContain( + allocatedPackSpecId as string, + ); + expect(allocatedPackSpecId).toBe("notify-c4"); + }); +*/ + test("Verify if suppliers without capacity are filtered out", async () => { + const letterVariant = getVariantsForAllocation(2); + const domainId = randomUUID(); - expect(supplierId).toBe("supplier2"); + const letterVariantConfig = + await getLetterVariantConfigFromDb(letterVariant); + + const dailyAllocation = await getLetterDailyAllocationFromDb(); + logger.info( + `Daily allocation before test execution ${JSON.stringify(dailyAllocation.allocations)}`, + ); + const originalSupplier1Allocation = dailyAllocation.allocations.supplier1; + + // update one supplier's allocated daily capacity to max so it gets filtered out + if (dailyAllocation.allocations.supplier1 != 500_000) { + await updateSupplierDailyAllocation("supplier1", 500_000); + } + + try { + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, + }); + + const response = await sendSnsEvent(preparedEvent); + expect(response.MessageId).toBeTruthy(); + } finally { + await updateSupplierDailyAllocation( + "supplier1", + originalSupplier1Allocation, + ); + } + const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); + const supplierDetails = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec; + expect(supplierDetails?.supplierId).toBe("supplier2"); + expect( + supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.status, + ).toBe("PENDING"); }); }); diff --git a/tests/helpers/allocation-helper.ts b/tests/helpers/allocation-helper.ts index 0a6a03898..63b768022 100644 --- a/tests/helpers/allocation-helper.ts +++ b/tests/helpers/allocation-helper.ts @@ -1,5 +1,18 @@ -import { pollSupplierAllocatorLogForResolvedSpec } from "./aws-cloudwatch-helper"; -import { logger } from "./pino-logger"; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { + DynamoDBDocumentClient, + GetCommand, + UpdateCommand, +} from "@aws-sdk/lib-dynamodb"; +import { envName } from "tests/constants/api-constants"; +import { + pollAllocatorLogForPackSpec, + pollSupplierAllocatorLogForExceededDailyCapacity, + pollSupplierAllocatorLogForResolvedSpec, +} from "./aws-cloudwatch-helper"; + +const ddb = new DynamoDBClient({}); +const docClient = DynamoDBDocumentClient.from(ddb); export const AllocationTestVariantMap: Record = { "notify-standard-test1": 1, @@ -34,16 +47,177 @@ export type SupplierAllocatorLog = { }; }; +type PackSpecificationLog = { + description: string; + packSpecId?: string; + pageCount?: number; + constraintValue?: number; + constraintOperator?: string; +}; + +type LetterVariantConfig = { + id: string; + packSpecificationIds: string[]; +}; + +type DailyAllocationConfig = { + id: string; + date: string; + allocations: Record; +}; + +export type SupplierDailyCapacityExceededLog = { + level?: string; + timestamp?: string; + pid?: number; + hostname?: string; + description?: string; + supplierId?: string; + allocated?: number; + dailyCapacity?: number; +}; + +const getSupplierConfigTableName = (): string => + process.env.SUPPLIER_CONFIG_TABLE_NAME ?? + `nhs-${envName}-supapi-supplier-config`; + +const getSupplierQuotasTableName = (): string => + process.env.SUPPLIER_QUOTAS_TABLE_NAME ?? "nhs-pr578-supapi-supplier-quotas"; + +const getAllocationDate = (): string => new Date().toISOString().slice(0, 10); + export async function getAllocationLogForDomainId( domainId: string, ): Promise { const message = await pollSupplierAllocatorLogForResolvedSpec(domainId); const supplierAllocatorLog = JSON.parse(message) as SupplierAllocatorLog; - logger.info({ - description: "Received supplier allocator log message", - message: supplierAllocatorLog, - }); - return supplierAllocatorLog; } + +export async function getAllocationPackSpecLog( + description: string, +): Promise { + const message = await pollAllocatorLogForPackSpec(description); + const packSpecificationLog = JSON.parse(message) as PackSpecificationLog; + return packSpecificationLog; +} + +export async function getExceededDailyCapacityLog( + supplierId: string, + allocated: number, + dailyCapacity: number, +): Promise { + const message = + await pollSupplierAllocatorLogForExceededDailyCapacity(supplierId); + const exceededCapacityLog = JSON.parse( + message, + ) as SupplierDailyCapacityExceededLog; + + if ( + exceededCapacityLog.description !== "Supplier has exceeded daily capacity" + ) { + throw new Error( + `Unexpected log description: ${exceededCapacityLog.description}`, + ); + } + if (exceededCapacityLog.supplierId !== supplierId) { + throw new Error( + `Unexpected supplierId in log: ${exceededCapacityLog.supplierId}`, + ); + } + if (exceededCapacityLog.allocated !== allocated) { + throw new Error( + `Unexpected allocated value in log: ${exceededCapacityLog.allocated}`, + ); + } + if (exceededCapacityLog.dailyCapacity !== dailyCapacity) { + throw new Error( + `Unexpected dailyCapacity value in log: ${exceededCapacityLog.dailyCapacity}`, + ); + } + + return exceededCapacityLog; +} + +export async function getLetterVariantConfigFromDb( + letterVariantId: string, +): Promise { + const { Item } = await docClient.send( + new GetCommand({ + TableName: getSupplierConfigTableName(), + Key: { + pk: "ENTITY#letter-variant", + sk: `ID#${letterVariantId}`, + }, + }), + ); + + if (!Item) { + throw new Error( + `Letter variant config was not found in supplier config table for id ${letterVariantId}`, + ); + } + + return Item as LetterVariantConfig; +} + +export async function getLetterDailyAllocationFromDb( + allocationDate: string = getAllocationDate(), +): Promise { + const { Item } = await docClient.send( + new GetCommand({ + TableName: getSupplierQuotasTableName(), + Key: { + pk: "ENTITY#daily-allocation", + sk: `ID#${allocationDate}`, + }, + }), + ); + + if (!Item) { + throw new Error( + `Letter daily allocation was not found in supplier config table for date ${allocationDate}`, + ); + } + + return Item as DailyAllocationConfig; +} + +export async function updateSupplierDailyAllocation( + supplierId: string, + allocation: number, + allocationDate: string = getAllocationDate(), +): Promise { + const now = new Date().toISOString(); + + const key = { + pk: "ENTITY#daily-allocation", + sk: `ID#${allocationDate}`, + }; + + await docClient.send( + new UpdateCommand({ + TableName: getSupplierQuotasTableName(), + Key: key, + UpdateExpression: ` + SET + allocations.#supplierId = :allocation, + id = if_not_exists(id, :id), + #date = if_not_exists(#date, :date), + createdAt = if_not_exists(createdAt, :now), + updatedAt = :now + `, + ExpressionAttributeNames: { + "#supplierId": supplierId, + "#date": "date", + }, + ExpressionAttributeValues: { + ":allocation": allocation, + ":id": `ID#${allocationDate}`, + ":date": allocationDate, + ":now": now, + }, + }), + ); +} diff --git a/tests/helpers/aws-cloudwatch-helper.ts b/tests/helpers/aws-cloudwatch-helper.ts index facd450c4..3c6555657 100644 --- a/tests/helpers/aws-cloudwatch-helper.ts +++ b/tests/helpers/aws-cloudwatch-helper.ts @@ -113,3 +113,22 @@ export async function supplierIdFromSupplierAllocatorLog( } return supplierId; } + +export async function pollAllocatorLogForPackSpec( + description: string, +): Promise { + const filterPatterns = ['"INFO"', `"${description}"`]; + const log = await pollLambdaLog("supplier-allocator", filterPatterns); + return log; +} + +export async function pollSupplierAllocatorLogForExceededDailyCapacity( + supplierId: string, +): Promise { + const filterPatterns = [ + '"INFO"', + '"Supplier has exceeded daily capacity"', + `"${supplierId}"`, + ]; + return pollLambdaLog("supplier-allocator", filterPatterns); +} From 8ad4de107e6f0c7ef8872807580c78322410458a Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Thu, 21 May 2026 09:57:46 +0000 Subject: [PATCH 04/12] data --- config/suppliers/letter-variant/client1-campaign2.json | 2 +- .../supplier-allocation/supplier3-volumeGroup-test4.json | 7 +++++++ .../supplier-allocation/supplier4-volumeGroup-test4.json | 7 +++++++ .../supplier-pack/supplier3-client1-campaign2.json | 7 +++++++ .../supplier-pack/supplier4-client1-campaign2.json | 7 +++++++ config/suppliers/supplier/supplier3.json | 7 +++++++ config/suppliers/supplier/supplier4.json | 7 +++++++ config/suppliers/volume-group/volumeGroup-test4.json | 7 +++++++ 8 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 config/suppliers/supplier-allocation/supplier3-volumeGroup-test4.json create mode 100644 config/suppliers/supplier-allocation/supplier4-volumeGroup-test4.json create mode 100644 config/suppliers/supplier-pack/supplier3-client1-campaign2.json create mode 100644 config/suppliers/supplier-pack/supplier4-client1-campaign2.json create mode 100644 config/suppliers/supplier/supplier3.json create mode 100644 config/suppliers/supplier/supplier4.json create mode 100644 config/suppliers/volume-group/volumeGroup-test4.json diff --git a/config/suppliers/letter-variant/client1-campaign2.json b/config/suppliers/letter-variant/client1-campaign2.json index 0df86359d..0dac50d26 100644 --- a/config/suppliers/letter-variant/client1-campaign2.json +++ b/config/suppliers/letter-variant/client1-campaign2.json @@ -34,5 +34,5 @@ "priority": 1, "status": "INT", "type": "STANDARD", - "volumeGroupId": "volumeGroup-test3" + "volumeGroupId": "volumeGroup-test4" } diff --git a/config/suppliers/supplier-allocation/supplier3-volumeGroup-test4.json b/config/suppliers/supplier-allocation/supplier3-volumeGroup-test4.json new file mode 100644 index 000000000..76909d155 --- /dev/null +++ b/config/suppliers/supplier-allocation/supplier3-volumeGroup-test4.json @@ -0,0 +1,7 @@ +{ + "allocationPercentage": 50, + "id": "supplier3-volumeGroup-test4", + "status": "PROD", + "supplier": "supplier3", + "volumeGroup": "volumeGroup-test4" +} diff --git a/config/suppliers/supplier-allocation/supplier4-volumeGroup-test4.json b/config/suppliers/supplier-allocation/supplier4-volumeGroup-test4.json new file mode 100644 index 000000000..e4888175b --- /dev/null +++ b/config/suppliers/supplier-allocation/supplier4-volumeGroup-test4.json @@ -0,0 +1,7 @@ +{ + "allocationPercentage": 50, + "id": "supplier4-volumeGroup-test4", + "status": "PROD", + "supplier": "supplier4", + "volumeGroup": "volumeGroup-test4" +} diff --git a/config/suppliers/supplier-pack/supplier3-client1-campaign2.json b/config/suppliers/supplier-pack/supplier3-client1-campaign2.json new file mode 100644 index 000000000..1783eb4c7 --- /dev/null +++ b/config/suppliers/supplier-pack/supplier3-client1-campaign2.json @@ -0,0 +1,7 @@ +{ + "approval": "SUBMITTED", + "id": "supplier1-client1-campaign2", + "packSpecificationId": "client1-campaign2", + "status": "INT", + "supplierId": "supplier3" +} diff --git a/config/suppliers/supplier-pack/supplier4-client1-campaign2.json b/config/suppliers/supplier-pack/supplier4-client1-campaign2.json new file mode 100644 index 000000000..b04bddf6a --- /dev/null +++ b/config/suppliers/supplier-pack/supplier4-client1-campaign2.json @@ -0,0 +1,7 @@ +{ + "approval": "SUBMITTED", + "id": "supplier1-client1-campaign2", + "packSpecificationId": "client1-campaign2", + "status": "INT", + "supplierId": "supplier4" +} diff --git a/config/suppliers/supplier/supplier3.json b/config/suppliers/supplier/supplier3.json new file mode 100644 index 000000000..aa1492568 --- /dev/null +++ b/config/suppliers/supplier/supplier3.json @@ -0,0 +1,7 @@ +{ + "channelType": "LETTER", + "dailyCapacity": 500000, + "id": "supplier3", + "name": "Supplier3", + "status": "PROD" +} diff --git a/config/suppliers/supplier/supplier4.json b/config/suppliers/supplier/supplier4.json new file mode 100644 index 000000000..47d358491 --- /dev/null +++ b/config/suppliers/supplier/supplier4.json @@ -0,0 +1,7 @@ +{ + "channelType": "LETTER", + "dailyCapacity": 500000, + "id": "supplier4", + "name": "Supplier4", + "status": "PROD" +} diff --git a/config/suppliers/volume-group/volumeGroup-test4.json b/config/suppliers/volume-group/volumeGroup-test4.json new file mode 100644 index 000000000..93e11e651 --- /dev/null +++ b/config/suppliers/volume-group/volumeGroup-test4.json @@ -0,0 +1,7 @@ +{ + "description": "Dev Test Volume Group 4", + "id": "volumeGroup-test4", + "name": "Dev Test Volume Group 4", + "startDate": "2026-01-01", + "status": "PROD" +} From cb5b4b358bff9902c63513338141290f4c5b0fb4 Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Thu, 21 May 2026 10:04:19 +0000 Subject: [PATCH 05/12] tests --- .../letter-allocation-capacity.spec.ts | 185 +++++++++++++ .../letter-allocation-rejected.spec.ts | 90 ++++++ .../letter-allocation-weighting.spec.ts | 262 ++++++++++++++++++ .../letter-allocation.spec.ts | 156 ----------- tests/helpers/allocation-factor-helper.ts | 57 ++++ tests/helpers/allocation-helper.ts | 228 ++++++++++++--- tests/helpers/aws-cloudwatch-helper.ts | 30 +- tests/helpers/generate-fetch-test-data.ts | 35 +++ 8 files changed, 843 insertions(+), 200 deletions(-) create mode 100644 tests/component-tests/allocation-tests/letter-allocation-capacity.spec.ts create mode 100644 tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts create mode 100644 tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts delete mode 100644 tests/component-tests/allocation-tests/letter-allocation.spec.ts create mode 100644 tests/helpers/allocation-factor-helper.ts diff --git a/tests/component-tests/allocation-tests/letter-allocation-capacity.spec.ts b/tests/component-tests/allocation-tests/letter-allocation-capacity.spec.ts new file mode 100644 index 000000000..3a0f56b91 --- /dev/null +++ b/tests/component-tests/allocation-tests/letter-allocation-capacity.spec.ts @@ -0,0 +1,185 @@ +import { expect, test } from "@playwright/test"; +import { sendSnsEvent } from "tests/helpers/send-sns-event"; +import { createPreparedV1Event } from "tests/helpers/event-fixtures"; +import { randomUUID } from "node:crypto"; +import { logger } from "tests/helpers/pino-logger"; +import { + getAllocationLog, + getAllocationLogForDomainId, + getExceededDailyCapacityLog, + getLetterVariantConfigFromDb, + getOrSeedLetterDailyAllocationFromDb, + getVariantsForAllocation, + updateSupplierDailyAllocation, +} from "tests/helpers/allocation-helper"; +import { getLettersFromSupplierTable } from "tests/helpers/generate-fetch-test-data"; + +test.describe("Allocator Lambda Tests", () => { + test.setTimeout(180_000); // 3 minutes for long running polling + + test(`Verify that allocator successfully allocates a letter and emits PENDING event`, async () => { + const domainId = randomUUID(); + logger.info(`Testing event subscription with domainId: ${domainId}`); + + const letterVariant = getVariantsForAllocation(1); + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, + }); + + const response = await sendSnsEvent(preparedEvent); + + expect(response.MessageId).toBeTruthy(); + + const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); + const supplierId = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; + const lettersInDb = await getLettersFromSupplierTable( + supplierId!, + domainId, + "PENDING", + ); + const allocationStatus = + supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.status; + expect(lettersInDb).toBeTruthy(); + expect(lettersInDb.status).toBe(allocationStatus); + }); + + test("Verify that first eligible supplier is selected", async () => { + const letterVariant = getVariantsForAllocation(1); + const domainId = randomUUID(); + + const letterVariantConfig = + await getLetterVariantConfigFromDb(letterVariant); + expect(letterVariantConfig.packSpecificationIds).toEqual( + expect.arrayContaining(["notify-c4", "notify-c5"]), + ); + + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, + pageCount: 6, // pagecount that makes notify-c5 ineligible and notify-c4 eligible based on their pack configs + }); + + const response = await sendSnsEvent(preparedEvent); + expect(response.MessageId).toBeTruthy(); + + const supplierAllocatorLog = await getAllocationLog( + "Pack specification filtered out based on constraints", + ); + const filteredPackSpecId = supplierAllocatorLog.packSpecId; + logger.info(`Pack spec filtered out ${filteredPackSpecId}`); + expect(filteredPackSpecId).toBe("notify-c5"); + expect(letterVariantConfig.packSpecificationIds).toContain( + filteredPackSpecId as string, + ); + + const allocationLog = await getAllocationLogForDomainId(domainId); + const lettersInDb = await getLettersFromSupplierTable( + allocationLog.msg?.allocationDetails?.supplierSpec?.supplierId!, + domainId, + "PENDING", + ); + expect(lettersInDb.status).toBe("PENDING"); + const allocatedPackSpecId = + allocationLog.msg?.allocationDetails?.supplierSpec?.specId; + expect(allocatedPackSpecId).toBe("notify-c4"); + expect(lettersInDb.specificationId).toBe(allocatedPackSpecId); + }); + + const supplierCapacityTestCases = [ + { + testCase: "Verify if suppliers without capacity are filtered out", + letterVariantId: 4, + expectedSupplierId: "supplier2", + }, + { + testCase: + "Verify that fallback is triggered when a suppliers are at daily capacity, ignoring capacity", + letterVariantId: 3, + expectedSupplierId: "supplier1", + }, + ]; + + for (const { + expectedSupplierId, + letterVariantId, + testCase, + } of supplierCapacityTestCases) { + test(testCase, async () => { + const letterVariant = getVariantsForAllocation(letterVariantId); + const domainId = randomUUID(); + const dailyAllocatedCapacity = 500_000; + const allocationDate = new Intl.DateTimeFormat("en-CA", { + timeZone: "Europe/London", + }).format(new Date()); + + const dailyAllocation = await getOrSeedLetterDailyAllocationFromDb( + { + supplier3: 0, + supplier4: 0, + }, + allocationDate, + ); + logger.info( + `Daily allocation before test execution ${JSON.stringify(dailyAllocation.allocations)}`, + ); + + const originalSupplier3Allocation = + dailyAllocation.allocations.supplier3 ?? 0; + + // set supplier3 to exactly daily capacity so allocator filters it out + if (dailyAllocation.allocations.supplier3 !== dailyAllocatedCapacity) { + await updateSupplierDailyAllocation( + "supplier3", + dailyAllocatedCapacity, + allocationDate, + ); + } + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, + }); + + const response = await sendSnsEvent(preparedEvent); + expect(response.MessageId).toBeTruthy(); + + const exceededCapacityLog = + await getExceededDailyCapacityLog("supplier3"); + expect(exceededCapacityLog.description).toBe( + "Supplier has exceeded daily capacity", + ); + + const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); + const supplierDetails = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec; + expect(supplierDetails?.supplierId).toBe(expectedSupplierId); + + const lettersInDb = await getLettersFromSupplierTable( + expectedSupplierId, + domainId, + "PENDING", + ); + expect(lettersInDb.status).toBe("PENDING"); + expect(lettersInDb.specificationId).toBe( + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.specId, + ); + + if (testCase.includes("fallback")) { + const fallbackDailyAllocation = + await getOrSeedLetterDailyAllocationFromDb({ + supplier1: 0, + }); + expect(fallbackDailyAllocation.allocations.supplier1).toBe( + dailyAllocatedCapacity + 1, + ); + } + + await updateSupplierDailyAllocation( + "supplier3", + originalSupplier3Allocation, + allocationDate, + ); + }); + } +}); diff --git a/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts b/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts new file mode 100644 index 000000000..d6f7ffafe --- /dev/null +++ b/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts @@ -0,0 +1,90 @@ +import { randomUUID } from "node:crypto"; +import test, { expect } from "playwright/test"; +import { + getAllocationLog, + getAllocationLogForDomainId, + getVariantsForAllocation, +} from "tests/helpers/allocation-helper"; +import { createPreparedV1Event } from "tests/helpers/event-fixtures"; +import { getLettersFromSupplierTable } from "tests/helpers/generate-fetch-test-data"; +import { logger } from "tests/helpers/pino-logger"; +import { sendSnsEvent } from "tests/helpers/send-sns-event"; + +test.describe("Allocator Rejected Allocation Tests", () => { + test.setTimeout(180_000); // 3 minutes for long running polling + test("Verify that unknown letter variant is marked as rejected allocation", async () => { + const domainId = randomUUID(); + logger.info(`Testing rejected allocation with domainId: ${domainId}`); + + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: `unknown-variant-${domainId}`, + }); + + const response = await sendSnsEvent(preparedEvent); + + expect(response.MessageId).toBeTruthy(); + + const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); + const supplierId = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; + const allocationStatus = + supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.status; + const reasonCode = + supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.reasonCode; + + expect(supplierId).toBe("unknown"); + expect(allocationStatus).toBe("REJECTED"); + expect(reasonCode).toBe("NO_SUPPLIERS_AVAILABLE"); + + const lettersInDb = await getLettersFromSupplierTable( + supplierId!, + domainId, + "REJECTED", + ); + expect(lettersInDb).toBeTruthy(); + expect(lettersInDb.status).toBe(allocationStatus); + expect(lettersInDb.reasonCode).toBe(reasonCode); + expect(lettersInDb.reasonText).toBe( + `No letter variant details found for id unknown-variant-${domainId}`, + ); + }); + + test("Verify that the letters are REJECTED when no pack specification is eligible", async () => { + const letterVariant = getVariantsForAllocation(1); + const domainId = randomUUID(); + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, + pageCount: 100, // high page count to ensure pack specifications are filtered out based on constraints + }); + + const response = await sendSnsEvent(preparedEvent); + expect(response.MessageId).toBeTruthy(); + + const supplierAllocatorLog = await getAllocationLog( + "No eligible pack specifications found for letter", + ); + + const packSpecificationIds = + supplierAllocatorLog.packSpecificationIds ?? + supplierAllocatorLog.packSpecificationId; + expect(packSpecificationIds).toBeTruthy(); + expect(supplierAllocatorLog.letterVariantId).toBe(letterVariant); + + const allocationLog = await getAllocationLogForDomainId(domainId); + const lettersInDb = await getLettersFromSupplierTable( + "unknown", + domainId, + "REJECTED", + ); + + expect(lettersInDb.status).toBe("REJECTED"); + expect(lettersInDb.supplierId).toBe( + allocationLog.msg?.allocationDetails?.supplierSpec?.supplierId, + ); + expect(lettersInDb.reasonText).toBe( + `No eligible pack specifications found for letter variant id ${letterVariant} and pack specification ids ${packSpecificationIds?.join(", ")}`, + ); + }); +}); diff --git a/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts b/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts new file mode 100644 index 000000000..81372787d --- /dev/null +++ b/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts @@ -0,0 +1,262 @@ +import { randomUUID } from "node:crypto"; +import { expect, test } from "@playwright/test"; +import { + SupplierFactorLog, + getAllocationLog, + getAllocationLogForDomainId, + getOrSeedLetterDailyAllocationFromDb, + getOrSeedOverallAllocationFromDb, + getOverallAllocationFromDb, + getVariantsForAllocation, + updateSupplierDailyAllocation, + updateSupplierOverallAllocation, +} from "tests/helpers/allocation-helper"; +import { createPreparedV1Event } from "tests/helpers/event-fixtures"; +import { sendSnsEvent } from "tests/helpers/send-sns-event"; +import { + buildWeightingSnapshot, + getLowestWeightingSupplier, +} from "tests/helpers/allocation-factor-helper"; +import { logger } from "tests/helpers/pino-logger"; + +test.describe("Allocator Weighting Tests", () => { + test.setTimeout(180_000); + + test("Verify weighting and lowest weighting supplier selection for multiple suppliers", async () => { + const testStartedAt = Date.now(); + const domainId = randomUUID(); + const letterVariant = getVariantsForAllocation(2); + const volumeGroupId = "volumeGroup-test3"; + const targetPercentages = { + supplier1: 50, + supplier2: 50, + }; + + const dailyAllocation = await getOrSeedLetterDailyAllocationFromDb({ + supplier1: 0, + supplier2: 0, + }); + const overallAllocation = await getOrSeedOverallAllocationFromDb( + { + supplier1: 0, + supplier2: 0, + }, + volumeGroupId, + ); + + const originalDailySupplier1 = dailyAllocation.allocations.supplier1 ?? 0; + const originalDailySupplier2 = dailyAllocation.allocations.supplier2 ?? 0; + const originalOverallSupplier1 = + overallAllocation.allocations.supplier1 ?? 0; + const originalOverallSupplier2 = + overallAllocation.allocations.supplier2 ?? 0; + + const seededOverall = { + supplier1: 900, + supplier2: 100, + }; + + try { + await updateSupplierDailyAllocation("supplier1", 0); + await updateSupplierDailyAllocation("supplier2", 0); + await updateSupplierOverallAllocation( + "supplier1", + seededOverall.supplier1, + volumeGroupId, + ); + await updateSupplierOverallAllocation( + "supplier2", + seededOverall.supplier2, + volumeGroupId, + ); + + const weightingSnapshot = buildWeightingSnapshot( + seededOverall, + targetPercentages, + ); + const lowestWeightingSupplier = + getLowestWeightingSupplier(weightingSnapshot); + + expect(weightingSnapshot.supplier1.allocatedVolume).toBe(900); + expect(weightingSnapshot.supplier2.allocatedVolume).toBe(100); + expect(weightingSnapshot.supplier1.allocatedPercentage).toBe(90); + expect(weightingSnapshot.supplier2.allocatedPercentage).toBe(10); + expect(lowestWeightingSupplier).toBe("supplier2"); + + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, + }); + const response = await sendSnsEvent(preparedEvent); + expect(response.MessageId).toBeTruthy(); + + console.log(`testStartedAt: ${testStartedAt}`); + const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); + const supplierFactorLog = await getAllocationLog( + "Calculated supplier factors for allocation", + { + startTimeMs: testStartedAt, + }, + ); + + const selectedSupplierId = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; + + expect(selectedSupplierId).toBe(lowestWeightingSupplier); + expect(weightingSnapshot.supplier1.weighting).toBe( + supplierFactorLog.supplierFactors?.find( + (factor) => factor.supplierId === "supplier1", + )?.factor, + ); + expect(weightingSnapshot.supplier2.weighting).toBe( + supplierFactorLog.supplierFactors?.find( + (factor) => factor.supplierId === "supplier2", + )?.factor, + ); + + const updatedOverallAllocation = + await getOverallAllocationFromDb(volumeGroupId); + expect(updatedOverallAllocation.allocations.supplier1).toBe(900); + expect(updatedOverallAllocation.allocations.supplier2).toBe(101); + expect(supplierFactorLog.description).toBe( + "Calculated supplier factors for allocation", + ); + } finally { + await updateSupplierDailyAllocation("supplier1", originalDailySupplier1); + await updateSupplierDailyAllocation("supplier2", originalDailySupplier2); + await updateSupplierOverallAllocation( + "supplier1", + originalOverallSupplier1, + volumeGroupId, + ); + await updateSupplierOverallAllocation( + "supplier2", + originalOverallSupplier2, + volumeGroupId, + ); + } + }); + + test("Verify lowest weighting supplier selection with valid unequal target percentages", async () => { + const testStartedAt = Date.now(); + const domainId = randomUUID(); + const letterVariant = getVariantsForAllocation(1); + const volumeGroupId = "volumeGroup-test1"; + const targetPercentages = { + supplier1: 30, + supplier2: 70, + }; + + const dailyAllocation = await getOrSeedLetterDailyAllocationFromDb({ + supplier1: 0, + supplier2: 0, + }); + const overallAllocation = await getOrSeedOverallAllocationFromDb( + { + supplier1: 0, + supplier2: 0, + }, + volumeGroupId, + ); + + const originalDailySupplier1 = dailyAllocation.allocations.supplier1 ?? 0; + const originalDailySupplier2 = dailyAllocation.allocations.supplier2 ?? 0; + const originalOverallSupplier1 = + overallAllocation.allocations.supplier1 ?? 0; + const originalOverallSupplier2 = + overallAllocation.allocations.supplier2 ?? 0; + + const seededOverall = { + supplier1: 600, + supplier2: 400, + }; + + try { + await updateSupplierDailyAllocation("supplier1", 0); + await updateSupplierDailyAllocation("supplier2", 0); + await updateSupplierOverallAllocation( + "supplier1", + seededOverall.supplier1, + volumeGroupId, + ); + await updateSupplierOverallAllocation( + "supplier2", + seededOverall.supplier2, + volumeGroupId, + ); + + const weightingSnapshot = buildWeightingSnapshot( + seededOverall, + targetPercentages, + ); + const lowestWeightingSupplier = + getLowestWeightingSupplier(weightingSnapshot); + + expect(weightingSnapshot.supplier1.allocatedVolume).toBe(600); + expect(weightingSnapshot.supplier2.allocatedVolume).toBe(400); + expect(weightingSnapshot.supplier1.allocatedPercentage).toBe(60); + expect(weightingSnapshot.supplier2.allocatedPercentage).toBe(40); + expect(weightingSnapshot.supplier1.targetPercentage).toBe(30); + expect(weightingSnapshot.supplier2.targetPercentage).toBe(70); + expect(weightingSnapshot.supplier1.weighting).toBe(2); + expect(weightingSnapshot.supplier2.weighting).toBeCloseTo(4 / 7, 10); + expect(lowestWeightingSupplier).toBe("supplier2"); + expect(Object.keys(weightingSnapshot)).toEqual( + expect.arrayContaining(["supplier1", "supplier2"]), + ); + + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, + }); + const response = await sendSnsEvent(preparedEvent); + expect(response.MessageId).toBeTruthy(); + + const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); + const supplierFactorLog = await getAllocationLog( + "Calculated supplier factors for allocation", + { + startTimeMs: testStartedAt, + }, + ); + + const selectedSupplierId = + supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; + + expect(selectedSupplierId).toBe(lowestWeightingSupplier); + logger.info( + `Weighting snapshot: ${JSON.stringify(weightingSnapshot)}, Supplier factors from log: ${JSON.stringify( + supplierFactorLog.supplierFactors, + )}`, + ); + expect(weightingSnapshot.supplier1.weighting).toBe( + supplierFactorLog.supplierFactors?.find( + (factor) => factor.supplierId === "supplier1", + )?.factor, + ); + expect(weightingSnapshot.supplier2.weighting).toBe( + supplierFactorLog.supplierFactors?.find( + (factor) => factor.supplierId === "supplier2", + )?.factor, + ); + + const updatedOverallAllocation = + await getOverallAllocationFromDb(volumeGroupId); + expect(updatedOverallAllocation.allocations.supplier1).toBe(600); + expect(updatedOverallAllocation.allocations.supplier2).toBe(401); + } finally { + await updateSupplierDailyAllocation("supplier1", originalDailySupplier1); + await updateSupplierDailyAllocation("supplier2", originalDailySupplier2); + await updateSupplierOverallAllocation( + "supplier1", + originalOverallSupplier1, + volumeGroupId, + ); + await updateSupplierOverallAllocation( + "supplier2", + originalOverallSupplier2, + volumeGroupId, + ); + } + }); +}); diff --git a/tests/component-tests/allocation-tests/letter-allocation.spec.ts b/tests/component-tests/allocation-tests/letter-allocation.spec.ts deleted file mode 100644 index c994d1fa5..000000000 --- a/tests/component-tests/allocation-tests/letter-allocation.spec.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { expect, test } from "@playwright/test"; -import { sendSnsEvent } from "tests/helpers/send-sns-event"; -import { createPreparedV1Event } from "tests/helpers/event-fixtures"; -import { randomUUID } from "node:crypto"; -import { logger } from "tests/helpers/pino-logger"; -import { - getAllocationLogForDomainId, - getAllocationPackSpecLog, - getLetterDailyAllocationFromDb, - getLetterVariantConfigFromDb, - getVariantsForAllocation, - updateSupplierDailyAllocation, -} from "tests/helpers/allocation-helper"; - -test.describe("Allocator Lambda Tests", () => { - test.setTimeout(180_000); // 3 minutes for long running polling - - /* test(`Verify that allocator successfully allocates a letter and emits PENDING event`, async () => { - const domainId = randomUUID(); - logger.info(`Testing event subscription with domainId: ${domainId}`); - - const letterVariant = getVariantsForAllocation(1); - const preparedEvent = createPreparedV1Event({ - domainId, - letterVariantId: letterVariant, - }); - - const response = await sendSnsEvent(preparedEvent); - - expect(response.MessageId).toBeTruthy(); - - const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); - const supplierId = - supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; - const specId = - supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.specId; - const billingId = - supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.billingId; - const allocationStatus = - supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.status; - - if (!supplierId) { - throw new Error("supplierId was not found in supplier allocator log"); - } - - expect(specId).toBeTruthy(); - expect(billingId).toBeTruthy(); - expect(allocationStatus).toBe("PENDING"); - }); - - test("Verify that unknown letter variant is marked as rejected allocation", async () => { - const domainId = randomUUID(); - logger.info(`Testing rejected allocation with domainId: ${domainId}`); - - const preparedEvent = createPreparedV1Event({ - domainId, - letterVariantId: `unknown-variant-${domainId}`, - }); - - const response = await sendSnsEvent(preparedEvent); - - expect(response.MessageId).toBeTruthy(); - - const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); - const supplierId = - supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; - const allocationStatus = - supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.status; - const reasonCode = - supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.reasonCode; - - expect(supplierId).toBe("unknown"); - expect(allocationStatus).toBe("REJECTED"); - expect(reasonCode).toBe("NO_SUPPLIERS_AVAILABLE"); - }); - - test("Verify that first eligible supplier is selected", async () => { - const letterVariant = getVariantsForAllocation(1); - const domainId = randomUUID(); - - const letterVariantConfig = - await getLetterVariantConfigFromDb(letterVariant); - expect(letterVariantConfig.packSpecificationIds).toEqual( - expect.arrayContaining(["notify-c4", "notify-c5"]), - ); - - const preparedEvent = createPreparedV1Event({ - domainId, - letterVariantId: letterVariant, - pageCount: 6, // pagecount that makes notify-c5 ineligible and notify-c4 eligible based on their pack configs - }); - - const response = await sendSnsEvent(preparedEvent); - expect(response.MessageId).toBeTruthy(); - - const supplierAllocatorLog = await getAllocationPackSpecLog( - "Pack specification filtered out based on constraints", - ); - const filteredPackSpecId = supplierAllocatorLog.packSpecId; - logger.info(`Pack spec filtered out ${filteredPackSpecId}`); - expect(filteredPackSpecId).toBe("notify-c5"); - expect(letterVariantConfig.packSpecificationIds).toContain( - filteredPackSpecId as string, - ); - - const allocationLog = await getAllocationLogForDomainId(domainId); - const allocatedPackSpecId = - allocationLog.msg?.allocationDetails?.supplierSpec?.specId; - expect(allocatedPackSpecId).toBeTruthy(); - expect(letterVariantConfig.packSpecificationIds).toContain( - allocatedPackSpecId as string, - ); - expect(allocatedPackSpecId).toBe("notify-c4"); - }); -*/ - test("Verify if suppliers without capacity are filtered out", async () => { - const letterVariant = getVariantsForAllocation(2); - const domainId = randomUUID(); - - const letterVariantConfig = - await getLetterVariantConfigFromDb(letterVariant); - - const dailyAllocation = await getLetterDailyAllocationFromDb(); - logger.info( - `Daily allocation before test execution ${JSON.stringify(dailyAllocation.allocations)}`, - ); - const originalSupplier1Allocation = dailyAllocation.allocations.supplier1; - - // update one supplier's allocated daily capacity to max so it gets filtered out - if (dailyAllocation.allocations.supplier1 != 500_000) { - await updateSupplierDailyAllocation("supplier1", 500_000); - } - - try { - const preparedEvent = createPreparedV1Event({ - domainId, - letterVariantId: letterVariant, - }); - - const response = await sendSnsEvent(preparedEvent); - expect(response.MessageId).toBeTruthy(); - } finally { - await updateSupplierDailyAllocation( - "supplier1", - originalSupplier1Allocation, - ); - } - const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); - const supplierDetails = - supplierAllocatorLog.msg?.allocationDetails?.supplierSpec; - expect(supplierDetails?.supplierId).toBe("supplier2"); - expect( - supplierAllocatorLog.msg?.allocationDetails?.allocationStatus?.status, - ).toBe("PENDING"); - }); -}); diff --git a/tests/helpers/allocation-factor-helper.ts b/tests/helpers/allocation-factor-helper.ts new file mode 100644 index 000000000..dc1036b9f --- /dev/null +++ b/tests/helpers/allocation-factor-helper.ts @@ -0,0 +1,57 @@ +type SnapshotEntry = { + allocatedVolume: number; + allocatedPercentage: number; + targetPercentage: number; + weighting: number; +}; + +export function buildWeightingSnapshot( + allocations: Record, + targetPercentages: Record, +): Record { + const allocationsMap = new Map(Object.entries(allocations)); + let totalAllocated = 0; + for (const value of allocationsMap.values()) { + totalAllocated += value; + } + + return Object.fromEntries( + Object.entries(targetPercentages).map(([supplierId, targetPercentage]) => { + const allocatedVolume = allocationsMap.get(supplierId) ?? 0; + const allocatedPercentage = + totalAllocated > 0 ? (allocatedVolume / totalAllocated) * 100 : 0; + const weighting = allocatedPercentage / targetPercentage; + + return [ + supplierId, + { + allocatedVolume, + allocatedPercentage, + targetPercentage, + weighting, + }, + ]; + }), + ); +} + +export function getLowestWeightingSupplier( + snapshot: Record, +): string { + const entries = Object.entries(snapshot); + if (entries.length === 0) { + throw new Error("Weighting snapshot is empty"); + } + + let lowestSupplierId = entries[0][0]; + let lowestWeighting = entries[0][1].weighting; + + for (const [supplierId, snapshotEntry] of entries) { + if (snapshotEntry.weighting < lowestWeighting) { + lowestWeighting = snapshotEntry.weighting; + lowestSupplierId = supplierId; + } + } + + return lowestSupplierId; +} diff --git a/tests/helpers/allocation-helper.ts b/tests/helpers/allocation-helper.ts index 63b768022..1820ed8fe 100644 --- a/tests/helpers/allocation-helper.ts +++ b/tests/helpers/allocation-helper.ts @@ -2,11 +2,12 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, GetCommand, + PutCommand, UpdateCommand, } from "@aws-sdk/lib-dynamodb"; import { envName } from "tests/constants/api-constants"; import { - pollAllocatorLogForPackSpec, + pollAllocatorLogWithOptions, pollSupplierAllocatorLogForExceededDailyCapacity, pollSupplierAllocatorLogForResolvedSpec, } from "./aws-cloudwatch-helper"; @@ -17,11 +18,12 @@ const docClient = DynamoDBDocumentClient.from(ddb); export const AllocationTestVariantMap: Record = { "notify-standard-test1": 1, "client1-campaign1": 2, + "notify-standard-colour": 3, + "client1-campaign2": 4, }; export function getVariantsForAllocation(testCase: number) { const variants = Object.keys(AllocationTestVariantMap).filter( - // safe as comes from map's keys which are controlled by us // eslint-disable-next-line security/detect-object-injection (variant) => AllocationTestVariantMap[variant] === testCase, ); @@ -55,9 +57,36 @@ type PackSpecificationLog = { constraintOperator?: string; }; +exporttype PackErrorLog = { + description: string; + letterVariantId?: string; + packSpecificationId?: string[]; +}; + +export type SupplierFactorEntry = { + supplierId: string; + factor: number; +}; + +export type SupplierFactorLog = { + description: string; + supplierFactors?: SupplierFactorEntry[]; +}; + +export type AllocationLogOptions = { + startTimeMs?: number; + extraPatterns?: string[]; +}; + type LetterVariantConfig = { id: string; packSpecificationIds: string[]; + constraints: { + blackCoveragePercentage: Record; + deliveryDays: Record; + sides: Record; + sheets: Record; + }; }; type DailyAllocationConfig = { @@ -66,6 +95,12 @@ type DailyAllocationConfig = { allocations: Record; }; +type OverallAllocationConfig = { + id: string; + volumeGroup: string; + allocations: Record; +}; + export type SupplierDailyCapacityExceededLog = { level?: string; timestamp?: string; @@ -82,7 +117,8 @@ const getSupplierConfigTableName = (): string => `nhs-${envName}-supapi-supplier-config`; const getSupplierQuotasTableName = (): string => - process.env.SUPPLIER_QUOTAS_TABLE_NAME ?? "nhs-pr578-supapi-supplier-quotas"; + process.env.SUPPLIER_QUOTAS_TABLE_NAME ?? + `nhs-${envName}-supapi-supplier-quotas`; const getAllocationDate = (): string => new Date().toISOString().slice(0, 10); @@ -95,49 +131,20 @@ export async function getAllocationLogForDomainId( return supplierAllocatorLog; } -export async function getAllocationPackSpecLog( - description: string, -): Promise { - const message = await pollAllocatorLogForPackSpec(description); - const packSpecificationLog = JSON.parse(message) as PackSpecificationLog; - return packSpecificationLog; +export async function getAllocationLog< + TLog extends { description: string } = PackSpecificationLog, +>(description: string, options?: AllocationLogOptions): Promise { + const message = await pollAllocatorLogWithOptions(description, options); + const allocationLog = JSON.parse(message) as TLog; + return allocationLog; } export async function getExceededDailyCapacityLog( supplierId: string, - allocated: number, - dailyCapacity: number, ): Promise { const message = await pollSupplierAllocatorLogForExceededDailyCapacity(supplierId); - const exceededCapacityLog = JSON.parse( - message, - ) as SupplierDailyCapacityExceededLog; - - if ( - exceededCapacityLog.description !== "Supplier has exceeded daily capacity" - ) { - throw new Error( - `Unexpected log description: ${exceededCapacityLog.description}`, - ); - } - if (exceededCapacityLog.supplierId !== supplierId) { - throw new Error( - `Unexpected supplierId in log: ${exceededCapacityLog.supplierId}`, - ); - } - if (exceededCapacityLog.allocated !== allocated) { - throw new Error( - `Unexpected allocated value in log: ${exceededCapacityLog.allocated}`, - ); - } - if (exceededCapacityLog.dailyCapacity !== dailyCapacity) { - throw new Error( - `Unexpected dailyCapacity value in log: ${exceededCapacityLog.dailyCapacity}`, - ); - } - - return exceededCapacityLog; + return JSON.parse(message) as SupplierDailyCapacityExceededLog; } export async function getLetterVariantConfigFromDb( @@ -184,6 +191,112 @@ export async function getLetterDailyAllocationFromDb( return Item as DailyAllocationConfig; } +export async function getOverallAllocationFromDb( + volumeGroupId: string, +): Promise { + const { Item } = await docClient.send( + new GetCommand({ + TableName: getSupplierQuotasTableName(), + Key: { + pk: "ENTITY#overall-allocation", + sk: `ID#${volumeGroupId}`, + }, + }), + ); + + if (!Item) { + throw new Error( + `Overall allocation was not found in supplier config table for volume group ${volumeGroupId}`, + ); + } + + return Item as OverallAllocationConfig; +} + +export async function seedLetterDailyAllocation( + allocations: Record, + allocationDate: string = getAllocationDate(), +): Promise { + const now = new Date().toISOString(); + const item: DailyAllocationConfig & { + pk: string; + sk: string; + createdAt: string; + updatedAt: string; + } = { + pk: "ENTITY#daily-allocation", + sk: `ID#${allocationDate}`, + id: `ID#${allocationDate}`, + date: allocationDate, + allocations, + createdAt: now, + updatedAt: now, + }; + + await docClient.send( + new PutCommand({ + TableName: getSupplierQuotasTableName(), + Item: item, + ConditionExpression: "attribute_not_exists(pk)", + }), + ); + + return item; +} + +export async function seedOverallAllocation( + allocations: Record, + volumeGroupId: string, +): Promise { + const now = new Date().toISOString(); + const item: OverallAllocationConfig & { + pk: string; + sk: string; + createdAt: string; + updatedAt: string; + } = { + pk: "ENTITY#overall-allocation", + sk: `ID#${volumeGroupId}`, + id: volumeGroupId, + volumeGroup: volumeGroupId, + allocations, + createdAt: now, + updatedAt: now, + }; + + await docClient.send( + new PutCommand({ + TableName: getSupplierQuotasTableName(), + Item: item, + ConditionExpression: "attribute_not_exists(pk)", + }), + ); + + return item; +} + +export async function getOrSeedLetterDailyAllocationFromDb( + defaultAllocations: Record, + allocationDate: string = getAllocationDate(), +): Promise { + try { + return await getLetterDailyAllocationFromDb(allocationDate); + } catch { + return seedLetterDailyAllocation(defaultAllocations, allocationDate); + } +} + +export async function getOrSeedOverallAllocationFromDb( + defaultAllocations: Record, + volumeGroupId: string, +): Promise { + try { + return await getOverallAllocationFromDb(volumeGroupId); + } catch { + return seedOverallAllocation(defaultAllocations, volumeGroupId); + } +} + export async function updateSupplierDailyAllocation( supplierId: string, allocation: number, @@ -221,3 +334,40 @@ export async function updateSupplierDailyAllocation( }), ); } + +export async function updateSupplierOverallAllocation( + supplierId: string, + allocation: number, + volumeGroupId: string, +): Promise { + const now = new Date().toISOString(); + + const key = { + pk: "ENTITY#overall-allocation", + sk: `ID#${volumeGroupId}`, + }; + + await docClient.send( + new UpdateCommand({ + TableName: getSupplierQuotasTableName(), + Key: key, + UpdateExpression: ` + SET + allocations.#supplierId = :allocation, + id = if_not_exists(id, :id), + volumeGroup = if_not_exists(volumeGroup, :volumeGroup), + createdAt = if_not_exists(createdAt, :now), + updatedAt = :now + `, + ExpressionAttributeNames: { + "#supplierId": supplierId, + }, + ExpressionAttributeValues: { + ":allocation": allocation, + ":id": volumeGroupId, + ":volumeGroup": volumeGroupId, + ":now": now, + }, + }), + ); +} diff --git a/tests/helpers/aws-cloudwatch-helper.ts b/tests/helpers/aws-cloudwatch-helper.ts index 3c6555657..140082f70 100644 --- a/tests/helpers/aws-cloudwatch-helper.ts +++ b/tests/helpers/aws-cloudwatch-helper.ts @@ -14,9 +14,10 @@ async function pollLambdaLog( lambdaName: string, filterPatterns: string[], extraPatterns?: string[], + startTimeMs?: number, ): Promise { const intervalMs = 5000; - const startTimeMs = Date.now() - 5 * 60_000; + const queryStartTimeMs = startTimeMs ?? Date.now() - 5 * 60_000; const timeoutMs = 120_000; const client = new CloudWatchLogsClient({ region: AWS_REGION }); @@ -27,7 +28,7 @@ async function pollLambdaLog( const response = await client.send( new FilterLogEventsCommand({ logGroupName, - startTime: startTimeMs, + startTime: queryStartTimeMs, interleaved: true, limit: 100, filterPattern: filterPatterns.join(" "), @@ -40,6 +41,7 @@ async function pollLambdaLog( ? extraPatterns.some((pattern) => message.includes(pattern)) : true; }); + if (foundEvent?.message) { return foundEvent.message; } @@ -114,14 +116,32 @@ export async function supplierIdFromSupplierAllocatorLog( return supplierId; } -export async function pollAllocatorLogForPackSpec( - description: string, -): Promise { +export async function pollAllocatorLog(description: string): Promise { const filterPatterns = ['"INFO"', `"${description}"`]; const log = await pollLambdaLog("supplier-allocator", filterPatterns); return log; } +export type AllocatorLogPollOptions = { + startTimeMs?: number; + extraPatterns?: string[]; +}; + +export async function pollAllocatorLogWithOptions( + description: string, + options?: AllocatorLogPollOptions, +): Promise { + const filterPatterns = [`"${description}"`]; + console.log(filterPatterns); + const log = await pollLambdaLog( + "supplier-allocator", + filterPatterns, + options?.extraPatterns, + options?.startTimeMs, + ); + return log; +} + export async function pollSupplierAllocatorLogForExceededDailyCapacity( supplierId: string, ): Promise { diff --git a/tests/helpers/generate-fetch-test-data.ts b/tests/helpers/generate-fetch-test-data.ts index 60aa046ae..832f70d85 100644 --- a/tests/helpers/generate-fetch-test-data.ts +++ b/tests/helpers/generate-fetch-test-data.ts @@ -306,3 +306,38 @@ export async function getLetterFromQueueById( return []; } } + +export async function getLettersFromSupplierTable( + supplierId: string, + id: string, + status: string, + options?: { + timeoutMs?: number; + intervalMs?: number; + }, +): Promise { + const timeoutMs = options?.timeoutMs ?? 60_000; + const intervalMs = options?.intervalMs ?? 5000; + const startedAt = Date.now(); + + while (Date.now() - startedAt < timeoutMs) { + const { Item } = await docClient.send( + new GetCommand({ + TableName: LETTERSTABLENAME, + Key: { id, supplierId }, + }), + ); + + const letter = Item as SupplierApiLetters; + + if (letter && letter.status === status) { + return letter; + } + + await delay(intervalMs); + } + + throw new Error( + `Timed out waiting for letter ${id} to reach status ${status} for supplier ${supplierId}.`, + ); +} From 3843953bb6cde7fcebbebcc345823197576ee10e Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Thu, 21 May 2026 10:20:11 +0000 Subject: [PATCH 06/12] lint --- .../allocation-tests/letter-allocation-weighting.spec.ts | 1 - tests/helpers/allocation-helper.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts b/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts index 81372787d..b14245b4d 100644 --- a/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts +++ b/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts @@ -90,7 +90,6 @@ test.describe("Allocator Weighting Tests", () => { const response = await sendSnsEvent(preparedEvent); expect(response.MessageId).toBeTruthy(); - console.log(`testStartedAt: ${testStartedAt}`); const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); const supplierFactorLog = await getAllocationLog( "Calculated supplier factors for allocation", diff --git a/tests/helpers/allocation-helper.ts b/tests/helpers/allocation-helper.ts index 1820ed8fe..b740bd9ba 100644 --- a/tests/helpers/allocation-helper.ts +++ b/tests/helpers/allocation-helper.ts @@ -57,7 +57,7 @@ type PackSpecificationLog = { constraintOperator?: string; }; -exporttype PackErrorLog = { +export type PackErrorLog = { description: string; letterVariantId?: string; packSpecificationId?: string[]; From 165ec605b0bfc6ee70cf8164ef6f58418235f1c6 Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Thu, 21 May 2026 10:47:22 +0000 Subject: [PATCH 07/12] fix --- .../allocation-tests/letter-allocation-rejected.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts b/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts index d6f7ffafe..7df152486 100644 --- a/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts +++ b/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts @@ -4,6 +4,7 @@ import { getAllocationLog, getAllocationLogForDomainId, getVariantsForAllocation, + PackErrorLog, } from "tests/helpers/allocation-helper"; import { createPreparedV1Event } from "tests/helpers/event-fixtures"; import { getLettersFromSupplierTable } from "tests/helpers/generate-fetch-test-data"; @@ -66,9 +67,7 @@ test.describe("Allocator Rejected Allocation Tests", () => { "No eligible pack specifications found for letter", ); - const packSpecificationIds = - supplierAllocatorLog.packSpecificationIds ?? - supplierAllocatorLog.packSpecificationId; + const packSpecificationIds = supplierAllocatorLog.packSpecificationId; expect(packSpecificationIds).toBeTruthy(); expect(supplierAllocatorLog.letterVariantId).toBe(letterVariant); From f88a9279b6c2867958ca41af4c0f9a6c8c35ca8e Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Thu, 21 May 2026 11:00:02 +0000 Subject: [PATCH 08/12] lint fix --- .../allocation-tests/letter-allocation-rejected.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts b/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts index 7df152486..cf305bf47 100644 --- a/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts +++ b/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts @@ -1,10 +1,10 @@ import { randomUUID } from "node:crypto"; import test, { expect } from "playwright/test"; import { + PackErrorLog, getAllocationLog, getAllocationLogForDomainId, getVariantsForAllocation, - PackErrorLog, } from "tests/helpers/allocation-helper"; import { createPreparedV1Event } from "tests/helpers/event-fixtures"; import { getLettersFromSupplierTable } from "tests/helpers/generate-fetch-test-data"; From b722f67379f5b33b5a099214d8456f57d6cfafb4 Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Thu, 21 May 2026 11:16:49 +0000 Subject: [PATCH 09/12] config --- config/suppliers/supplier-pack/supplier3-client1-campaign2.json | 2 +- config/suppliers/supplier-pack/supplier4-client1-campaign2.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/suppliers/supplier-pack/supplier3-client1-campaign2.json b/config/suppliers/supplier-pack/supplier3-client1-campaign2.json index 1783eb4c7..02720972f 100644 --- a/config/suppliers/supplier-pack/supplier3-client1-campaign2.json +++ b/config/suppliers/supplier-pack/supplier3-client1-campaign2.json @@ -1,6 +1,6 @@ { "approval": "SUBMITTED", - "id": "supplier1-client1-campaign2", + "id": "supplier3-client1-campaign2", "packSpecificationId": "client1-campaign2", "status": "INT", "supplierId": "supplier3" diff --git a/config/suppliers/supplier-pack/supplier4-client1-campaign2.json b/config/suppliers/supplier-pack/supplier4-client1-campaign2.json index b04bddf6a..cb124b2e2 100644 --- a/config/suppliers/supplier-pack/supplier4-client1-campaign2.json +++ b/config/suppliers/supplier-pack/supplier4-client1-campaign2.json @@ -1,6 +1,6 @@ { "approval": "SUBMITTED", - "id": "supplier1-client1-campaign2", + "id": "supplier4-client1-campaign2", "packSpecificationId": "client1-campaign2", "status": "INT", "supplierId": "supplier4" From 1421d42bb253c8e3cc85bb964121448caf68168b Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Thu, 21 May 2026 15:13:25 +0000 Subject: [PATCH 10/12] config changes --- .../letter-variant/client1-campaign2.json | 2 +- .../letter-variant/notify-first-test.json | 30 +++ .../pack-specification/client1-campaign2.json | 2 +- .../pack-specification/notify-first-test.json | 48 +++++ .../supplier5-volumeGroup-test5.json | 7 + .../supplier3-client1-campaign2.json | 4 +- .../supplier4-client1-campaign2.json | 4 +- .../supplier5-notify-first-test.json | 7 + config/suppliers/supplier/supplier5.json | 7 + .../volume-group/volumeGroup-test5.json | 7 + .../letter-allocation-capacity.spec.ts | 191 ++++++++++-------- .../letter-allocation-rejected.spec.ts | 4 +- .../letter-allocation-weighting.spec.ts | 6 - tests/helpers/allocation-helper.ts | 3 +- 14 files changed, 223 insertions(+), 99 deletions(-) create mode 100644 config/suppliers/letter-variant/notify-first-test.json create mode 100644 config/suppliers/pack-specification/notify-first-test.json create mode 100644 config/suppliers/supplier-allocation/supplier5-volumeGroup-test5.json create mode 100644 config/suppliers/supplier-pack/supplier5-notify-first-test.json create mode 100644 config/suppliers/supplier/supplier5.json create mode 100644 config/suppliers/volume-group/volumeGroup-test5.json diff --git a/config/suppliers/letter-variant/client1-campaign2.json b/config/suppliers/letter-variant/client1-campaign2.json index 0dac50d26..83ac0e308 100644 --- a/config/suppliers/letter-variant/client1-campaign2.json +++ b/config/suppliers/letter-variant/client1-campaign2.json @@ -32,7 +32,7 @@ "client1-campaign2" ], "priority": 1, - "status": "INT", + "status": "PROD", "type": "STANDARD", "volumeGroupId": "volumeGroup-test4" } diff --git a/config/suppliers/letter-variant/notify-first-test.json b/config/suppliers/letter-variant/notify-first-test.json new file mode 100644 index 000000000..8fa5993b0 --- /dev/null +++ b/config/suppliers/letter-variant/notify-first-test.json @@ -0,0 +1,30 @@ +{ + "constraints": { + "blackCoveragePercentage": { + "operator": "LESS_THAN", + "value": 20 + }, + "deliveryDays": { + "operator": "LESS_THAN", + "value": 2 + }, + "sheets": { + "operator": "LESS_THAN", + "value": 5 + }, + "sides": { + "operator": "LESS_THAN", + "value": 10 + } + }, + "description": "Black printing, first class postage tariff", + "id": "notify-first-test", + "name": "First class letter", + "packSpecificationIds": [ + "notify-first-test" + ], + "priority": 10, + "status": "DRAFT", + "type": "STANDARD", + "volumeGroupId": "volumeGroup-test5" +} diff --git a/config/suppliers/pack-specification/client1-campaign2.json b/config/suppliers/pack-specification/client1-campaign2.json index 9710d4aa8..7cc5b572d 100644 --- a/config/suppliers/pack-specification/client1-campaign2.json +++ b/config/suppliers/pack-specification/client1-campaign2.json @@ -49,7 +49,7 @@ "maxWeightGrams": 100, "size": "STANDARD" }, - "status": "INT", + "status": "PROD", "updatedAt": "2026-04-14T00:00:00.000Z", "version": 1 } diff --git a/config/suppliers/pack-specification/notify-first-test.json b/config/suppliers/pack-specification/notify-first-test.json new file mode 100644 index 000000000..f92d2f2f1 --- /dev/null +++ b/config/suppliers/pack-specification/notify-first-test.json @@ -0,0 +1,48 @@ +{ + "assembly": { + "duplex": true, + "envelopeId": "first-c5", + "paper": { + "colour": "WHITE", + "id": "paper-std-white-80", + "name": "Standard White 80gsm", + "recycled": true, + "size": "A4", + "weightGSM": 80 + }, + "printColour": "BLACK" + }, + "billingId": "notify-first-test", + "constraints": { + "blackCoveragePercentage": { + "operator": "LESS_THAN", + "value": 20 + }, + "deliveryDays": { + "operator": "LESS_THAN", + "value": 2 + }, + "sheets": { + "operator": "LESS_THAN", + "value": 5 + }, + "sides": { + "operator": "LESS_THAN", + "value": 10 + } + }, + "createdAt": "2026-01-12T00:00:00.000Z", + "description": "First class postage tariff", + "id": "notify-first-test", + "name": "First class", + "postage": { + "deliveryDays": 2, + "id": "first-class", + "maxThicknessMm": 5, + "maxWeightGrams": 100, + "size": "STANDARD" + }, + "status": "PROD", + "updatedAt": "2026-01-12T00:00:00.000Z", + "version": 1 +} diff --git a/config/suppliers/supplier-allocation/supplier5-volumeGroup-test5.json b/config/suppliers/supplier-allocation/supplier5-volumeGroup-test5.json new file mode 100644 index 000000000..b3339096c --- /dev/null +++ b/config/suppliers/supplier-allocation/supplier5-volumeGroup-test5.json @@ -0,0 +1,7 @@ +{ + "allocationPercentage": 100, + "id": "supplier5-volumeGroup-test5", + "status": "PROD", + "supplier": "supplier5", + "volumeGroup": "volumeGroup-test5" +} diff --git a/config/suppliers/supplier-pack/supplier3-client1-campaign2.json b/config/suppliers/supplier-pack/supplier3-client1-campaign2.json index 02720972f..d19f1bffd 100644 --- a/config/suppliers/supplier-pack/supplier3-client1-campaign2.json +++ b/config/suppliers/supplier-pack/supplier3-client1-campaign2.json @@ -1,7 +1,7 @@ { - "approval": "SUBMITTED", + "approval": "APPROVED", "id": "supplier3-client1-campaign2", "packSpecificationId": "client1-campaign2", - "status": "INT", + "status": "PROD", "supplierId": "supplier3" } diff --git a/config/suppliers/supplier-pack/supplier4-client1-campaign2.json b/config/suppliers/supplier-pack/supplier4-client1-campaign2.json index cb124b2e2..a131fddc2 100644 --- a/config/suppliers/supplier-pack/supplier4-client1-campaign2.json +++ b/config/suppliers/supplier-pack/supplier4-client1-campaign2.json @@ -1,7 +1,7 @@ { - "approval": "SUBMITTED", + "approval": "APPROVED", "id": "supplier4-client1-campaign2", "packSpecificationId": "client1-campaign2", - "status": "INT", + "status": "PROD", "supplierId": "supplier4" } diff --git a/config/suppliers/supplier-pack/supplier5-notify-first-test.json b/config/suppliers/supplier-pack/supplier5-notify-first-test.json new file mode 100644 index 000000000..21a5ab2b7 --- /dev/null +++ b/config/suppliers/supplier-pack/supplier5-notify-first-test.json @@ -0,0 +1,7 @@ +{ + "approval": "APPROVED", + "id": "supplier1-notify-first", + "packSpecificationId": "notify-first", + "status": "PROD", + "supplierId": "supplier1" +} diff --git a/config/suppliers/supplier/supplier5.json b/config/suppliers/supplier/supplier5.json new file mode 100644 index 000000000..fa7af6895 --- /dev/null +++ b/config/suppliers/supplier/supplier5.json @@ -0,0 +1,7 @@ +{ + "channelType": "LETTER", + "dailyCapacity": 500000, + "id": "supplier5", + "name": "Supplier5", + "status": "PROD" +} diff --git a/config/suppliers/volume-group/volumeGroup-test5.json b/config/suppliers/volume-group/volumeGroup-test5.json new file mode 100644 index 000000000..fec914c32 --- /dev/null +++ b/config/suppliers/volume-group/volumeGroup-test5.json @@ -0,0 +1,7 @@ +{ + "description": "Dev Test Volume Group 5", + "id": "volumeGroup-test5", + "name": "Dev Test Volume Group 5", + "startDate": "2026-01-01", + "status": "PROD" +} diff --git a/tests/component-tests/allocation-tests/letter-allocation-capacity.spec.ts b/tests/component-tests/allocation-tests/letter-allocation-capacity.spec.ts index 3a0f56b91..d8537efa5 100644 --- a/tests/component-tests/allocation-tests/letter-allocation-capacity.spec.ts +++ b/tests/component-tests/allocation-tests/letter-allocation-capacity.spec.ts @@ -87,99 +87,122 @@ test.describe("Allocator Lambda Tests", () => { expect(lettersInDb.specificationId).toBe(allocatedPackSpecId); }); - const supplierCapacityTestCases = [ - { - testCase: "Verify if suppliers without capacity are filtered out", - letterVariantId: 4, - expectedSupplierId: "supplier2", - }, - { - testCase: - "Verify that fallback is triggered when a suppliers are at daily capacity, ignoring capacity", - letterVariantId: 3, - expectedSupplierId: "supplier1", - }, - ]; - - for (const { - expectedSupplierId, - letterVariantId, - testCase, - } of supplierCapacityTestCases) { - test(testCase, async () => { - const letterVariant = getVariantsForAllocation(letterVariantId); - const domainId = randomUUID(); - const dailyAllocatedCapacity = 500_000; - const allocationDate = new Intl.DateTimeFormat("en-CA", { - timeZone: "Europe/London", - }).format(new Date()); - - const dailyAllocation = await getOrSeedLetterDailyAllocationFromDb( - { - supplier3: 0, - supplier4: 0, - }, + test("Verify if suppliers without capacity are filtered out", async () => { + const letterVariant = getVariantsForAllocation(4); + const domainId = `supcapacity-4-${randomUUID()}`; + const dailyAllocatedCapacity = 500_000; + const allocationDate = new Intl.DateTimeFormat("en-CA", { + timeZone: "Europe/London", + }).format(new Date()); + + const dailyAllocation = await getOrSeedLetterDailyAllocationFromDb( + { + supplier1: 0, + supplier2: 0, + }, + allocationDate, + ); + logger.info( + `Daily allocation before test execution ${JSON.stringify(dailyAllocation.allocations)}`, + ); + const originalSupplier1Allocation = + dailyAllocation.allocations.supplier3 ?? 0; + + // set supplier1 to exactly daily capacity so allocator filters it out + if (dailyAllocation.allocations.supplier1 !== dailyAllocatedCapacity) { + await updateSupplierDailyAllocation( + "supplier3", + dailyAllocatedCapacity, allocationDate, ); - logger.info( - `Daily allocation before test execution ${JSON.stringify(dailyAllocation.allocations)}`, - ); + } + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, + }); - const originalSupplier3Allocation = - dailyAllocation.allocations.supplier3 ?? 0; - - // set supplier3 to exactly daily capacity so allocator filters it out - if (dailyAllocation.allocations.supplier3 !== dailyAllocatedCapacity) { - await updateSupplierDailyAllocation( - "supplier3", - dailyAllocatedCapacity, - allocationDate, - ); - } - const preparedEvent = createPreparedV1Event({ - domainId, - letterVariantId: letterVariant, - }); - - const response = await sendSnsEvent(preparedEvent); - expect(response.MessageId).toBeTruthy(); - - const exceededCapacityLog = - await getExceededDailyCapacityLog("supplier3"); - expect(exceededCapacityLog.description).toBe( - "Supplier has exceeded daily capacity", - ); + const response = await sendSnsEvent(preparedEvent); + expect(response.MessageId).toBeTruthy(); - const supplierAllocatorLog = await getAllocationLogForDomainId(domainId); - const supplierDetails = - supplierAllocatorLog.msg?.allocationDetails?.supplierSpec; - expect(supplierDetails?.supplierId).toBe(expectedSupplierId); + const exceededCapacityLog = await getExceededDailyCapacityLog("supplier3"); + expect(exceededCapacityLog.description).toBe( + "Supplier has exceeded daily capacity", + ); - const lettersInDb = await getLettersFromSupplierTable( - expectedSupplierId, - domainId, - "PENDING", - ); - expect(lettersInDb.status).toBe("PENDING"); - expect(lettersInDb.specificationId).toBe( - supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.specId, - ); + const lettersInDb = await getLettersFromSupplierTable( + "supplier4", + domainId, + "PENDING", + ); + expect(lettersInDb.status).toBe("PENDING"); + expect(lettersInDb.supplierId).toBe("supplier4"); - if (testCase.includes("fallback")) { - const fallbackDailyAllocation = - await getOrSeedLetterDailyAllocationFromDb({ - supplier1: 0, - }); - expect(fallbackDailyAllocation.allocations.supplier1).toBe( - dailyAllocatedCapacity + 1, - ); - } + await updateSupplierDailyAllocation( + "supplier3", + originalSupplier1Allocation, + allocationDate, + ); + }); + test("Verify that fallback is triggered when a suppliers are at daily capacity, ignoring capacity", async () => { + const letterVariant = getVariantsForAllocation(5); + const domainId = `supcapacity-5-${randomUUID()}`; + const dailyAllocatedCapacity = 500_000; + const allocationDate = new Intl.DateTimeFormat("en-CA", { + timeZone: "Europe/London", + }).format(new Date()); + + const dailyAllocation = await getOrSeedLetterDailyAllocationFromDb( + { + supplier5: 0, + }, + allocationDate, + ); + logger.info( + `Daily allocation before test execution ${JSON.stringify(dailyAllocation.allocations)}`, + ); + + const originalSupplierAllocation = + dailyAllocation.allocations.supplier5 ?? 0; + + // set supplier1 to exactly daily capacity so allocator filters it out + if (dailyAllocation.allocations.supplier1 !== dailyAllocatedCapacity) { await updateSupplierDailyAllocation( - "supplier3", - originalSupplier3Allocation, + "supplier5", + dailyAllocatedCapacity, allocationDate, ); + } + const preparedEvent = createPreparedV1Event({ + domainId, + letterVariantId: letterVariant, }); - } + + const response = await sendSnsEvent(preparedEvent); + expect(response.MessageId).toBeTruthy(); + + const exceededCapacityLog = await getExceededDailyCapacityLog("supplier5"); + expect(exceededCapacityLog.description).toBe( + "Supplier has exceeded daily capacity", + ); + + const lettersInDb = await getLettersFromSupplierTable( + "supplier5", + domainId, + "PENDING", + ); + expect(lettersInDb.status).toBe("PENDING"); + const fallbackDailyAllocation = await getOrSeedLetterDailyAllocationFromDb({ + supplier5: 0, + }); + expect(fallbackDailyAllocation.allocations.supplier5).toBe( + dailyAllocatedCapacity + 1, + ); + + await updateSupplierDailyAllocation( + "supplier5", + originalSupplierAllocation, + allocationDate, + ); + }); }); diff --git a/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts b/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts index cf305bf47..45f0d73fd 100644 --- a/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts +++ b/tests/component-tests/allocation-tests/letter-allocation-rejected.spec.ts @@ -53,7 +53,7 @@ test.describe("Allocator Rejected Allocation Tests", () => { test("Verify that the letters are REJECTED when no pack specification is eligible", async () => { const letterVariant = getVariantsForAllocation(1); - const domainId = randomUUID(); + const domainId = `NoEligiblePackSpecs-${randomUUID()}`; const preparedEvent = createPreparedV1Event({ domainId, letterVariantId: letterVariant, @@ -67,7 +67,7 @@ test.describe("Allocator Rejected Allocation Tests", () => { "No eligible pack specifications found for letter", ); - const packSpecificationIds = supplierAllocatorLog.packSpecificationId; + const { packSpecificationIds } = supplierAllocatorLog; expect(packSpecificationIds).toBeTruthy(); expect(supplierAllocatorLog.letterVariantId).toBe(letterVariant); diff --git a/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts b/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts index b14245b4d..6f598c799 100644 --- a/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts +++ b/tests/component-tests/allocation-tests/letter-allocation-weighting.spec.ts @@ -17,7 +17,6 @@ import { buildWeightingSnapshot, getLowestWeightingSupplier, } from "tests/helpers/allocation-factor-helper"; -import { logger } from "tests/helpers/pino-logger"; test.describe("Allocator Weighting Tests", () => { test.setTimeout(180_000); @@ -223,11 +222,6 @@ test.describe("Allocator Weighting Tests", () => { supplierAllocatorLog.msg?.allocationDetails?.supplierSpec?.supplierId; expect(selectedSupplierId).toBe(lowestWeightingSupplier); - logger.info( - `Weighting snapshot: ${JSON.stringify(weightingSnapshot)}, Supplier factors from log: ${JSON.stringify( - supplierFactorLog.supplierFactors, - )}`, - ); expect(weightingSnapshot.supplier1.weighting).toBe( supplierFactorLog.supplierFactors?.find( (factor) => factor.supplierId === "supplier1", diff --git a/tests/helpers/allocation-helper.ts b/tests/helpers/allocation-helper.ts index b740bd9ba..34ad1a3d2 100644 --- a/tests/helpers/allocation-helper.ts +++ b/tests/helpers/allocation-helper.ts @@ -20,6 +20,7 @@ export const AllocationTestVariantMap: Record = { "client1-campaign1": 2, "notify-standard-colour": 3, "client1-campaign2": 4, + "notify-first-test": 5, }; export function getVariantsForAllocation(testCase: number) { @@ -60,7 +61,7 @@ type PackSpecificationLog = { export type PackErrorLog = { description: string; letterVariantId?: string; - packSpecificationId?: string[]; + packSpecificationIds?: string[]; }; export type SupplierFactorEntry = { From 8f9f1697ff29e842043fe35863c6194c3fecedb8 Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Thu, 21 May 2026 15:57:18 +0000 Subject: [PATCH 11/12] config changes --- config/suppliers/letter-variant/notify-first-test.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/suppliers/letter-variant/notify-first-test.json b/config/suppliers/letter-variant/notify-first-test.json index 8fa5993b0..a32388158 100644 --- a/config/suppliers/letter-variant/notify-first-test.json +++ b/config/suppliers/letter-variant/notify-first-test.json @@ -24,7 +24,7 @@ "notify-first-test" ], "priority": 10, - "status": "DRAFT", + "status": "PROD", "type": "STANDARD", "volumeGroupId": "volumeGroup-test5" } From 944fc505b201d16250e6edc04f9bd8182c910b9b Mon Sep 17 00:00:00 2001 From: Namitha-Prabhu Date: Fri, 22 May 2026 06:37:49 +0000 Subject: [PATCH 12/12] config fix --- .../supplier-pack/supplier5-notify-first-test.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/suppliers/supplier-pack/supplier5-notify-first-test.json b/config/suppliers/supplier-pack/supplier5-notify-first-test.json index 21a5ab2b7..e67ebfbc7 100644 --- a/config/suppliers/supplier-pack/supplier5-notify-first-test.json +++ b/config/suppliers/supplier-pack/supplier5-notify-first-test.json @@ -1,7 +1,7 @@ { "approval": "APPROVED", - "id": "supplier1-notify-first", - "packSpecificationId": "notify-first", + "id": "supplier5-notify-first-test", + "packSpecificationId": "notify-first-test", "status": "PROD", - "supplierId": "supplier1" + "supplierId": "supplier5" }