Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -42,6 +42,7 @@ No requirements.

| Name | Source | Version |
|------|--------|---------|
| <a name="module_allocate_letter"></a> [allocate\_letter](#module\_allocate\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_authorizer_lambda"></a> [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_domain_truststore"></a> [domain\_truststore](#module\_domain\_truststore) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-s3bucket.zip | n/a |
| <a name="module_eventpub"></a> [eventpub](#module\_eventpub) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-eventpub.zip | n/a |
Expand All @@ -60,6 +61,7 @@ No requirements.
| <a name="module_post_letters"></a> [post\_letters](#module\_post\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <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_allocated_letters"></a> [sqs\_allocated\_letters](#module\_sqs\_allocated\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.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_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 |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "aws_lambda_event_source_mapping" "allocate_letter" {
event_source_arn = module.sqs_letter_updates.sqs_queue_arn
function_name = module.allocate_letter.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
@@ -1,5 +1,5 @@
resource "aws_lambda_event_source_mapping" "upsert_letter" {
event_source_arn = module.sqs_letter_updates.sqs_queue_arn
event_source_arn = module.sqs_allocated_letters.sqs_queue_arn
function_name = module.upsert_letter.function_name
batch_size = 10
maximum_batching_window_in_seconds = 5
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
module "allocate_letter" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip"

function_name = "allocate_letter"
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.allocate_letter_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 = "allocate-letter/dist"
function_include_common = true
handler_function_name = "allocateLetterHandler"
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)
ALLOCATED_LETTERS_QUEUE_URL = module.sqs_allocated_letters.sqs_queue_url
})
}

data "aws_iam_policy_document" "allocate_letter_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_letter_updates.sqs_queue_arn
]
}

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

actions = [
"sqs:SendMessage"
]

resources = [
module.sqs_allocated_letters.sqs_queue_arn
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ data "aws_iam_policy_document" "upsert_letter_lambda" {
]

resources = [
module.sqs_letter_updates.sqs_queue_arn
module.sqs_allocated_letters.sqs_queue_arn
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module "sqs_allocated_letters" {
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 = "allocated-letters"

sqs_kms_key_arn = module.kms.key_arn

visibility_timeout_seconds = 60

create_dlq = true
}
1 change: 1 addition & 0 deletions lambdas/allocate-letter/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
4 changes: 4 additions & 0 deletions lambdas/allocate-letter/.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/allocate-letter/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;
35 changes: 35 additions & 0 deletions lambdas/allocate-letter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.858.0",
"@aws-sdk/client-sqs": "^3.984.0",
"@aws-sdk/lib-dynamodb": "^3.858.0",
"@internal/datastore": "*",
"@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"
}
44 changes: 44 additions & 0 deletions lambdas/allocate-letter/src/config/__tests__/deps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Deps } from "lambdas/allocate-letter/src/config/deps";

describe("createDependenciesContainer", () => {
const env = {
LETTERS_TABLE_NAME: "LettersTable",
LETTER_TTL_HOURS: 12_960,
};

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

// pino
jest.mock("pino", () => ({
__esModule: true,
default: jest.fn(() => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
})),
}));

// Repo client
jest.mock("@internal/datastore", () => ({
LetterRepository: jest.fn(),
}));

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

test("constructs deps and wires repository config correctly", async () => {
// get current mock instances
const pinoMock = jest.requireMock("pino");

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

expect(pinoMock.default).toHaveBeenCalledTimes(1);
expect(deps.env).toEqual(env);
});
});
48 changes: 48 additions & 0 deletions lambdas/allocate-letter/src/config/__tests__/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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.LETTERS_TABLE_NAME = "letters-table";
process.env.LETTER_TTL_HOURS = "12960";
process.env.VARIANT_MAP = `{
"lv1": {
"supplierId": "supplier1",
"specId": "spec1"
}
}`;

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

expect(envVars).toEqual({
LETTERS_TABLE_NAME: "letters-table",
LETTER_TTL_HOURS: 12_960,
VARIANT_MAP: {
lv1: {
supplierId: "supplier1",
specId: "spec1",
},
},
});
});

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

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

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

export function createDependenciesContainer(): Deps {
const log = pino();

return {
logger: log,
env: envVars,
sqsClient: new SQSClient({}),
};
}
23 changes: 23 additions & 0 deletions lambdas/allocate-letter/src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { z } from "zod";

const LetterVariantSchema = z.record(
z.string(),
z.object({
supplierId: z.string(),
specId: z.string(),
}),
);
export type LetterVariant = z.infer<typeof LetterVariantSchema>;

const EnvVarsSchema = z.object({
LETTERS_TABLE_NAME: z.string(),
LETTER_TTL_HOURS: z.coerce.number().int(),
VARIANT_MAP: z.string().transform((str, _) => {
const parsed = JSON.parse(str);
return LetterVariantSchema.parse(parsed);
}),
});

export type EnvVars = z.infer<typeof EnvVarsSchema>;

export const envVars = EnvVarsSchema.parse(process.env);
Loading
Loading