Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
9b57d8e
Add new endpoint to get mi information
RossFaulds171 Apr 7, 2026
7be4ed2
Merge branch 'main' into feature/CCM-12185-addGetMiEndpoint
RossFaulds171 Apr 7, 2026
f160ba2
remove TODOs
RossFaulds171 Apr 7, 2026
c1a3d3a
fix default imports for mi operations
RossFaulds171 Apr 7, 2026
d3fa12b
lint fixes
RossFaulds171 Apr 7, 2026
c4d6009
more lint fixes
RossFaulds171 Apr 7, 2026
b55c3df
yet more lint
RossFaulds171 Apr 7, 2026
c3d6b3a
lint fixes
RossFaulds171 Apr 7, 2026
a4219c5
more lint fixes
RossFaulds171 Apr 7, 2026
5d80195
lint fix
RossFaulds171 Apr 7, 2026
766fe7d
amend prerequesites in README.md
RossFaulds171 Apr 7, 2026
68bc7d1
Revert "lint fix"
RossFaulds171 Apr 7, 2026
0de123e
Revert "amend prerequesites in README.md"
RossFaulds171 Apr 7, 2026
3b04c0d
Merge branch 'main' into feature/CCM-12185-addGetMiEndpoint
RossFaulds171 Apr 7, 2026
eac269b
lint fix
RossFaulds171 Apr 7, 2026
4a8ae7f
fix type issues
RossFaulds171 Apr 7, 2026
9aaeec8
add back in data property
RossFaulds171 Apr 7, 2026
1c26896
fix typing
RossFaulds171 Apr 7, 2026
d01b663
fix mi typecheck
RossFaulds171 Apr 7, 2026
c3e3ef8
fix mi linting
RossFaulds171 Apr 7, 2026
41dbe10
add mi repository tests for exception
RossFaulds171 Apr 8, 2026
705e4e6
lint fix and improve test coverage
RossFaulds171 Apr 8, 2026
5eb29e4
add missing import getMI
RossFaulds171 Apr 8, 2026
01f2e52
more linting
RossFaulds171 Apr 8, 2026
3d995c9
implement createGetMIHandler
RossFaulds171 Apr 8, 2026
9844c13
lint fix
RossFaulds171 Apr 8, 2026
d6dfffb
more linting
RossFaulds171 Apr 8, 2026
2978aad
add api handler tests for get-mi
RossFaulds171 Apr 8, 2026
41bb81f
fix type errors due to casing
RossFaulds171 Apr 8, 2026
990ab71
amend json stringify format in test
RossFaulds171 Apr 8, 2026
82e105d
more linting
RossFaulds171 Apr 8, 2026
6644f4e
fix error enum
RossFaulds171 Apr 8, 2026
80519fa
refactor metrics
RossFaulds171 Apr 8, 2026
697e446
linting
RossFaulds171 Apr 8, 2026
b0e8ac6
more linting
RossFaulds171 Apr 8, 2026
8c3b890
yet more linting
RossFaulds171 Apr 8, 2026
2516dc6
add oas documentation for getMI endpoint
RossFaulds171 Apr 9, 2026
f0221e6
markdown fixes
RossFaulds171 Apr 9, 2026
64419b1
remove problematic english check regex
RossFaulds171 Apr 9, 2026
56a21bb
remove request body for getMI endpoint
RossFaulds171 Apr 9, 2026
01b2c70
add get_mi lambda module in terraform
RossFaulds171 Apr 9, 2026
1012d8e
add getmi handler to index.ts
RossFaulds171 Apr 9, 2026
934b660
wire up get_mi lambda in spec and tf
RossFaulds171 Apr 9, 2026
5568bae
more wiring for get_mi lambda
RossFaulds171 Apr 9, 2026
d57721b
refactor getMi for better not found handling
RossFaulds171 Apr 9, 2026
d60be84
merge latest main
RossFaulds171 Apr 9, 2026
07440f4
refactor 404 error handling for getMi as per letter operations
RossFaulds171 Apr 9, 2026
162fa86
fix notfound error handling for getMI
RossFaulds171 Apr 9, 2026
7325fa5
fix test
RossFaulds171 Apr 9, 2026
20a7afa
lint fix
RossFaulds171 Apr 9, 2026
6b36c2a
more lint fixes
RossFaulds171 Apr 9, 2026
3a701c2
yet more linting
RossFaulds171 Apr 9, 2026
72d06a4
rename NOTIFY_LETTER_NOT_FOUND to NOTIFY_NOT_FOUND
RossFaulds171 Apr 9, 2026
d883564
missed rename
RossFaulds171 Apr 9, 2026
8907183
Merge branch 'main' into feature/CCM-12185-addGetMiEndpoint
RossFaulds171 Apr 13, 2026
9d329c6
add get management information to postman collection
RossFaulds171 Apr 13, 2026
5650d70
remove getMI from new endpoint
RossFaulds171 Apr 14, 2026
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
1 change: 1 addition & 0 deletions infrastructure/terraform/components/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ No requirements.
| <a name="module_get_letter"></a> [get\_letter](#module\_get\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_get_letter_data"></a> [get\_letter\_data](#module\_get\_letter\_data) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_get_letters"></a> [get\_letters](#module\_get\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_get_mi"></a> [get\_mi](#module\_get\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_get_status"></a> [get\_status](#module\_get\_status) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_kms"></a> [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-kms.zip | n/a |
| <a name="module_lambda_alarms"></a> [lambda\_alarms](#module\_lambda\_alarms) | ../../modules/alarms-lambda | n/a |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
module.get_letter.function_arn,
module.get_letter_data.function_arn,
module.get_letters.function_arn,
module.get_mi.function_arn,
module.patch_letter.function_arn,
module.post_letters.function_arn,
module.get_status.function_arn,
Expand Down
1 change: 1 addition & 0 deletions infrastructure/terraform/components/api/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ locals {
PATCH_LETTER_LAMBDA_ARN = module.patch_letter.function_arn
POST_LETTERS_LAMBDA_ARN = module.post_letters.function_arn
POST_MI_LAMBDA_ARN = module.post_mi.function_arn
GET_MI_LAMBDA_ARN = module.get_mi.function_arn
})

destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"
Expand Down
1 change: 1 addition & 0 deletions infrastructure/terraform/components/api/locals_alarms.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ locals {
get_letter = module.get_letter.function_name
get_letters = module.get_letters.function_name
get_letter_data = module.get_letter_data.function_name
get_mi = module.get_mi.function_name
get_status = module.get_status.function_name
patch_letter = module.patch_letter.function_name
post_letters = module.post_letters.function_name
Expand Down
68 changes: 68 additions & 0 deletions infrastructure/terraform/components/api/module_lambda_get_mi.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module "get_mi" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip"

function_name = "get_mi"
description = "Retrieve management information"

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.get_mi_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 = "api-handler/dist"
function_include_common = true
handler_function_name = "getMI"
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, {})
}

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

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

resources = [
module.kms.key_arn, ## Requires shared kms module
]
}

statement {
sid = "AllowDynamoDBAccess"
effect = "Allow"

actions = [
"dynamodb:GetItem",
"dynamodb:Query"
]

resources = [
aws_dynamodb_table.mi.arn,
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,56 @@
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${POST_MI_LAMBDA_ARN}/invocations"
}
}
},
"/mi/{id}": {
"get": {
"description": "Returns 200 OK with management information.",
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad request, invalid input data"
},
"404": {
"description": "Resource not found"
},
"500": {
"description": "Server error"
}
},
"security": [
{
"LambdaAuthorizer": []
}
],
"summary": "Get MI",
"x-amazon-apigateway-integration": {
"contentHandling": "CONVERT_TO_TEXT",
"credentials": "${APIG_EXECUTION_ROLE_ARN}",
"httpMethod": "POST",
"passthroughBehavior": "WHEN_NO_TEMPLATES",
"responses": {
".*": {
"statusCode": "200"
}
},
"timeoutInMillis": 29000,
"type": "AWS_PROXY",
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${GET_MI_LAMBDA_ARN}/invocations"
}
},
"parameters": [
{
"description": "Unique identifier of this resource",
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
]
}
},
"x-amazon-apigateway-endpoint-access-mode": "${ENDPOINT_ACCESS_MODE}",
Expand Down
50 changes: 50 additions & 0 deletions internal/datastore/src/__test__/mi-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,54 @@ describe("MiRepository", () => {
);
});
});

describe("getMi", () => {
it("throws an error when fetching MI information that does not exist", async () => {
await expect(miRepository.getMI("XXX", "supplier1")).rejects.toThrow(
"Management information not found: supplierId=supplier1, miId=XXX",
);
});

it("creates MI with id and timestamps", async () => {
jest.useFakeTimers();
// Month is zero-indexed in JS Date
jest.setSystemTime(new Date(2020, 1, 1));
const mi = {
specificationId: "spec1",
supplierId: "supplier1",
groupId: "group1",
lineItem: "item1",
quantity: 12,
timestamp: new Date().toISOString(),
stockRemaining: 0,
};

const persistedMi = await miRepository.putMI(mi);

expect(persistedMi).toEqual(
expect.objectContaining({
id: expect.any(String),
createdAt: "2020-02-01T00:00:00.000Z",
updatedAt: "2020-02-01T00:00:00.000Z",
ttl: 1_580_518_800, // 2020-02-01T00:01:00.000Z, seconds since epoch
...mi,
}),
);

const fetchedMi = await miRepository.getMI(
persistedMi.id,
persistedMi.supplierId,
);

expect(fetchedMi).toEqual(
expect.objectContaining({
id: expect.any(String),
createdAt: "2020-02-01T00:00:00.000Z",
updatedAt: "2020-02-01T00:00:00.000Z",
ttl: 1_580_518_800, // 2020-02-01T00:01:00.000Z, seconds since epoch
...mi,
}),
);
});
});
});
14 changes: 14 additions & 0 deletions internal/datastore/src/errors/mi-not-found-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Error thrown when management information is not found in the database.
*/
export default class MiNotFoundError extends Error {
constructor(
public readonly supplierId: string,
public readonly miId: string,
) {
super(
`Management information not found: supplierId=${supplierId}, miId=${miId}`,
);
this.name = "MiNotFoundError";
}
}
1 change: 1 addition & 0 deletions internal/datastore/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { default as LetterQueueRepository } from "./letter-queue-repository";
export { default as DBHealthcheck } from "./healthcheck";
export { default as LetterAlreadyExistsError } from "./errors/letter-already-exists-error";
export { default as LetterNotFoundError } from "./errors/letter-not-found-error";
export { default as MiNotFoundError } from "./errors/mi-not-found-error";
25 changes: 24 additions & 1 deletion internal/datastore/src/mi-repository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import {
DynamoDBDocumentClient,
GetCommand,
PutCommand,
} from "@aws-sdk/lib-dynamodb";
import { Logger } from "pino";
import { randomUUID } from "node:crypto";
import { MI, MISchema } from "./types";
import MiNotFoundError from "./errors/mi-not-found-error";

export type MIRepositoryConfig = {
miTableName: string;
Expand Down Expand Up @@ -36,4 +41,22 @@ export class MIRepository {

return MISchema.parse(miDb);
}

async getMI(miId: string, supplierId: string): Promise<MI> {
const result = await this.ddbClient.send(
new GetCommand({
TableName: this.config.miTableName,
Key: {
id: miId,
supplierId,
},
}),
);

if (!result.Item) {
throw new MiNotFoundError(supplierId, miId);
}

return MISchema.parse(result.Item);
}
}
5 changes: 3 additions & 2 deletions lambdas/api-handler/src/contracts/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface ApiError {
export enum ApiErrorCode {
InternalServerError = "NOTIFY_INTERNAL_SERVER_ERROR",
InvalidRequest = "NOTIFY_INVALID_REQUEST",
NotFound = "NOTIFY_LETTER_NOT_FOUND",
NotFound = "NOTIFY_NOT_FOUND",
}

export enum ApiErrorTitle {
Expand All @@ -28,10 +28,11 @@ export enum ApiErrorStatus {
}

export enum ApiErrorDetail {
NotFoundLetterId = "No resource found with that ID",
NotFoundId = "No resource found with that ID",
InvalidRequestMissingBody = "The request is missing the body",
InvalidRequestMissingLetterIdPathParameter = "The request is missing the letter id path parameter",
InvalidRequestLetterIdsMismatch = "The letter ID in the request body does not match the letter ID path parameter",
InvalidRequestMissingMiIdPathParameter = "The request is missing the mi id path parameter",
InvalidRequestBody = "The request body is invalid",
InvalidRequestLimitNotANumber = "The limit parameter is not a number",
InvalidRequestLimitNotInRange = "The limit parameter must be a positive number not greater than %s",
Expand Down
13 changes: 13 additions & 0 deletions lambdas/api-handler/src/contracts/mi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,16 @@ export type PostMIResponse = z.infer<typeof PostMIResponseSchema>;
export type IncomingMI = PostMIRequest["data"]["attributes"] & {
supplierId: string;
};

export const GetMIResponseResourceSchema = z
.object({
id: z.string(),
...PostMIRequestResourceSchema.shape,
})
.strict();

export const GetMIResponseSchema = makeDocumentSchema(
GetMIResponseResourceSchema,
);

export type GetMIResponse = z.infer<typeof GetMIResponseSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ describe("API Lambda handler", () => {
it("returns 404 Not Found when letter matching id is not found", async () => {
const mockedGetLetterById = letterService.getLetterById as jest.Mock;
mockedGetLetterById.mockImplementation(() => {
throw new NotFoundError(ApiErrorDetail.NotFoundLetterId);
throw new NotFoundError(ApiErrorDetail.NotFoundId);
});

const event = makeApiGwEvent({
Expand Down
Loading
Loading