Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions infrastructure/terraform/components/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ No requirements.
| <a name="module_post_mi"></a> [post\_mi](#module\_post\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_s3bucket_test_letters"></a> [s3bucket\_test\_letters](#module\_s3bucket\_test\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-s3bucket.zip | n/a |
| <a name="module_sqs_letter_updates"></a> [sqs\_letter\_updates](#module\_sqs\_letter\_updates) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a |
| <a name="module_sqs_supplier_allocator"></a> [sqs\_supplier\_allocator](#module\_sqs\_supplier\_allocator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a |
| <a name="module_supplier_allocator"></a> [supplier\_allocator](#module\_supplier\_allocator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_supplier_ssl"></a> [supplier\_ssl](#module\_supplier\_ssl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-ssl.zip | n/a |
| <a name="module_upsert_letter"></a> [upsert\_letter](#module\_upsert\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
## Outputs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "aws_lambda_event_source_mapping" "supplier_allocator" {
event_source_arn = module.sqs_supplier_allocator.sqs_queue_arn
function_name = module.supplier_allocator.function_name
batch_size = 10
maximum_batching_window_in_seconds = 5
function_response_types = [
"ReportBatchItemFailures"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
module "supplier_allocator" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip"

function_name = "supplier-allocator"
description = "Allocate a letter to a supplier"

aws_account_id = var.aws_account_id
component = var.component
environment = var.environment
project = var.project
region = var.region
group = var.group

log_retention_in_days = var.log_retention_in_days
kms_key_arn = module.kms.key_arn

iam_policy_document = {
body = data.aws_iam_policy_document.supplier_allocator_lambda.json
}

function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
function_code_base_path = local.aws_lambda_functions_dir_path
function_code_dir = "supplier-allocator/dist"
function_include_common = true
handler_function_name = "supplierAllocatorHandler"
runtime = "nodejs22.x"
memory = 512
timeout = 29
log_level = var.log_level

force_lambda_code_deploy = var.force_lambda_code_deploy
enable_lambda_insights = false

log_destination_arn = local.destination_arn
log_subscription_role_arn = local.acct.log_subscription_role_arn

lambda_env_vars = merge(local.common_lambda_env_vars, {
VARIANT_MAP = jsonencode(var.letter_variant_map)
UPSERT_LETTERS_QUEUE_URL = module.sqs_letter_updates.sqs_queue_url
})
}

data "aws_iam_policy_document" "supplier_allocator_lambda" {
statement {
sid = "KMSPermissions"
effect = "Allow"

actions = [
"kms:Decrypt",
"kms:GenerateDataKey",
]

resources = [
module.kms.key_arn,
]
}

statement {
sid = "AllowSQSRead"
effect = "Allow"

actions = [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes"
]

resources = [
module.sqs_supplier_allocator.sqs_queue_arn
]
}

statement {
sid = "AllowSQSWrite"
effect = "Allow"

actions = [
"sqs:SendMessage"
]

resources = [
module.sqs_letter_updates.sqs_queue_arn
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module "sqs_supplier_allocator" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip"

aws_account_id = var.aws_account_id
component = var.component
environment = var.environment
project = var.project
region = var.region
name = "supplier-allocator"

sqs_kms_key_arn = module.kms.key_arn

visibility_timeout_seconds = 60

create_dlq = true
sqs_policy_overload = data.aws_iam_policy_document.supplier_allocator_queue_policy.json
}

data "aws_iam_policy_document" "supplier_allocator_queue_policy" {
version = "2012-10-17"

statement {
sid = "AllowSNSPermissions"
effect = "Allow"

principals {
type = "Service"
identifiers = ["sns.amazonaws.com"]
}

actions = [
"sqs:SendMessage",
"sqs:ListQueueTags",
"sqs:GetQueueUrl",
"sqs:GetQueueAttributes",
]

resources = [
"arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-supplier-allocator-queue"
]

condition {
test = "ArnEquals"
variable = "aws:SourceArn"
values = [module.eventsub.sns_topic.arn]
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
resource "aws_sns_topic_subscription" "eventsub_sqs_letter_updates" {
topic_arn = module.eventsub.sns_topic.arn
protocol = "sqs"
endpoint = module.sqs_letter_updates.sqs_queue_arn
topic_arn = module.eventsub.sns_topic.arn
protocol = "sqs"
endpoint = module.sqs_letter_updates.sqs_queue_arn
raw_message_delivery = true

filter_policy_scope = "MessageBody"
filter_policy = jsonencode({
type = [{ prefix = "uk.nhs.notify.supplier-api.letter" }]
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
resource "aws_sns_topic_subscription" "eventsub_sqs_supplier_allocator" {
topic_arn = module.eventsub.sns_topic.arn
protocol = "sqs"
endpoint = module.sqs_supplier_allocator.sqs_queue_arn
raw_message_delivery = true

filter_policy_scope = "MessageBody"
filter_policy = jsonencode({
type = [{ prefix = "uk.nhs.notify.letter-rendering.letter-request.prepared" }]
})
}
1 change: 1 addition & 0 deletions lambdas/supplier-allocator/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
4 changes: 4 additions & 0 deletions lambdas/supplier-allocator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
coverage
node_modules
dist
.reports
66 changes: 66 additions & 0 deletions lambdas/supplier-allocator/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
export const baseJestConfig = {
preset: "ts-jest",
extensionsToTreatAsEsm: [".ts"],
transform: {
"^.+\\.ts$": [
"ts-jest",
{
useESM: true,
},
],
},

// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,

// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,

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

// Indicates which provider should be used to instrument code for coverage
coverageProvider: "babel",

coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: -10,
},
},

coveragePathIgnorePatterns: ["/__tests__/"],
testPathIgnorePatterns: [".build"],
testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],

// Use this configuration option to add custom reporters to Jest
reporters: [
"default",
[
"jest-html-reporter",
{
pageTitle: "Test Report",
outputPath: "./.reports/unit/test-report.html",
includeFailureMsg: true,
},
],
],

// The test environment that will be used for testing
testEnvironment: "jsdom",
};

const utilsJestConfig = {
...baseJestConfig,

testEnvironment: "node",

coveragePathIgnorePatterns: [
...(baseJestConfig.coveragePathIgnorePatterns ?? []),
"zod-validators.ts",
],
};

export default utilsJestConfig;
36 changes: 36 additions & 0 deletions lambdas/supplier-allocator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.858.0",
"@aws-sdk/client-sqs": "^3.984.0",
"@aws-sdk/lib-dynamodb": "^3.858.0",
"@internal/datastore": "*",
"@internal/helpers": "^0.1.0",
"@nhsdigital/nhs-notify-event-schemas-letter-rendering": "^2.0.1",
"@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1": "npm:@nhsdigital/nhs-notify-event-schemas-letter-rendering@^1.1.5",
"@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.8",
"@types/aws-lambda": "^8.10.148",
"aws-lambda": "^1.0.7",
"esbuild": "^0.27.2",
"pino": "^9.7.0",
"zod": "^4.1.11"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
"@types/aws-lambda": "^8.10.148",
"@types/jest": "^30.0.0",
"jest": "^30.2.0",
"jest-mock-extended": "^4.0.0",
"ts-jest": "^29.4.0",
"typescript": "^5.8.3"
},
"name": "nhs-notify-supplier-api-allocate-letter",
"private": true,
"scripts": {
"lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test:unit": "jest",
"typecheck": "tsc --noEmit"
},
"version": "0.0.1"
}
43 changes: 43 additions & 0 deletions lambdas/supplier-allocator/src/config/__tests__/deps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Deps } from "lambdas/supplier-allocator/src/config/deps";

describe("createDependenciesContainer", () => {
const env = {
VARIANT_MAP: {
lv1: {
supplierId: "supplier1",
specId: "spec1",
},
},
};

beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();

// @internal/helpers - createLogger
jest.mock("@internal/helpers", () => ({
createLogger: jest.fn(() => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
level: "info",
})),
}));

// Env
jest.mock("../env", () => ({ envVars: env }));
});

test("constructs deps and wires repository config correctly", async () => {
// get current mock instances
const { createLogger } = jest.requireMock("@internal/helpers");

// eslint-disable-next-line @typescript-eslint/no-require-imports
const { createDependenciesContainer } = require("../deps");
const deps: Deps = createDependenciesContainer();
expect(createLogger).toHaveBeenCalledTimes(1);

expect(deps.env).toEqual(env);
});
});
42 changes: 42 additions & 0 deletions lambdas/supplier-allocator/src/config/__tests__/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ZodError } from "zod";
/* eslint-disable @typescript-eslint/no-require-imports */
/* Allow require imports to enable re-import of modules */

describe("lambdaEnv", () => {
const OLD_ENV = process.env;

beforeEach(() => {
jest.resetModules(); // Clears cached modules
process.env = { ...OLD_ENV }; // Clone original env
});

afterAll(() => {
process.env = OLD_ENV; // Restore
});

it("should load all environment variables successfully", () => {
process.env.VARIANT_MAP = `{
"lv1": {
"supplierId": "supplier1",
"specId": "spec1"
}
}`;

const { envVars } = require("../env");

expect(envVars).toEqual({
VARIANT_MAP: {
lv1: {
supplierId: "supplier1",
specId: "spec1",
},
},
});
});

it("should throw if a required env var is missing", () => {
process.env.VARIANT_MAP = undefined;

expect(() => require("../env")).toThrow(ZodError);
});
});
20 changes: 20 additions & 0 deletions lambdas/supplier-allocator/src/config/deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { SQSClient } from "@aws-sdk/client-sqs";
import { Logger } from "pino";
import { createLogger } from "@internal/helpers";
import { EnvVars, envVars } from "./env";

export type Deps = {
logger: Logger;
env: EnvVars;
sqsClient: SQSClient;
};

export function createDependenciesContainer(): Deps {
const log = createLogger({ logLevel: envVars.PINO_LOG_LEVEL });

return {
logger: log,
env: envVars,
sqsClient: new SQSClient({}),
};
}
Loading
Loading