From 9b57d8e48622b63088fb6701e5d56aae7db5dc1d Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 11:57:08 +0100 Subject: [PATCH 01/53] Add new endpoint to get mi information --- .../src/__test__/mi-repository.test.ts | 41 +++++++ internal/datastore/src/mi-repository.ts | 28 ++++- lambdas/api-handler/src/contracts/mi.ts | 26 +++++ lambdas/api-handler/src/handlers/get-mi.ts | 104 ++++++++++++++++++ lambdas/api-handler/src/mappers/mi-mapper.ts | 19 ++++ .../api-handler/src/services/mi-operations.ts | 14 ++- .../getMI_2WL5eYSWGzCHlGmzNxuqVusPxDg.json | 14 +++ .../api/components/endpoints/getMI.yml | 19 ++++ .../api/components/responses/getMI200.yml | 13 +++ specification/api/notify-supplier-phase1.yml | 8 ++ 10 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 lambdas/api-handler/src/handlers/get-mi.ts create mode 100644 sandbox/data/examples/getMI/responses/getMI_2WL5eYSWGzCHlGmzNxuqVusPxDg.json create mode 100644 specification/api/components/endpoints/getMI.yml create mode 100644 specification/api/components/responses/getMI200.yml diff --git a/internal/datastore/src/__test__/mi-repository.test.ts b/internal/datastore/src/__test__/mi-repository.test.ts index dd34e5d15..a68598613 100644 --- a/internal/datastore/src/__test__/mi-repository.test.ts +++ b/internal/datastore/src/__test__/mi-repository.test.ts @@ -64,4 +64,45 @@ describe("MiRepository", () => { ); }); }); + + describe("getMi", () => { + 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, + }) + ) + }); + }); + }); diff --git a/internal/datastore/src/mi-repository.ts b/internal/datastore/src/mi-repository.ts index 1a92fe715..78ad7bff7 100644 --- a/internal/datastore/src/mi-repository.ts +++ b/internal/datastore/src/mi-repository.ts @@ -1,4 +1,4 @@ -import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb"; +import { DynamoDBDocumentClient, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb"; import { Logger } from "pino"; import { randomUUID } from "node:crypto"; import { MI, MISchema } from "./types"; @@ -36,4 +36,30 @@ export class MIRepository { return MISchema.parse(miDb); } + + // TODO should the miId and supplierId be encapsulated in a getMIRequest + async getMI( + miId: string, + supplierId: string, + ): Promise { + + const result = await this.ddbClient.send( + new GetCommand({ + TableName: this.config.miTableName, + Key: { + id: miId, + supplierId, + }, + }), + ); + + if (!result.Item) { + throw new Error( + `Management Information with id ${miId} not found for supplier ${supplierId}`, + ); + } + + return MISchema.parse(result.Item); + } + } diff --git a/lambdas/api-handler/src/contracts/mi.ts b/lambdas/api-handler/src/contracts/mi.ts index 81a5d166e..93fdac473 100644 --- a/lambdas/api-handler/src/contracts/mi.ts +++ b/lambdas/api-handler/src/contracts/mi.ts @@ -1,6 +1,32 @@ import z from "zod"; import { makeDocumentSchema } from "./json-api"; +// TODO this is exactly the same as the PostMIRequestResourceSchema +// check if I should reuse this +export const GetMIResponseResourceSchema = z + .object({ + id: z.string(), + type: z.literal("ManagementInformation"), + attributes: z + .object({ + lineItem: z.string(), + timestamp: z.string(), + quantity: z.string(), + specificationId: z.string().optional(), + groupId: z.string().optional(), + stockRemaining: z.number().optional() + }) + .strict(), + }) + .strict(); + +export const GetMIResponseSchema = makeDocumentSchema( + GetMIResponseResourceSchema, +); + +export type GetMIResponse = z.infer; + + export const PostMIRequestResourceSchema = z .object({ type: z.literal("ManagementInformation"), diff --git a/lambdas/api-handler/src/handlers/get-mi.ts b/lambdas/api-handler/src/handlers/get-mi.ts new file mode 100644 index 000000000..74409cdc7 --- /dev/null +++ b/lambdas/api-handler/src/handlers/get-mi.ts @@ -0,0 +1,104 @@ +import { APIGatewayProxyHandler } from "aws-lambda"; +import { Unit } from "aws-embedded-metrics"; +import pino from "pino"; +import { MetricEntry, MetricStatus, buildEMFObject } from "@internal/helpers"; +import postMIOperation from "../services/mi-operations"; +import { ApiErrorDetail } from "../contracts/errors"; +import ValidationError from "../errors/validation-error"; +import { processError } from "../mappers/error-mapper"; +import { assertNotEmpty, validateIso8601Timestamp } from "../utils/validation"; +import { extractCommonIds } from "../utils/common-ids"; +import { PostMIRequest, PostMIRequestSchema } from "../contracts/mi"; +import { mapToMI } from "../mappers/mi-mapper"; +import { Deps } from "../config/deps"; + +export default function createPostMIHandler( + deps: Deps, +): APIGatewayProxyHandler { + return async (event) => { + const commonIds = extractCommonIds( + event.headers, + event.requestContext, + deps, + ); + + if (!commonIds.ok) { + return processError( + commonIds.error, + commonIds.correlationId, + deps.logger, + ); + } + + const { supplierId } = commonIds.value; + try { + const body = assertNotEmpty( + event.body, + new ValidationError(ApiErrorDetail.InvalidRequestMissingBody), + ); + + let postMIRequest: PostMIRequest; + + try { + postMIRequest = PostMIRequestSchema.parse(JSON.parse(body)); + } catch (error) { + emitErrorMetric(supplierId, deps.logger); + const typedError = + error instanceof Error + ? new ValidationError(ApiErrorDetail.InvalidRequestBody, { + cause: error, + }) + : error; + throw typedError; + } + validateIso8601Timestamp(postMIRequest.data.attributes.timestamp); + + const result = await postMIOperation( + mapToMI(postMIRequest, supplierId), + deps.miRepo, + ); + + deps.logger.info({ + description: "Posted management information", + supplierId: commonIds.value.supplierId, + correlationId: commonIds.value.correlationId, + }); + + // metric with count 1 specifying the supplier + const dimensions: Record = { supplier: supplierId }; + const metric: MetricEntry = { + key: MetricStatus.Success, + value: 1, + unit: Unit.Count, + }; + let emf = buildEMFObject("postMi", dimensions, metric); + deps.logger.info(emf); + + // metric displaying the type/number of lineItems posted per supplier + dimensions.lineItem = postMIRequest.data.attributes.lineItem; + metric.key = "LineItem per supplier"; + metric.value = postMIRequest.data.attributes.quantity; + emf = buildEMFObject("postMi", dimensions, metric); + deps.logger.info(emf); + + return { + statusCode: 201, + body: JSON.stringify(result, null, 2), + }; + } catch (error) { + emitErrorMetric(supplierId, deps.logger); + return processError(error, commonIds.value.correlationId, deps.logger); + } + }; +} + +function emitErrorMetric(supplierId: string, logger: pino.Logger) { + const dimensions: Record = { supplier: supplierId }; + const metric: MetricEntry = { + key: MetricStatus.Failure, + value: 1, + unit: Unit.Count, + }; + const emf = buildEMFObject("postMi", dimensions, metric); + logger.info(emf); +} diff --git a/lambdas/api-handler/src/mappers/mi-mapper.ts b/lambdas/api-handler/src/mappers/mi-mapper.ts index d20d19b69..745f0172c 100644 --- a/lambdas/api-handler/src/mappers/mi-mapper.ts +++ b/lambdas/api-handler/src/mappers/mi-mapper.ts @@ -4,6 +4,8 @@ import { PostMIRequest, PostMIResponse, PostMIResponseSchema, + GetMIResponse, + GetMIResponseResourceSchema } from "../contracts/mi"; export function mapToMI( @@ -32,3 +34,20 @@ export function mapToPostMIResponse(mi: MIBase): PostMIResponse { }, }); } + +export function mapToGetMIResponse(mi: MIBase): GetMIResponse { + return GetMIResponseResourceSchema.parse({ + data: { + id: mi.id, + type: "ManagementInformation", + attributes: { + lineItem: mi.lineItem, + timestamp: mi.timestamp, + quantity: mi.quantity, + specificationId: mi.specificationId, + groupId: mi.groupId, + stockRemaining: mi.stockRemaining, + }, + }, + }) +} diff --git a/lambdas/api-handler/src/services/mi-operations.ts b/lambdas/api-handler/src/services/mi-operations.ts index a6b791e44..346763f4b 100644 --- a/lambdas/api-handler/src/services/mi-operations.ts +++ b/lambdas/api-handler/src/services/mi-operations.ts @@ -1,12 +1,18 @@ import { MIRepository } from "@internal/datastore/src/mi-repository"; -import { IncomingMI, PostMIResponse } from "../contracts/mi"; -import { mapToPostMIResponse } from "../mappers/mi-mapper"; +import { IncomingMI, PostMIResponse, GetMIResponse } from "../contracts/mi"; +import { mapToPostMIResponse, mapToGetMIResponse } from "../mappers/mi-mapper"; -const postMI = async ( +export const postMI = async ( incomingMi: IncomingMI, miRepo: MIRepository, ): Promise => { return mapToPostMIResponse(await miRepo.putMI(incomingMi)); }; -export default postMI; +export const getMI = async ( + miId: string, + supplierId: string, + miRepo: MIRepository, +): Promise => { + return mapToGetMIResponse(await miRepo.getMI(miId, supplierId)); +}; diff --git a/sandbox/data/examples/getMI/responses/getMI_2WL5eYSWGzCHlGmzNxuqVusPxDg.json b/sandbox/data/examples/getMI/responses/getMI_2WL5eYSWGzCHlGmzNxuqVusPxDg.json new file mode 100644 index 000000000..87ba61bfc --- /dev/null +++ b/sandbox/data/examples/getMI/responses/getMI_2WL5eYSWGzCHlGmzNxuqVusPxDg.json @@ -0,0 +1,14 @@ +{ + "data": { + "attributes": { + "groupId": "abc123", + "lineItem": "envelope-business-standard", + "quantity": 22, + "specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg", + "stockRemaining": 2000, + "timestamp": "2023-11-17T14:27:51.413Z" + }, + "id": "2WL5eYSWGzCHlGmzNxuqVusPxDg", + "type": "ManagementInformation" + } +} diff --git a/specification/api/components/endpoints/getMI.yml b/specification/api/components/endpoints/getMI.yml new file mode 100644 index 000000000..b31d117aa --- /dev/null +++ b/specification/api/components/endpoints/getMI.yml @@ -0,0 +1,19 @@ +summary: Fetch an existing MI record +description: + $ref: '../documentation/getMI.md' +operationId: getMI +tags: + - mi +requestBody: + $ref: '../requests/getMIRequest.yml' +responses: + "200": + $ref: "../responses/getMI200.yml" + "404": + $ref: "../responses/errors/resourceNotFound.yml" + "429": + $ref: "../responses/errors/tooManyRequests.yml" + "500": + $ref: "../responses/errors/serverError.yml" + "502": + $ref: "../responses/errors/badGateway.yml" diff --git a/specification/api/components/responses/getMI200.yml b/specification/api/components/responses/getMI200.yml new file mode 100644 index 000000000..d8ef96a68 --- /dev/null +++ b/specification/api/components/responses/getMI200.yml @@ -0,0 +1,13 @@ +description: "Management information returned successfully" +headers: + X-Request-ID: + $ref: "../responseHeaders/xRequestId.yml" + X-Correlation-ID: + $ref: "../responseHeaders/xCorrelationId.yml" +content: + application/vnd.api+json: + schema: + $ref: "../schemas/miResponse.yml" + examples: + get-mi-response: + $ref: "../examples/getMI/responses/getMI_2WL5eYSWGzCHlGmzNxuqVusPxDg.json" diff --git a/specification/api/notify-supplier-phase1.yml b/specification/api/notify-supplier-phase1.yml index 2e26e8b76..519b62f70 100644 --- a/specification/api/notify-supplier-phase1.yml +++ b/specification/api/notify-supplier-phase1.yml @@ -43,6 +43,14 @@ paths: - $ref: 'components/parameters/correlationId.yml' post: $ref: 'components/endpoints/createMI.yml' + '/mi/getMI/{id}': + parameters: + - $ref: 'components/parameters/authorization/authorization.yml' + - $ref: 'components/parameters/requestId.yml' + - $ref: 'components/parameters/correlationId.yml' + - $ref: 'components/parameters/resourceId.yml' + get: + $ref: 'components/endpoints/getMI.yml' /_status: get: description: Returns 200 OK if the service is up. From f160ba29584f583d12748687a76ae351ccdc2ef9 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 12:17:15 +0100 Subject: [PATCH 02/53] remove TODOs --- internal/datastore/src/mi-repository.ts | 1 - lambdas/api-handler/src/contracts/mi.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/internal/datastore/src/mi-repository.ts b/internal/datastore/src/mi-repository.ts index 78ad7bff7..2bb320fb5 100644 --- a/internal/datastore/src/mi-repository.ts +++ b/internal/datastore/src/mi-repository.ts @@ -37,7 +37,6 @@ export class MIRepository { return MISchema.parse(miDb); } - // TODO should the miId and supplierId be encapsulated in a getMIRequest async getMI( miId: string, supplierId: string, diff --git a/lambdas/api-handler/src/contracts/mi.ts b/lambdas/api-handler/src/contracts/mi.ts index 93fdac473..f2e0fb27c 100644 --- a/lambdas/api-handler/src/contracts/mi.ts +++ b/lambdas/api-handler/src/contracts/mi.ts @@ -1,8 +1,6 @@ import z from "zod"; import { makeDocumentSchema } from "./json-api"; -// TODO this is exactly the same as the PostMIRequestResourceSchema -// check if I should reuse this export const GetMIResponseResourceSchema = z .object({ id: z.string(), From c1a3d3a07670d6c53f944cc44f4237cb88ff3e01 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 12:33:12 +0100 Subject: [PATCH 03/53] fix default imports for mi operations --- lambdas/api-handler/src/handlers/__tests__/post-mi.test.ts | 2 +- lambdas/api-handler/src/handlers/get-mi.ts | 4 ++-- lambdas/api-handler/src/handlers/post-mi.ts | 2 +- .../api-handler/src/services/__tests__/mi-operations.test.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lambdas/api-handler/src/handlers/__tests__/post-mi.test.ts b/lambdas/api-handler/src/handlers/__tests__/post-mi.test.ts index c10908799..a5eee1812 100644 --- a/lambdas/api-handler/src/handlers/__tests__/post-mi.test.ts +++ b/lambdas/api-handler/src/handlers/__tests__/post-mi.test.ts @@ -4,7 +4,7 @@ import pino from "pino"; import { MIRepository } from "@internal/datastore/src"; import { makeApiGwEvent } from "./utils/test-utils"; import { PostMIRequest, PostMIResponse } from "../../contracts/mi"; -import postMiOperation from "../../services/mi-operations"; +import { postMI as postMiOperation } from "../../services/mi-operations"; import { Deps } from "../../config/deps"; import { EnvVars } from "../../config/env"; import createPostMIHandler from "../post-mi"; diff --git a/lambdas/api-handler/src/handlers/get-mi.ts b/lambdas/api-handler/src/handlers/get-mi.ts index 74409cdc7..72a235a73 100644 --- a/lambdas/api-handler/src/handlers/get-mi.ts +++ b/lambdas/api-handler/src/handlers/get-mi.ts @@ -2,7 +2,7 @@ import { APIGatewayProxyHandler } from "aws-lambda"; import { Unit } from "aws-embedded-metrics"; import pino from "pino"; import { MetricEntry, MetricStatus, buildEMFObject } from "@internal/helpers"; -import postMIOperation from "../services/mi-operations"; +import { postMI as postMIOperation } from "../services/mi-operations"; import { ApiErrorDetail } from "../contracts/errors"; import ValidationError from "../errors/validation-error"; import { processError } from "../mappers/error-mapper"; @@ -12,7 +12,7 @@ import { PostMIRequest, PostMIRequestSchema } from "../contracts/mi"; import { mapToMI } from "../mappers/mi-mapper"; import { Deps } from "../config/deps"; -export default function createPostMIHandler( +export default function createGettMIHandler( deps: Deps, ): APIGatewayProxyHandler { return async (event) => { diff --git a/lambdas/api-handler/src/handlers/post-mi.ts b/lambdas/api-handler/src/handlers/post-mi.ts index 74409cdc7..f39759183 100644 --- a/lambdas/api-handler/src/handlers/post-mi.ts +++ b/lambdas/api-handler/src/handlers/post-mi.ts @@ -2,7 +2,7 @@ import { APIGatewayProxyHandler } from "aws-lambda"; import { Unit } from "aws-embedded-metrics"; import pino from "pino"; import { MetricEntry, MetricStatus, buildEMFObject } from "@internal/helpers"; -import postMIOperation from "../services/mi-operations"; +import { postMI as postMIOperation } from "../services/mi-operations"; import { ApiErrorDetail } from "../contracts/errors"; import ValidationError from "../errors/validation-error"; import { processError } from "../mappers/error-mapper"; diff --git a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts index f831c4061..65bf033ba 100644 --- a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts +++ b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts @@ -1,5 +1,5 @@ import { IncomingMI } from "../../contracts/mi"; -import postMI from "../mi-operations"; +import {postMI} from "../mi-operations"; describe("postMI function", () => { const incomingMi: IncomingMI = { From d3fa12baa913c6eeebd35fb94e97fb41f859188c Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 12:56:26 +0100 Subject: [PATCH 04/53] lint fixes --- lambdas/api-handler/src/contracts/mi.ts | 5 ++--- lambdas/api-handler/src/mappers/mi-mapper.ts | 6 +++--- .../src/services/__tests__/mi-operations.test.ts | 2 +- lambdas/api-handler/src/services/mi-operations.ts | 4 ++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lambdas/api-handler/src/contracts/mi.ts b/lambdas/api-handler/src/contracts/mi.ts index f2e0fb27c..f86c43ec1 100644 --- a/lambdas/api-handler/src/contracts/mi.ts +++ b/lambdas/api-handler/src/contracts/mi.ts @@ -12,7 +12,7 @@ export const GetMIResponseResourceSchema = z quantity: z.string(), specificationId: z.string().optional(), groupId: z.string().optional(), - stockRemaining: z.number().optional() + stockRemaining: z.number().optional(), }) .strict(), }) @@ -22,8 +22,7 @@ export const GetMIResponseSchema = makeDocumentSchema( GetMIResponseResourceSchema, ); -export type GetMIResponse = z.infer; - +export type GetMIResponse = z.infer; export const PostMIRequestResourceSchema = z .object({ diff --git a/lambdas/api-handler/src/mappers/mi-mapper.ts b/lambdas/api-handler/src/mappers/mi-mapper.ts index 745f0172c..7b8ba422a 100644 --- a/lambdas/api-handler/src/mappers/mi-mapper.ts +++ b/lambdas/api-handler/src/mappers/mi-mapper.ts @@ -1,11 +1,11 @@ import { MIBase } from "@internal/datastore/src"; import { + GetMIResponse, + GetMIResponseResourceSchema, IncomingMI, PostMIRequest, PostMIResponse, PostMIResponseSchema, - GetMIResponse, - GetMIResponseResourceSchema } from "../contracts/mi"; export function mapToMI( @@ -49,5 +49,5 @@ export function mapToGetMIResponse(mi: MIBase): GetMIResponse { stockRemaining: mi.stockRemaining, }, }, - }) + }); } diff --git a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts index 65bf033ba..334871f04 100644 --- a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts +++ b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts @@ -1,5 +1,5 @@ import { IncomingMI } from "../../contracts/mi"; -import {postMI} from "../mi-operations"; +import { postMI } from "../mi-operations"; describe("postMI function", () => { const incomingMi: IncomingMI = { diff --git a/lambdas/api-handler/src/services/mi-operations.ts b/lambdas/api-handler/src/services/mi-operations.ts index 346763f4b..e5d9e0e79 100644 --- a/lambdas/api-handler/src/services/mi-operations.ts +++ b/lambdas/api-handler/src/services/mi-operations.ts @@ -1,6 +1,6 @@ import { MIRepository } from "@internal/datastore/src/mi-repository"; -import { IncomingMI, PostMIResponse, GetMIResponse } from "../contracts/mi"; -import { mapToPostMIResponse, mapToGetMIResponse } from "../mappers/mi-mapper"; +import { GetMIResponse, IncomingMI, PostMIResponse } from "../contracts/mi"; +import { mapToGetMIResponse, mapToPostMIResponse } from "../mappers/mi-mapper"; export const postMI = async ( incomingMi: IncomingMI, From c4d6009e9a2fe20224f4a03612c98abf7147b491 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 13:10:50 +0100 Subject: [PATCH 05/53] more lint fixes --- .../datastore/src/__test__/mi-repository.test.ts | 10 +++++++--- internal/datastore/src/mi-repository.ts | 12 ++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/internal/datastore/src/__test__/mi-repository.test.ts b/internal/datastore/src/__test__/mi-repository.test.ts index a68598613..87eeb4423 100644 --- a/internal/datastore/src/__test__/mi-repository.test.ts +++ b/internal/datastore/src/__test__/mi-repository.test.ts @@ -92,7 +92,11 @@ describe("MiRepository", () => { }), ); - const fetchedMi = await miRepository.getMI(persistedMi.id, persistedMi.supplierId); + const fetchedMi = await miRepository.getMI( + persistedMi.id, + persistedMi.supplierId, + ); + expect(fetchedMi).toEqual( expect.objectContaining({ id: expect.any(String), @@ -100,8 +104,8 @@ describe("MiRepository", () => { updatedAt: "2020-02-01T00:00:00.000Z", ttl: 1_580_518_800, // 2020-02-01T00:01:00.000Z, seconds since epoch ...mi, - }) - ) + }), + ); }); }); diff --git a/internal/datastore/src/mi-repository.ts b/internal/datastore/src/mi-repository.ts index 2bb320fb5..0770589cb 100644 --- a/internal/datastore/src/mi-repository.ts +++ b/internal/datastore/src/mi-repository.ts @@ -1,4 +1,8 @@ -import { DynamoDBDocumentClient, PutCommand, GetCommand } 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"; @@ -37,10 +41,7 @@ export class MIRepository { return MISchema.parse(miDb); } - async getMI( - miId: string, - supplierId: string, - ): Promise { + async getMI(miId: string, supplierId: string): Promise { const result = await this.ddbClient.send( new GetCommand({ @@ -60,5 +61,4 @@ export class MIRepository { return MISchema.parse(result.Item); } - } From b55c3dfaf4bf43ca6e3adb4f96e279b269c6bae8 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 13:16:17 +0100 Subject: [PATCH 06/53] yet more lint --- internal/datastore/src/__test__/mi-repository.test.ts | 2 +- internal/datastore/src/mi-repository.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/datastore/src/__test__/mi-repository.test.ts b/internal/datastore/src/__test__/mi-repository.test.ts index 87eeb4423..3ba673e51 100644 --- a/internal/datastore/src/__test__/mi-repository.test.ts +++ b/internal/datastore/src/__test__/mi-repository.test.ts @@ -93,7 +93,7 @@ describe("MiRepository", () => { ); const fetchedMi = await miRepository.getMI( - persistedMi.id, + persistedMi.id, persistedMi.supplierId, ); diff --git a/internal/datastore/src/mi-repository.ts b/internal/datastore/src/mi-repository.ts index 0770589cb..5a44fb56d 100644 --- a/internal/datastore/src/mi-repository.ts +++ b/internal/datastore/src/mi-repository.ts @@ -1,7 +1,7 @@ -import { - DynamoDBDocumentClient, - GetCommand, - PutCommand, +import { + DynamoDBDocumentClient, + GetCommand, + PutCommand, } from "@aws-sdk/lib-dynamodb"; import { Logger } from "pino"; import { randomUUID } from "node:crypto"; From c3d6b3a2865f93d8f5ec44d7bbf2624c622a7000 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 13:25:55 +0100 Subject: [PATCH 07/53] lint fixes --- .../src/__test__/mi-repository.test.ts | 1 - internal/datastore/src/mi-repository.ts | 31 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/internal/datastore/src/__test__/mi-repository.test.ts b/internal/datastore/src/__test__/mi-repository.test.ts index 3ba673e51..05942b5d2 100644 --- a/internal/datastore/src/__test__/mi-repository.test.ts +++ b/internal/datastore/src/__test__/mi-repository.test.ts @@ -108,5 +108,4 @@ describe("MiRepository", () => { ); }); }); - }); diff --git a/internal/datastore/src/mi-repository.ts b/internal/datastore/src/mi-repository.ts index 5a44fb56d..ff52eb2bf 100644 --- a/internal/datastore/src/mi-repository.ts +++ b/internal/datastore/src/mi-repository.ts @@ -42,23 +42,22 @@ export class MIRepository { } async getMI(miId: string, supplierId: string): Promise { - - const result = await this.ddbClient.send( - new GetCommand({ - TableName: this.config.miTableName, - Key: { - id: miId, - supplierId, - }, - }), - ); - - if (!result.Item) { - throw new Error( - `Management Information with id ${miId} not found for supplier ${supplierId}`, + const result = await this.ddbClient.send( + new GetCommand({ + TableName: this.config.miTableName, + Key: { + id: miId, + supplierId, + }, + }), ); - } - return MISchema.parse(result.Item); + if (!result.Item) { + throw new Error( + `Management Information with id ${miId} not found for supplier ${supplierId}`, + ); + } + + return MISchema.parse(result.Item); } } From a4219c5518914572552c4ac74dbfaf0206b2ea5f Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 13:42:16 +0100 Subject: [PATCH 08/53] more lint fixes --- internal/datastore/src/mi-repository.ts | 33 +++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/internal/datastore/src/mi-repository.ts b/internal/datastore/src/mi-repository.ts index ff52eb2bf..d03478588 100644 --- a/internal/datastore/src/mi-repository.ts +++ b/internal/datastore/src/mi-repository.ts @@ -41,23 +41,24 @@ export class MIRepository { return MISchema.parse(miDb); } - async getMI(miId: string, supplierId: string): Promise { - const result = await this.ddbClient.send( - new GetCommand({ - TableName: this.config.miTableName, - Key: { - id: miId, - supplierId, - }, - }), - ); + async getMI(miId: string, supplierId: string): Promise { + + const result = await this.ddbClient.send( + new GetCommand({ + TableName: this.config.miTableName, + Key: { + id: miId, + supplierId, + }, + }), + ); - if (!result.Item) { - throw new Error( - `Management Information with id ${miId} not found for supplier ${supplierId}`, - ); - } + if (!result.Item) { + throw new Error( + `Management Information with id ${miId} not found for supplier ${supplierId}`, + ); + } - return MISchema.parse(result.Item); + return MISchema.parse(result.Item); } } From 5d80195a8772b19c612c7848f45a5b9e2618aa23 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 13:49:40 +0100 Subject: [PATCH 09/53] lint fix --- internal/datastore/src/mi-repository.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/datastore/src/mi-repository.ts b/internal/datastore/src/mi-repository.ts index d03478588..f61bae665 100644 --- a/internal/datastore/src/mi-repository.ts +++ b/internal/datastore/src/mi-repository.ts @@ -42,7 +42,6 @@ export class MIRepository { } async getMI(miId: string, supplierId: string): Promise { - const result = await this.ddbClient.send( new GetCommand({ TableName: this.config.miTableName, From 766fe7d9cb4be8eb08c552412a4afe0207880787 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 14:06:47 +0100 Subject: [PATCH 10/53] amend prerequesites in README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8e61610ca..cf8a160ca 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ New developers of the NHS Notify Supplier API should understand the below. #### Prerequisites and Configuration +- Copy .env.template to .env and edit for your specific PR / Github tokens +- Extract your Zscaler CA certificate using Certificate manager on windows or extract from Keychain from Mac and copy to the /scripts/devcontainer/custom-ca-certs/ folder - Utilised the devcontainer, for pre reqs and configuration. - You should open in a devcontainer or a Github workspaces. - By default it will run `make config` when the container is first setup From 68bc7d1b45f3c49cebb4172152035f53b9b13945 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 14:12:20 +0100 Subject: [PATCH 11/53] Revert "lint fix" This reverts commit 5d80195a8772b19c612c7848f45a5b9e2618aa23. --- internal/datastore/src/mi-repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/datastore/src/mi-repository.ts b/internal/datastore/src/mi-repository.ts index f61bae665..d03478588 100644 --- a/internal/datastore/src/mi-repository.ts +++ b/internal/datastore/src/mi-repository.ts @@ -42,6 +42,7 @@ export class MIRepository { } async getMI(miId: string, supplierId: string): Promise { + const result = await this.ddbClient.send( new GetCommand({ TableName: this.config.miTableName, From 0de123eaf100d54c41b5eaac14aa0ae98d6914b1 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 14:13:01 +0100 Subject: [PATCH 12/53] Revert "amend prerequesites in README.md" This reverts commit 766fe7d9cb4be8eb08c552412a4afe0207880787. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index cf8a160ca..8e61610ca 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,6 @@ New developers of the NHS Notify Supplier API should understand the below. #### Prerequisites and Configuration -- Copy .env.template to .env and edit for your specific PR / Github tokens -- Extract your Zscaler CA certificate using Certificate manager on windows or extract from Keychain from Mac and copy to the /scripts/devcontainer/custom-ca-certs/ folder - Utilised the devcontainer, for pre reqs and configuration. - You should open in a devcontainer or a Github workspaces. - By default it will run `make config` when the container is first setup From eac269b648f10ca4edfc1faa2da931ee1f753c28 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 14:23:04 +0100 Subject: [PATCH 13/53] lint fix --- internal/datastore/src/mi-repository.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/datastore/src/mi-repository.ts b/internal/datastore/src/mi-repository.ts index d03478588..f61bae665 100644 --- a/internal/datastore/src/mi-repository.ts +++ b/internal/datastore/src/mi-repository.ts @@ -42,7 +42,6 @@ export class MIRepository { } async getMI(miId: string, supplierId: string): Promise { - const result = await this.ddbClient.send( new GetCommand({ TableName: this.config.miTableName, From 4a8ae7f40ad9b8b6a2776bec5f9b7cf08d896865 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 15:06:05 +0100 Subject: [PATCH 14/53] fix type issues --- lambdas/api-handler/src/mappers/mi-mapper.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lambdas/api-handler/src/mappers/mi-mapper.ts b/lambdas/api-handler/src/mappers/mi-mapper.ts index 7b8ba422a..52b1afd42 100644 --- a/lambdas/api-handler/src/mappers/mi-mapper.ts +++ b/lambdas/api-handler/src/mappers/mi-mapper.ts @@ -37,17 +37,15 @@ export function mapToPostMIResponse(mi: MIBase): PostMIResponse { export function mapToGetMIResponse(mi: MIBase): GetMIResponse { return GetMIResponseResourceSchema.parse({ - data: { - id: mi.id, - type: "ManagementInformation", - attributes: { - lineItem: mi.lineItem, - timestamp: mi.timestamp, - quantity: mi.quantity, - specificationId: mi.specificationId, - groupId: mi.groupId, - stockRemaining: mi.stockRemaining, - }, + id: mi.id, + type: "ManagementInformation", + attributes: { + lineItem: mi.lineItem, + timestamp: mi.timestamp, + quantity: mi.quantity, + specificationId: mi.specificationId, + groupId: mi.groupId, + stockRemaining: mi.stockRemaining, }, }); } From 9aaeec8da58c3a720b4dc66af720b42f0a0533d1 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 15:14:02 +0100 Subject: [PATCH 15/53] add back in data property --- lambdas/api-handler/src/mappers/mi-mapper.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lambdas/api-handler/src/mappers/mi-mapper.ts b/lambdas/api-handler/src/mappers/mi-mapper.ts index 52b1afd42..7b8ba422a 100644 --- a/lambdas/api-handler/src/mappers/mi-mapper.ts +++ b/lambdas/api-handler/src/mappers/mi-mapper.ts @@ -37,15 +37,17 @@ export function mapToPostMIResponse(mi: MIBase): PostMIResponse { export function mapToGetMIResponse(mi: MIBase): GetMIResponse { return GetMIResponseResourceSchema.parse({ - id: mi.id, - type: "ManagementInformation", - attributes: { - lineItem: mi.lineItem, - timestamp: mi.timestamp, - quantity: mi.quantity, - specificationId: mi.specificationId, - groupId: mi.groupId, - stockRemaining: mi.stockRemaining, + data: { + id: mi.id, + type: "ManagementInformation", + attributes: { + lineItem: mi.lineItem, + timestamp: mi.timestamp, + quantity: mi.quantity, + specificationId: mi.specificationId, + groupId: mi.groupId, + stockRemaining: mi.stockRemaining, + }, }, }); } From 1c268967a6407b13aeb617ab633d5c9ce4a68670 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 16:31:23 +0100 Subject: [PATCH 16/53] fix typing --- lambdas/api-handler/src/contracts/mi.ts | 36 +++++++------------- lambdas/api-handler/src/mappers/mi-mapper.ts | 2 +- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/lambdas/api-handler/src/contracts/mi.ts b/lambdas/api-handler/src/contracts/mi.ts index f86c43ec1..3dce6003f 100644 --- a/lambdas/api-handler/src/contracts/mi.ts +++ b/lambdas/api-handler/src/contracts/mi.ts @@ -1,29 +1,6 @@ import z from "zod"; import { makeDocumentSchema } from "./json-api"; -export const GetMIResponseResourceSchema = z - .object({ - id: z.string(), - type: z.literal("ManagementInformation"), - attributes: z - .object({ - lineItem: z.string(), - timestamp: z.string(), - quantity: z.string(), - specificationId: z.string().optional(), - groupId: z.string().optional(), - stockRemaining: z.number().optional(), - }) - .strict(), - }) - .strict(); - -export const GetMIResponseSchema = makeDocumentSchema( - GetMIResponseResourceSchema, -); - -export type GetMIResponse = z.infer; - export const PostMIRequestResourceSchema = z .object({ type: z.literal("ManagementInformation"), @@ -60,3 +37,16 @@ export type PostMIResponse = z.infer; 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; diff --git a/lambdas/api-handler/src/mappers/mi-mapper.ts b/lambdas/api-handler/src/mappers/mi-mapper.ts index 7b8ba422a..40eca839e 100644 --- a/lambdas/api-handler/src/mappers/mi-mapper.ts +++ b/lambdas/api-handler/src/mappers/mi-mapper.ts @@ -38,7 +38,6 @@ export function mapToPostMIResponse(mi: MIBase): PostMIResponse { export function mapToGetMIResponse(mi: MIBase): GetMIResponse { return GetMIResponseResourceSchema.parse({ data: { - id: mi.id, type: "ManagementInformation", attributes: { lineItem: mi.lineItem, @@ -48,6 +47,7 @@ export function mapToGetMIResponse(mi: MIBase): GetMIResponse { groupId: mi.groupId, stockRemaining: mi.stockRemaining, }, + id: mi.id, }, }); } From d01b66365972937d9ec4a8bb00fa0644c761a9b1 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 17:13:38 +0100 Subject: [PATCH 17/53] fix mi typecheck --- lambdas/api-handler/src/contracts/mi.ts | 17 +++++++++++++++++ lambdas/api-handler/src/mappers/mi-mapper.ts | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lambdas/api-handler/src/contracts/mi.ts b/lambdas/api-handler/src/contracts/mi.ts index 3dce6003f..07ffb9de2 100644 --- a/lambdas/api-handler/src/contracts/mi.ts +++ b/lambdas/api-handler/src/contracts/mi.ts @@ -45,6 +45,23 @@ export const GetMIResponseResourceSchema = z }) .strict(); + // export const GetMIResponseResourceSchema = z + // .object({ + // id: z.string(), + // type: z.literal("ManagementInformation"), + // attributes: z + // .object({ + // lineItem: z.string(), + // timestamp: z.string(), + // quantity: z.number(), + // specificationId: z.string().optional(), + // groupId: z.string().optional(), + // stockRemaining: z.number().optional(), + // }) + // .strict(), + // }) + // .strict(); + export const GetMIResponseSchema = makeDocumentSchema( GetMIResponseResourceSchema, ); diff --git a/lambdas/api-handler/src/mappers/mi-mapper.ts b/lambdas/api-handler/src/mappers/mi-mapper.ts index 40eca839e..88400062d 100644 --- a/lambdas/api-handler/src/mappers/mi-mapper.ts +++ b/lambdas/api-handler/src/mappers/mi-mapper.ts @@ -1,7 +1,7 @@ import { MIBase } from "@internal/datastore/src"; import { GetMIResponse, - GetMIResponseResourceSchema, + GetMIResponseSchema, IncomingMI, PostMIRequest, PostMIResponse, @@ -36,8 +36,9 @@ export function mapToPostMIResponse(mi: MIBase): PostMIResponse { } export function mapToGetMIResponse(mi: MIBase): GetMIResponse { - return GetMIResponseResourceSchema.parse({ + return GetMIResponseSchema.parse({ data: { + id: mi.id, type: "ManagementInformation", attributes: { lineItem: mi.lineItem, @@ -47,7 +48,6 @@ export function mapToGetMIResponse(mi: MIBase): GetMIResponse { groupId: mi.groupId, stockRemaining: mi.stockRemaining, }, - id: mi.id, }, }); } From c3e3ef897a037e98c38f35e10b67db8bde2b23ec Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 7 Apr 2026 17:19:45 +0100 Subject: [PATCH 18/53] fix mi linting --- lambdas/api-handler/src/contracts/mi.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lambdas/api-handler/src/contracts/mi.ts b/lambdas/api-handler/src/contracts/mi.ts index 07ffb9de2..3dce6003f 100644 --- a/lambdas/api-handler/src/contracts/mi.ts +++ b/lambdas/api-handler/src/contracts/mi.ts @@ -45,23 +45,6 @@ export const GetMIResponseResourceSchema = z }) .strict(); - // export const GetMIResponseResourceSchema = z - // .object({ - // id: z.string(), - // type: z.literal("ManagementInformation"), - // attributes: z - // .object({ - // lineItem: z.string(), - // timestamp: z.string(), - // quantity: z.number(), - // specificationId: z.string().optional(), - // groupId: z.string().optional(), - // stockRemaining: z.number().optional(), - // }) - // .strict(), - // }) - // .strict(); - export const GetMIResponseSchema = makeDocumentSchema( GetMIResponseResourceSchema, ); From 41dbe1019e7d99fcb209973718181e2e237ac87b Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 09:04:22 +0000 Subject: [PATCH 19/53] add mi repository tests for exception --- internal/datastore/src/__test__/mi-repository.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/datastore/src/__test__/mi-repository.test.ts b/internal/datastore/src/__test__/mi-repository.test.ts index 05942b5d2..4f3a94184 100644 --- a/internal/datastore/src/__test__/mi-repository.test.ts +++ b/internal/datastore/src/__test__/mi-repository.test.ts @@ -66,6 +66,14 @@ 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 with id XXX not found for supplier supplier1", + ); + }); + it("creates MI with id and timestamps", async () => { jest.useFakeTimers(); // Month is zero-indexed in JS Date From 705e4e6efe52753cde9067bcae3537b2d84ae5d7 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 09:25:27 +0000 Subject: [PATCH 20/53] lint fix and improve test coverage --- .../src/__test__/mi-repository.test.ts | 5 ++- .../src/mappers/__tests__/mi-mapper.test.ts | 31 ++++++++++++++++++- .../services/__tests__/mi-operations.test.ts | 25 +++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/internal/datastore/src/__test__/mi-repository.test.ts b/internal/datastore/src/__test__/mi-repository.test.ts index 4f3a94184..e63a7ef5a 100644 --- a/internal/datastore/src/__test__/mi-repository.test.ts +++ b/internal/datastore/src/__test__/mi-repository.test.ts @@ -66,9 +66,8 @@ describe("MiRepository", () => { }); describe("getMi", () => { - it("throws an error when fetching MI information that does not exist", async() => { - await expect( - miRepository.getMI("XXX", "supplier1"), + it("throws an error when fetching MI information that does not exist", async () => { + await expect(miRepository.getMI("XXX", "supplier1") ).rejects.toThrow( "Management Information with id XXX not found for supplier supplier1", ); diff --git a/lambdas/api-handler/src/mappers/__tests__/mi-mapper.test.ts b/lambdas/api-handler/src/mappers/__tests__/mi-mapper.test.ts index 10791ba59..2ac728e38 100644 --- a/lambdas/api-handler/src/mappers/__tests__/mi-mapper.test.ts +++ b/lambdas/api-handler/src/mappers/__tests__/mi-mapper.test.ts @@ -1,6 +1,6 @@ import { MIBase } from "@internal/datastore/src"; import { IncomingMI, PostMIRequest } from "../../contracts/mi"; -import { mapToMI, mapToPostMIResponse } from "../mi-mapper"; +import { mapToGetMIResponse, mapToMI, mapToPostMIResponse } from "../mi-mapper"; describe("mi-mapper", () => { it("maps a PostMIRequest to an IncomingMI object", async () => { @@ -59,4 +59,33 @@ describe("mi-mapper", () => { }, }); }); + + it("maps an internal MIBase object to a GetMIResponse", async () => { + const mi: MIBase = { + id: "id1", + lineItem: "envelope-business-standard", + timestamp: "2023-11-17T14:27:51.413Z", + quantity: 22, + specificationId: "spec1", + groupId: "group1", + stockRemaining: 20_000, + }; + + const result = mapToGetMIResponse(mi); + + expect(result).toEqual({ + data: { + id: "id1", + type: "ManagementInformation", + attributes: { + lineItem: "envelope-business-standard", + timestamp: "2023-11-17T14:27:51.413Z", + quantity: 22, + specificationId: "spec1", + groupId: "group1", + stockRemaining: 20_000, + }, + }, + }); + }); }); diff --git a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts index 334871f04..f1a7a102a 100644 --- a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts +++ b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts @@ -36,4 +36,29 @@ describe("postMI function", () => { }, }); }); + + it("retrieves the MI from the repository", async () => { + const persistedMi = { id: "id1", ...incomingMi }; + + const mockRepo = { + getMI: jest.fn().mockResolvedValue(persistedMi), + }; + + const result = await getMI("id1", "supplier1" , mockRepo as any); + + expect(result).toEqual({ + data: { + id: "id1", + type: "ManagementInformation", + attributes: { + lineItem: "envelope-business-standard", + timestamp: "2023-11-17T14:27:51.413Z", + quantity: 22, + specificationId: "spec1", + groupId: "group1", + stockRemaining: 20_000, + }, + }, + }); + }); }); From 5eb29e44624cc36d04d78abedb49ef6c10ed16cb Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 10:11:30 +0000 Subject: [PATCH 21/53] add missing import getMI --- .../api-handler/src/services/__tests__/mi-operations.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts index f1a7a102a..32f4cf8cf 100644 --- a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts +++ b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts @@ -1,5 +1,5 @@ import { IncomingMI } from "../../contracts/mi"; -import { postMI } from "../mi-operations"; +import { getMI, postMI } from "../mi-operations"; describe("postMI function", () => { const incomingMi: IncomingMI = { @@ -44,7 +44,7 @@ describe("postMI function", () => { getMI: jest.fn().mockResolvedValue(persistedMi), }; - const result = await getMI("id1", "supplier1" , mockRepo as any); + const result = await getMI("id1", "supplier1", mockRepo as any); expect(result).toEqual({ data: { From 01f2e52c340d1db2e80c10575356600395634e91 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 10:34:06 +0000 Subject: [PATCH 22/53] more linting --- internal/datastore/src/__test__/mi-repository.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/datastore/src/__test__/mi-repository.test.ts b/internal/datastore/src/__test__/mi-repository.test.ts index e63a7ef5a..675155614 100644 --- a/internal/datastore/src/__test__/mi-repository.test.ts +++ b/internal/datastore/src/__test__/mi-repository.test.ts @@ -67,8 +67,7 @@ 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( + await expect(miRepository.getMI("XXX", "supplier1")).rejects.toThrow( "Management Information with id XXX not found for supplier supplier1", ); }); From 3d995c93c1f3ce752ab33177aa5dc96ab90d5ee8 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 11:16:18 +0000 Subject: [PATCH 23/53] implement createGetMIHandler --- lambdas/api-handler/src/contracts/errors.ts | 1 + lambdas/api-handler/src/handlers/get-mi.ts | 51 ++++++++------------- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/lambdas/api-handler/src/contracts/errors.ts b/lambdas/api-handler/src/contracts/errors.ts index ceb9ce51f..4e76c9c0c 100644 --- a/lambdas/api-handler/src/contracts/errors.ts +++ b/lambdas/api-handler/src/contracts/errors.ts @@ -32,6 +32,7 @@ export enum ApiErrorDetail { 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", diff --git a/lambdas/api-handler/src/handlers/get-mi.ts b/lambdas/api-handler/src/handlers/get-mi.ts index 72a235a73..f7c654a5b 100644 --- a/lambdas/api-handler/src/handlers/get-mi.ts +++ b/lambdas/api-handler/src/handlers/get-mi.ts @@ -2,17 +2,15 @@ import { APIGatewayProxyHandler } from "aws-lambda"; import { Unit } from "aws-embedded-metrics"; import pino from "pino"; import { MetricEntry, MetricStatus, buildEMFObject } from "@internal/helpers"; -import { postMI as postMIOperation } from "../services/mi-operations"; +import { getMI as getMIOperation } from "../services/mi-operations"; import { ApiErrorDetail } from "../contracts/errors"; import ValidationError from "../errors/validation-error"; import { processError } from "../mappers/error-mapper"; -import { assertNotEmpty, validateIso8601Timestamp } from "../utils/validation"; +import { assertNotEmpty } from "../utils/validation"; import { extractCommonIds } from "../utils/common-ids"; -import { PostMIRequest, PostMIRequestSchema } from "../contracts/mi"; -import { mapToMI } from "../mappers/mi-mapper"; import { Deps } from "../config/deps"; -export default function createGettMIHandler( +export default function createGetMIHandler( deps: Deps, ): APIGatewayProxyHandler { return async (event) => { @@ -32,34 +30,21 @@ export default function createGettMIHandler( const { supplierId } = commonIds.value; try { - const body = assertNotEmpty( - event.body, - new ValidationError(ApiErrorDetail.InvalidRequestMissingBody), + const miId = assertNotEmpty( + event.pathParameters?.id, + new ValidationError( + ApiErrorDetail.InvalidRequestMissingMiIdPathParameter, + ), ); - let postMIRequest: PostMIRequest; - - try { - postMIRequest = PostMIRequestSchema.parse(JSON.parse(body)); - } catch (error) { - emitErrorMetric(supplierId, deps.logger); - const typedError = - error instanceof Error - ? new ValidationError(ApiErrorDetail.InvalidRequestBody, { - cause: error, - }) - : error; - throw typedError; - } - validateIso8601Timestamp(postMIRequest.data.attributes.timestamp); - - const result = await postMIOperation( - mapToMI(postMIRequest, supplierId), + const result = await getMIOperation( + miId, + supplierId, deps.miRepo, ); deps.logger.info({ - description: "Posted management information", + description: "Retrieved management information", supplierId: commonIds.value.supplierId, correlationId: commonIds.value.correlationId, }); @@ -71,18 +56,18 @@ export default function createGettMIHandler( value: 1, unit: Unit.Count, }; - let emf = buildEMFObject("postMi", dimensions, metric); + let emf = buildEMFObject("getMi", dimensions, metric); deps.logger.info(emf); // metric displaying the type/number of lineItems posted per supplier - dimensions.lineItem = postMIRequest.data.attributes.lineItem; + dimensions.lineItem = result.data.attributes.lineItem; metric.key = "LineItem per supplier"; - metric.value = postMIRequest.data.attributes.quantity; - emf = buildEMFObject("postMi", dimensions, metric); + metric.value = result.data.attributes.quantity; + emf = buildEMFObject("getMi", dimensions, metric); deps.logger.info(emf); return { - statusCode: 201, + statusCode: 200, body: JSON.stringify(result, null, 2), }; } catch (error) { @@ -99,6 +84,6 @@ function emitErrorMetric(supplierId: string, logger: pino.Logger) { value: 1, unit: Unit.Count, }; - const emf = buildEMFObject("postMi", dimensions, metric); + const emf = buildEMFObject("getMi", dimensions, metric); logger.info(emf); } From 9844c13edba5f7cffd3b4ef084d02abe870ffb36 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 11:30:31 +0000 Subject: [PATCH 24/53] lint fix --- lambdas/api-handler/src/handlers/get-mi.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lambdas/api-handler/src/handlers/get-mi.ts b/lambdas/api-handler/src/handlers/get-mi.ts index f7c654a5b..3aa4b167e 100644 --- a/lambdas/api-handler/src/handlers/get-mi.ts +++ b/lambdas/api-handler/src/handlers/get-mi.ts @@ -10,8 +10,7 @@ import { assertNotEmpty } from "../utils/validation"; import { extractCommonIds } from "../utils/common-ids"; import { Deps } from "../config/deps"; -export default function createGetMIHandler( - deps: Deps, +export default function createGetMIHandler(deps: Deps ): APIGatewayProxyHandler { return async (event) => { const commonIds = extractCommonIds( @@ -37,11 +36,7 @@ export default function createGetMIHandler( ), ); - const result = await getMIOperation( - miId, - supplierId, - deps.miRepo, - ); + const result = await getMIOperation(miId, supplierId, deps.miRepo); deps.logger.info({ description: "Retrieved management information", From d6dfffb98d376ad663f5bc5ff1f13e90da91519e Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 11:37:07 +0000 Subject: [PATCH 25/53] more linting --- lambdas/api-handler/src/handlers/get-mi.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lambdas/api-handler/src/handlers/get-mi.ts b/lambdas/api-handler/src/handlers/get-mi.ts index 3aa4b167e..9c17703a2 100644 --- a/lambdas/api-handler/src/handlers/get-mi.ts +++ b/lambdas/api-handler/src/handlers/get-mi.ts @@ -10,8 +10,7 @@ import { assertNotEmpty } from "../utils/validation"; import { extractCommonIds } from "../utils/common-ids"; import { Deps } from "../config/deps"; -export default function createGetMIHandler(deps: Deps -): APIGatewayProxyHandler { +export default function createGetMIHandler(deps: Deps): APIGatewayProxyHandler { return async (event) => { const commonIds = extractCommonIds( event.headers, From 2978aad9fdd485738d51c5bf5d573888c6f2339d Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 13:44:30 +0000 Subject: [PATCH 26/53] add api handler tests for get-mi --- lambdas/api-handler/src/contracts/errors.ts | 1 + .../src/handlers/__tests__/get-mi.test.ts | 165 ++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts diff --git a/lambdas/api-handler/src/contracts/errors.ts b/lambdas/api-handler/src/contracts/errors.ts index 4e76c9c0c..8e3969ef1 100644 --- a/lambdas/api-handler/src/contracts/errors.ts +++ b/lambdas/api-handler/src/contracts/errors.ts @@ -29,6 +29,7 @@ export enum ApiErrorStatus { export enum ApiErrorDetail { NotFoundLetterId = "No resource found with that ID", + NotFoundMiId = "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", diff --git a/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts b/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts new file mode 100644 index 000000000..d053f218a --- /dev/null +++ b/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts @@ -0,0 +1,165 @@ +import { Context } from "aws-lambda"; +import { mockDeep } from "jest-mock-extended"; +import pino from "pino"; +import { MIRepository } from "@internal/datastore/src"; +import { gettMI as getMiOperation } from "../../services/mi-operations"; +import { makeApiGwEvent } from "./utils/test-utils"; +import { ApiErrorDetail } from "../../contracts/errors"; +import NotFoundError from "../../errors/not-found-error"; +import { Deps } from "../../config/deps"; +import { EnvVars } from "../../config/env"; +import createGetMIHandler from "../get-mi"; + +jest.mock("../../services/mi-operations"); + +describe("API Lambda handler", () => { + const mockedDeps: jest.Mocked = { + miRepo: {} as unknown as MIRepository, + logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger, + env: { + SUPPLIER_ID_HEADER: "nhsd-supplier-id", + APIM_CORRELATION_HEADER: "nhsd-correlation-id", + DOWNLOAD_URL_TTL_SECONDS: 1, + } as unknown as EnvVars, + } as Deps; + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + it("returns 200 OK and the letter status", async () => { + const mockedGetMiById = getMiOperation as jest.Mock; + mockedGetMiById.mockResolvedValue({ + data:{ + id: "id1", + type: "ManagementInformation", + attributes: { + lineItem: "envelope-business-standard", + timestamp: "2023-11-17T14:27:51.413Z", + quantity: 22, + specificationId: "spec1", + groupId: "group1", + stockRemaining: 20_000, + }, + }, + }); + + const event = makeApiGwEvent({ + path: "/mi/id1", + headers: { + "nhsd-supplier-id": "supplier1", + "nhsd-correlation-id": "correlationId", + "x-request-id": "requestId", + }, + pathParameters: { id: "id1" }, + }); + + const getMi = createGetMiHandler(mockedDeps); + const result = await getMi(event, mockDeep(), jest.fn()); + + const expected = { + data:{ + id: "id1", + type: "ManagementInformation", + attributes: { + lineItem: "envelope-business-standard", + timestamp: "2023-11-17T14:27:51.413Z", + quantity: 22, + specificationId: "spec1", + groupId: "group1", + stockRemaining: 20_000, + }, + }, + }; + + expect(result).toEqual({ + statusCode: 200, + body: JSON.stringify(expected), + }); + }); + + it("returns 404 Not Found when MI matching id is not found", async () => { + const mockedGetMiById = getMiOperation as jest.Mock; + + mockedGetMiById.mockImplementation(() => { + throw new NotFoundError(ApiErrorDetail.NotFoundMiId); + }); + + const event = makeApiGwEvent({ + path: "/mi/id1", + headers: { + "nhsd-supplier-id": "supplier1", + "nhsd-correlation-id": "correlationId", + "x-request-id": "requestId", + }, + pathParameters: { id: "id1" }, + }); + + const getMi = createGetMiHandler(mockedDeps); + const result = await getMi(event, mockDeep(), jest.fn()); + + expect(result).toEqual( + expect.objectContaining({ + statusCode: 404, + }), + ); + }); + + it("returns 500 when correlation id is missing from header", async () => { + const event = makeApiGwEvent({ + path: "/mi/id1", + headers: { "nhsd-supplier-id": "supplier1", "x-request-id": "requestId" }, + pathParameters: { id: "id1" }, + }); + + const getMi = createGetMiHandler(mockedDeps); + const result = await getMi(event, mockDeep(), jest.fn()); + + expect(result).toEqual( + expect.objectContaining({ + statusCode: 500, + }), + ); + }); + + it("returns 500 when supplier id is missing from header", async () => { + const event = makeApiGwEvent({ + path: "/mi/id1", + headers: { + "nhsd-correlation-id": "correlationId", + "x-request-id": "requestId", + }, + pathParameters: { id: "id1" }, + }); + + const getMi = createGetMiHandler(mockedDeps); + const result = await getMi(event, mockDeep(), jest.fn()); + + expect(result).toEqual( + expect.objectContaining({ + statusCode: 500, + }), + ); + }); + + it("returns 400 when letter id is missing from path", async () => { + const event = makeApiGwEvent({ + path: "/mi/id1", + headers: { + "nhsd-supplier-id": "supplier1", + "nhsd-correlation-id": "correlationId", + "x-request-id": "requestId", + }, + }); + + const getMi = createGetLetterHandler(mockedDeps); + const result = await getMi(event, mockDeep(), jest.fn()); + + expect(result).toEqual( + expect.objectContaining({ + statusCode: 400, + }), + ); + }); +}); From 41bb81ff0674d15ef66aa509a817b439d4948c66 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 13:53:46 +0000 Subject: [PATCH 27/53] fix type errors due to casing --- .../src/handlers/__tests__/get-mi.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts b/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts index d053f218a..5de08f115 100644 --- a/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts +++ b/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts @@ -2,7 +2,7 @@ import { Context } from "aws-lambda"; import { mockDeep } from "jest-mock-extended"; import pino from "pino"; import { MIRepository } from "@internal/datastore/src"; -import { gettMI as getMiOperation } from "../../services/mi-operations"; +import { getMI as getMiOperation } from "../../services/mi-operations"; import { makeApiGwEvent } from "./utils/test-utils"; import { ApiErrorDetail } from "../../contracts/errors"; import NotFoundError from "../../errors/not-found-error"; @@ -55,7 +55,7 @@ describe("API Lambda handler", () => { pathParameters: { id: "id1" }, }); - const getMi = createGetMiHandler(mockedDeps); + const getMi = createGetMIHandler(mockedDeps); const result = await getMi(event, mockDeep(), jest.fn()); const expected = { @@ -96,7 +96,7 @@ describe("API Lambda handler", () => { pathParameters: { id: "id1" }, }); - const getMi = createGetMiHandler(mockedDeps); + const getMi = createGetMIHandler(mockedDeps); const result = await getMi(event, mockDeep(), jest.fn()); expect(result).toEqual( @@ -113,7 +113,7 @@ describe("API Lambda handler", () => { pathParameters: { id: "id1" }, }); - const getMi = createGetMiHandler(mockedDeps); + const getMi = createGetMIHandler(mockedDeps); const result = await getMi(event, mockDeep(), jest.fn()); expect(result).toEqual( @@ -133,7 +133,7 @@ describe("API Lambda handler", () => { pathParameters: { id: "id1" }, }); - const getMi = createGetMiHandler(mockedDeps); + const getMi = createGetMIHandler(mockedDeps); const result = await getMi(event, mockDeep(), jest.fn()); expect(result).toEqual( @@ -153,7 +153,7 @@ describe("API Lambda handler", () => { }, }); - const getMi = createGetLetterHandler(mockedDeps); + const getMi = createGetMIHandler(mockedDeps); const result = await getMi(event, mockDeep(), jest.fn()); expect(result).toEqual( From 990ab713138088242644d742fd8e3d5d5ad2ac0d Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 14:52:42 +0000 Subject: [PATCH 28/53] amend json stringify format in test --- lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts b/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts index 5de08f115..7159d20b2 100644 --- a/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts +++ b/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts @@ -28,7 +28,7 @@ describe("API Lambda handler", () => { jest.resetModules(); }); - it("returns 200 OK and the letter status", async () => { + it("returns 200 OK and the MI information", async () => { const mockedGetMiById = getMiOperation as jest.Mock; mockedGetMiById.mockResolvedValue({ data:{ @@ -75,7 +75,7 @@ describe("API Lambda handler", () => { expect(result).toEqual({ statusCode: 200, - body: JSON.stringify(expected), + body: JSON.stringify(expected, null, 2), }); }); From 82e105d60a0016c0748b3fa61ef75fb0e43afa1a Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 15:05:00 +0000 Subject: [PATCH 29/53] more linting --- .../src/handlers/__tests__/get-mi.test.ts | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts b/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts index 7159d20b2..51c15d85c 100644 --- a/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts +++ b/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts @@ -31,18 +31,18 @@ describe("API Lambda handler", () => { it("returns 200 OK and the MI information", async () => { const mockedGetMiById = getMiOperation as jest.Mock; mockedGetMiById.mockResolvedValue({ - data:{ - id: "id1", - type: "ManagementInformation", - attributes: { - lineItem: "envelope-business-standard", - timestamp: "2023-11-17T14:27:51.413Z", - quantity: 22, - specificationId: "spec1", - groupId: "group1", - stockRemaining: 20_000, - }, + data: { + id: "id1", + type: "ManagementInformation", + attributes: { + lineItem: "envelope-business-standard", + timestamp: "2023-11-17T14:27:51.413Z", + quantity: 22, + specificationId: "spec1", + groupId: "group1", + stockRemaining: 20_000, }, + }, }); const event = makeApiGwEvent({ @@ -59,18 +59,18 @@ describe("API Lambda handler", () => { const result = await getMi(event, mockDeep(), jest.fn()); const expected = { - data:{ - id: "id1", - type: "ManagementInformation", - attributes: { - lineItem: "envelope-business-standard", - timestamp: "2023-11-17T14:27:51.413Z", - quantity: 22, - specificationId: "spec1", - groupId: "group1", - stockRemaining: 20_000, - }, + data: { + id: "id1", + type: "ManagementInformation", + attributes: { + lineItem: "envelope-business-standard", + timestamp: "2023-11-17T14:27:51.413Z", + quantity: 22, + specificationId: "spec1", + groupId: "group1", + stockRemaining: 20_000, }, + }, }; expect(result).toEqual({ From 6644f4eb40c2ee0b17391f83aaf8003d2e11e94e Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 15:22:59 +0000 Subject: [PATCH 30/53] fix error enum --- lambdas/api-handler/src/contracts/errors.ts | 3 +-- lambdas/api-handler/src/handlers/__tests__/get-letter.test.ts | 2 +- lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts | 2 +- .../api-handler/src/mappers/__tests__/error-mapper.test.ts | 2 +- lambdas/api-handler/src/services/letter-operations.ts | 4 ++-- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lambdas/api-handler/src/contracts/errors.ts b/lambdas/api-handler/src/contracts/errors.ts index 8e3969ef1..e9619b331 100644 --- a/lambdas/api-handler/src/contracts/errors.ts +++ b/lambdas/api-handler/src/contracts/errors.ts @@ -28,8 +28,7 @@ export enum ApiErrorStatus { } export enum ApiErrorDetail { - NotFoundLetterId = "No resource found with that ID", - NotFoundMiId = "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", diff --git a/lambdas/api-handler/src/handlers/__tests__/get-letter.test.ts b/lambdas/api-handler/src/handlers/__tests__/get-letter.test.ts index 1d85a2192..2449edc4e 100644 --- a/lambdas/api-handler/src/handlers/__tests__/get-letter.test.ts +++ b/lambdas/api-handler/src/handlers/__tests__/get-letter.test.ts @@ -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({ diff --git a/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts b/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts index 51c15d85c..8f4455578 100644 --- a/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts +++ b/lambdas/api-handler/src/handlers/__tests__/get-mi.test.ts @@ -83,7 +83,7 @@ describe("API Lambda handler", () => { const mockedGetMiById = getMiOperation as jest.Mock; mockedGetMiById.mockImplementation(() => { - throw new NotFoundError(ApiErrorDetail.NotFoundMiId); + throw new NotFoundError(ApiErrorDetail.NotFoundId); }); const event = makeApiGwEvent({ diff --git a/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts b/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts index 3a41f7d7a..83f2d6225 100644 --- a/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts +++ b/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts @@ -35,7 +35,7 @@ describe("processError", () => { }); it("should map NotFoundError to NotFound response", () => { - const err = new NotFoundError(ApiErrorDetail.NotFoundLetterId); + const err = new NotFoundError(ApiErrorDetail.NotFoundId); const res = processError(err, undefined, { info: jest.fn(), diff --git a/lambdas/api-handler/src/services/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts index dc2de921d..8b039278f 100644 --- a/lambdas/api-handler/src/services/letter-operations.ts +++ b/lambdas/api-handler/src/services/letter-operations.ts @@ -51,7 +51,7 @@ export const getLetterById = async ( letter = await letterRepo.getLetterById(supplierId, letterId); } catch (error) { if (isNotFoundError(error)) { - throw new NotFoundError(ApiErrorDetail.NotFoundLetterId); + throw new NotFoundError(ApiErrorDetail.NotFoundId); } throw error; } @@ -75,7 +75,7 @@ export const getLetterDataUrl = async ( ); } catch (error) { if (isNotFoundError(error)) { - throw new NotFoundError(ApiErrorDetail.NotFoundLetterId); + throw new NotFoundError(ApiErrorDetail.NotFoundId); } throw error; } From 80519fab13184449b4f57a774575a7122ed60c52 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 16:11:55 +0000 Subject: [PATCH 31/53] refactor metrics --- lambdas/api-handler/src/handlers/get-mi.ts | 26 +++++++--------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/lambdas/api-handler/src/handlers/get-mi.ts b/lambdas/api-handler/src/handlers/get-mi.ts index 9c17703a2..97dd00ad6 100644 --- a/lambdas/api-handler/src/handlers/get-mi.ts +++ b/lambdas/api-handler/src/handlers/get-mi.ts @@ -44,40 +44,30 @@ export default function createGetMIHandler(deps: Deps): APIGatewayProxyHandler { }); // metric with count 1 specifying the supplier - const dimensions: Record = { supplier: supplierId }; - const metric: MetricEntry = { - key: MetricStatus.Success, - value: 1, - unit: Unit.Count, - }; - let emf = buildEMFObject("getMi", dimensions, metric); - deps.logger.info(emf); + const dimensions: Record = {supplierId: supplierId}; + emitMetric("getMi", dimensions, deps.logger, MetricStatus.Success, 1); // metric displaying the type/number of lineItems posted per supplier dimensions.lineItem = result.data.attributes.lineItem; - metric.key = "LineItem per supplier"; - metric.value = result.data.attributes.quantity; - emf = buildEMFObject("getMi", dimensions, metric); - deps.logger.info(emf); + emitMetric("getMi", dimensions, deps.logger, "LineItem per supplier", result.data.attributes.quantity); return { statusCode: 200, body: JSON.stringify(result, null, 2), }; } catch (error) { - emitErrorMetric(supplierId, deps.logger); + emitMetric("getMi", {supplierId: supplierId}, deps.logger, MetricStatus.Failure, 1); return processError(error, commonIds.value.correlationId, deps.logger); } }; } -function emitErrorMetric(supplierId: string, logger: pino.Logger) { - const dimensions: Record = { supplier: supplierId }; +function emitMetric(source: string, dimensions: Record, logger: pino.Logger, key: string, value: number){ const metric: MetricEntry = { - key: MetricStatus.Failure, - value: 1, + key: key, + value: value, unit: Unit.Count, }; - const emf = buildEMFObject("getMi", dimensions, metric); + const emf = buildEMFObject(source, dimensions, metric); logger.info(emf); } From 697e4465022b6c66a5a5df43ddcd67fd3860ff62 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 16:18:07 +0000 Subject: [PATCH 32/53] linting --- lambdas/api-handler/src/handlers/get-mi.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lambdas/api-handler/src/handlers/get-mi.ts b/lambdas/api-handler/src/handlers/get-mi.ts index 97dd00ad6..fc75c854d 100644 --- a/lambdas/api-handler/src/handlers/get-mi.ts +++ b/lambdas/api-handler/src/handlers/get-mi.ts @@ -44,7 +44,7 @@ export default function createGetMIHandler(deps: Deps): APIGatewayProxyHandler { }); // metric with count 1 specifying the supplier - const dimensions: Record = {supplierId: supplierId}; + const dimensions: Record = { supplierId: supplierId }; emitMetric("getMi", dimensions, deps.logger, MetricStatus.Success, 1); // metric displaying the type/number of lineItems posted per supplier @@ -56,7 +56,7 @@ export default function createGetMIHandler(deps: Deps): APIGatewayProxyHandler { body: JSON.stringify(result, null, 2), }; } catch (error) { - emitMetric("getMi", {supplierId: supplierId}, deps.logger, MetricStatus.Failure, 1); + emitMetric("getMi", { supplierId: supplierId }, deps.logger, MetricStatus.Failure, 1); return processError(error, commonIds.value.correlationId, deps.logger); } }; @@ -64,8 +64,8 @@ export default function createGetMIHandler(deps: Deps): APIGatewayProxyHandler { function emitMetric(source: string, dimensions: Record, logger: pino.Logger, key: string, value: number){ const metric: MetricEntry = { - key: key, - value: value, + key, + value, unit: Unit.Count, }; const emf = buildEMFObject(source, dimensions, metric); From b0e8ac60bded118430f2ef9472ab2c9bd3395818 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 16:28:08 +0000 Subject: [PATCH 33/53] more linting --- lambdas/api-handler/src/handlers/get-mi.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lambdas/api-handler/src/handlers/get-mi.ts b/lambdas/api-handler/src/handlers/get-mi.ts index fc75c854d..a11cd74bc 100644 --- a/lambdas/api-handler/src/handlers/get-mi.ts +++ b/lambdas/api-handler/src/handlers/get-mi.ts @@ -44,19 +44,31 @@ export default function createGetMIHandler(deps: Deps): APIGatewayProxyHandler { }); // metric with count 1 specifying the supplier - const dimensions: Record = { supplierId: supplierId }; + const dimensions: Record = { supplierId }; emitMetric("getMi", dimensions, deps.logger, MetricStatus.Success, 1); // metric displaying the type/number of lineItems posted per supplier dimensions.lineItem = result.data.attributes.lineItem; - emitMetric("getMi", dimensions, deps.logger, "LineItem per supplier", result.data.attributes.quantity); + emitMetric( + "getMi", + dimensions, + deps.logger, + "LineItem per supplier", + result.data.attributes.quantity, + ); return { statusCode: 200, body: JSON.stringify(result, null, 2), }; } catch (error) { - emitMetric("getMi", { supplierId: supplierId }, deps.logger, MetricStatus.Failure, 1); + emitMetric( + "getMi", + { supplierId }, + deps.logger, + MetricStatus.Failure, + 1, + ); return processError(error, commonIds.value.correlationId, deps.logger); } }; From 8c3b890072f1aa82d767f5a403482ef5cafc8ff3 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Wed, 8 Apr 2026 16:36:33 +0000 Subject: [PATCH 34/53] yet more linting --- lambdas/api-handler/src/handlers/get-mi.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lambdas/api-handler/src/handlers/get-mi.ts b/lambdas/api-handler/src/handlers/get-mi.ts index a11cd74bc..5b0a17cc5 100644 --- a/lambdas/api-handler/src/handlers/get-mi.ts +++ b/lambdas/api-handler/src/handlers/get-mi.ts @@ -62,19 +62,19 @@ export default function createGetMIHandler(deps: Deps): APIGatewayProxyHandler { body: JSON.stringify(result, null, 2), }; } catch (error) { - emitMetric( - "getMi", - { supplierId }, - deps.logger, - MetricStatus.Failure, - 1, - ); + emitMetric("getMi", { supplierId }, deps.logger, MetricStatus.Failure, 1); return processError(error, commonIds.value.correlationId, deps.logger); } }; } -function emitMetric(source: string, dimensions: Record, logger: pino.Logger, key: string, value: number){ +function emitMetric( + source: string, + dimensions: Record, + logger: pino.Logger, + key: string, + value: number, +) { const metric: MetricEntry = { key, value, From 2516dc6fd369575fc36417ec54ac9a09ca7113d6 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 08:19:03 +0000 Subject: [PATCH 35/53] add oas documentation for getMI endpoint --- .../api/components/documentation/getMI.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 specification/api/components/documentation/getMI.md diff --git a/specification/api/components/documentation/getMI.md b/specification/api/components/documentation/getMI.md new file mode 100644 index 000000000..0ddc790e2 --- /dev/null +++ b/specification/api/components/documentation/getMI.md @@ -0,0 +1,17 @@ +## Overview + +Use this endpoint to retrieve management or operational metrics relating to letter processing and print fulfilment. + +When you submit a get management information request, the endpoint will respond with a 200 (Success) response code along with the created data including a unique id for the record or an unsuccessful (4xx/5xx) response. + +Rate limiting applies. On excess requests, you may receive **429 Too Many Requests** (example error code(s): `NOTIFY_QUOTA`). Back off and retry later. + +## Sandbox test scenarios + +You can test the following scenarios in our sandbox environment. + +|Scenario|Request|Response| +|--------|-------|--------| +|Success|Request for successful MI record retrieval| 200 (Success) with the retrieved management information in the response| +|Invalid Request|Invalid Request for MI record retrieval| 400 (Bad Request) with the error details in the body| +|Unknown specification|Request for MI record retrieval for unknown spec|404 (Not Found) with the error details in the body| From f0221e6c977210f8df9df32dee1bfe729d302a8d Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 08:23:23 +0000 Subject: [PATCH 36/53] markdown fixes --- specification/api/components/documentation/getMI.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specification/api/components/documentation/getMI.md b/specification/api/components/documentation/getMI.md index 0ddc790e2..386c850c4 100644 --- a/specification/api/components/documentation/getMI.md +++ b/specification/api/components/documentation/getMI.md @@ -1,4 +1,4 @@ -## Overview +# Overview Use this endpoint to retrieve management or operational metrics relating to letter processing and print fulfilment. @@ -12,6 +12,6 @@ You can test the following scenarios in our sandbox environment. |Scenario|Request|Response| |--------|-------|--------| -|Success|Request for successful MI record retrieval| 200 (Success) with the retrieved management information in the response| -|Invalid Request|Invalid Request for MI record retrieval| 400 (Bad Request) with the error details in the body| +|Success|Request for successful MI record retrieval|200 (Success) with the retrieved management information in the response| +|Invalid Request|Invalid Request for MI record retrieval|400 (Bad Request) with the error details in the body| |Unknown specification|Request for MI record retrieval for unknown spec|404 (Not Found) with the error details in the body| From 64419b1e217c0173f14c7cd772ea79a744f75058 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 08:48:12 +0000 Subject: [PATCH 37/53] remove problematic english check regex --- scripts/config/vale/styles/config/vocabularies/words/accept.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/config/vale/styles/config/vocabularies/words/accept.txt b/scripts/config/vale/styles/config/vocabularies/words/accept.txt index ead5f5a48..3ea2ec995 100644 --- a/scripts/config/vale/styles/config/vocabularies/words/accept.txt +++ b/scripts/config/vale/styles/config/vocabularies/words/accept.txt @@ -1,6 +1,5 @@ actioned API|api -[A-Z]+s Bitwarden bot Cognito From 56a21bb1fff69f0b2d71e87004d8de310f93076c Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 09:17:24 +0000 Subject: [PATCH 38/53] remove request body for getMI endpoint --- specification/api/components/endpoints/getMI.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/specification/api/components/endpoints/getMI.yml b/specification/api/components/endpoints/getMI.yml index b31d117aa..8cc5c0097 100644 --- a/specification/api/components/endpoints/getMI.yml +++ b/specification/api/components/endpoints/getMI.yml @@ -4,8 +4,6 @@ description: operationId: getMI tags: - mi -requestBody: - $ref: '../requests/getMIRequest.yml' responses: "200": $ref: "../responses/getMI200.yml" From 01b2c708f2e9d100859c54349313390cebea58f5 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 10:00:11 +0000 Subject: [PATCH 39/53] add get_mi lambda module in terraform --- .../terraform/components/api/README.md | 1 + .../components/api/module_lambda_get_mi.tf | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 infrastructure/terraform/components/api/module_lambda_get_mi.tf diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md index a0b2b3cdb..71e479165 100644 --- a/infrastructure/terraform/components/api/README.md +++ b/infrastructure/terraform/components/api/README.md @@ -64,6 +64,7 @@ No requirements. | [get\_letter](#module\_get\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/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 | | [get\_letters](#module\_get\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | +| [get\_mi](#module\_get\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [get\_status](#module\_get\_status) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-kms.zip | n/a | | [lambda\_alarms](#module\_lambda\_alarms) | ../../modules/alarms-lambda | n/a | diff --git a/infrastructure/terraform/components/api/module_lambda_get_mi.tf b/infrastructure/terraform/components/api/module_lambda_get_mi.tf new file mode 100644 index 000000000..b319b97d8 --- /dev/null +++ b/infrastructure/terraform/components/api/module_lambda_get_mi.tf @@ -0,0 +1,67 @@ +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", + ] + + resources = [ + aws_dynamodb_table.mi.arn, + ] + } +} From 1012d8e5b53534647a550f57bb31fe54dfd5527f Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 10:21:55 +0000 Subject: [PATCH 40/53] add getmi handler to index.ts --- lambdas/api-handler/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lambdas/api-handler/src/index.ts b/lambdas/api-handler/src/index.ts index 573be050d..72fcc7812 100644 --- a/lambdas/api-handler/src/index.ts +++ b/lambdas/api-handler/src/index.ts @@ -6,6 +6,7 @@ import createPatchLetterHandler from "./handlers/patch-letter"; import createPostLettersHandler from "./handlers/post-letters"; import createTransformAmendmentEventHandler from "./handlers/amendment-event-transformer"; import createPostMIHandler from "./handlers/post-mi"; +import createGetMIHandler from "./handlers/get-mi"; import createGetStatusHandler from "./handlers/get-status"; const container = createDependenciesContainer(); @@ -19,4 +20,5 @@ export const transformAmendmentEvent = export const postLetters = createPostLettersHandler(container); export const postMI = createPostMIHandler(container); +export const getMI = createGetMIHandler(container); export const getStatus = createGetStatusHandler(container); From 934b660ff9f7dc5697225282df1be04cb2d7ea53 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 10:50:35 +0000 Subject: [PATCH 41/53] wire up get_mi lambda in spec and tf --- .../terraform/components/api/locals.tf | 1 + .../components/api/resources/spec.tmpl.json | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/infrastructure/terraform/components/api/locals.tf b/infrastructure/terraform/components/api/locals.tf index 17e52b090..309cb2a2e 100644 --- a/infrastructure/terraform/components/api/locals.tf +++ b/infrastructure/terraform/components/api/locals.tf @@ -15,6 +15,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" diff --git a/infrastructure/terraform/components/api/resources/spec.tmpl.json b/infrastructure/terraform/components/api/resources/spec.tmpl.json index 5d3337807..51ad83dfb 100644 --- a/infrastructure/terraform/components/api/resources/spec.tmpl.json +++ b/infrastructure/terraform/components/api/resources/spec.tmpl.json @@ -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" + } + } + ] } } } From 5568baef26767118a136a438b1baba10dfe68c65 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 11:22:59 +0000 Subject: [PATCH 42/53] more wiring for get_mi lambda --- .../components/api/iam_role_api_gateway_execution_role.tf | 1 + infrastructure/terraform/components/api/locals_alarms.tf | 1 + infrastructure/terraform/components/api/module_lambda_get_mi.tf | 1 + 3 files changed, 3 insertions(+) diff --git a/infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf b/infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf index 8acebc8b8..9691a4dc5 100644 --- a/infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf +++ b/infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf @@ -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, diff --git a/infrastructure/terraform/components/api/locals_alarms.tf b/infrastructure/terraform/components/api/locals_alarms.tf index 873c6b38a..1123d0ff9 100644 --- a/infrastructure/terraform/components/api/locals_alarms.tf +++ b/infrastructure/terraform/components/api/locals_alarms.tf @@ -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 diff --git a/infrastructure/terraform/components/api/module_lambda_get_mi.tf b/infrastructure/terraform/components/api/module_lambda_get_mi.tf index b319b97d8..456bd588f 100644 --- a/infrastructure/terraform/components/api/module_lambda_get_mi.tf +++ b/infrastructure/terraform/components/api/module_lambda_get_mi.tf @@ -58,6 +58,7 @@ data "aws_iam_policy_document" "get_mi_lambda" { actions = [ "dynamodb:GetItem", + "dynamodb:Query" ] resources = [ From d57721b89a538af06778d356d88372071c504090 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 13:11:16 +0000 Subject: [PATCH 43/53] refactor getMi for better not found handling --- .../api-handler/src/services/mi-operations.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lambdas/api-handler/src/services/mi-operations.ts b/lambdas/api-handler/src/services/mi-operations.ts index e5d9e0e79..a2e3a18cd 100644 --- a/lambdas/api-handler/src/services/mi-operations.ts +++ b/lambdas/api-handler/src/services/mi-operations.ts @@ -1,6 +1,15 @@ import { MIRepository } from "@internal/datastore/src/mi-repository"; import { GetMIResponse, IncomingMI, PostMIResponse } from "../contracts/mi"; import { mapToGetMIResponse, mapToPostMIResponse } from "../mappers/mi-mapper"; +import { ApiErrorDetail } from "../contracts/errors"; +import NotFoundError from "../errors/not-found-error"; + +function isNotFoundError(error: any) { + return ( + error instanceof Error && + /^Management Information with id \w+ not found for supplier \w+$/.test(error.message) + ); +} export const postMI = async ( incomingMi: IncomingMI, @@ -14,5 +23,15 @@ export const getMI = async ( supplierId: string, miRepo: MIRepository, ): Promise => { - return mapToGetMIResponse(await miRepo.getMI(miId, supplierId)); + let mi; + + try { + mi = await miRepo.getMI(miId, supplierId); + } catch (error) { + if (isNotFoundError(error)) { + throw new NotFoundError(ApiErrorDetail.NotFoundId); + } + throw error; + } + return mapToGetMIResponse(mi); }; From 07440f45332b1d4a566acbda98cc35a25a22d2fd Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 13:41:16 +0000 Subject: [PATCH 44/53] refactor 404 error handling for getMi as per letter operations --- .../src/errors/mi-not-found-error.ts | 12 ++++++++++ internal/datastore/src/index.ts | 1 + .../services/__tests__/mi-operations.test.ts | 24 +++++++++++++++++++ .../src/services/letter-operations.ts | 4 ++-- .../api-handler/src/services/mi-operations.ts | 10 ++------ 5 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 internal/datastore/src/errors/mi-not-found-error.ts diff --git a/internal/datastore/src/errors/mi-not-found-error.ts b/internal/datastore/src/errors/mi-not-found-error.ts new file mode 100644 index 000000000..70ed40a0c --- /dev/null +++ b/internal/datastore/src/errors/mi-not-found-error.ts @@ -0,0 +1,12 @@ +/** + * 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"; + } +} diff --git a/internal/datastore/src/index.ts b/internal/datastore/src/index.ts index 9b656d9ee..4368823cc 100644 --- a/internal/datastore/src/index.ts +++ b/internal/datastore/src/index.ts @@ -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"; diff --git a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts index 32f4cf8cf..af82f306e 100644 --- a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts +++ b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts @@ -1,5 +1,7 @@ import { IncomingMI } from "../../contracts/mi"; import { getMI, postMI } from "../mi-operations"; +import MiNotFoundError from "@internal/datastore/src/errors/mi-not-found-error"; + describe("postMI function", () => { const incomingMi: IncomingMI = { @@ -61,4 +63,26 @@ describe("postMI function", () => { }, }); }); + + it("should throw notFoundError when letter does not exist", async () => { + const mockRepo = { + getMI: jest + .fn() + .mockRejectedValue(new MiNotFoundError("miId1", "supplier1")), + }; + + await expect( + getMI("miId1", "supplier1", mockRepo as any), + ).rejects.toThrow("No resource found with that ID"); + }); + + it("should throw unexpected error", async () => { + const mockRepo = { + getMI: jest.fn().mockRejectedValue(new Error("unexpected error")), + }; + + await expect( + getMI("miId1", "supplier1", mockRepo as any), + ).rejects.toThrow("unexpected error"); + }); }); diff --git a/lambdas/api-handler/src/services/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts index b96b3f8db..7ed0c4058 100644 --- a/lambdas/api-handler/src/services/letter-operations.ts +++ b/lambdas/api-handler/src/services/letter-operations.ts @@ -74,7 +74,7 @@ export const getLetterById = async ( letter = await letterRepo.getLetterById(supplierId, letterId); } catch (error) { if (error instanceof LetterNotFoundError) { - throw new NotFoundError(ApiErrorDetail.NotFoundLetterId); + throw new NotFoundError(ApiErrorDetail.NotFoundId); } throw error; } @@ -98,7 +98,7 @@ export const getLetterDataUrl = async ( ); } catch (error) { if (error instanceof LetterNotFoundError) { - throw new NotFoundError(ApiErrorDetail.NotFoundLetterId); + throw new NotFoundError(ApiErrorDetail.NotFoundId); } throw error; } diff --git a/lambdas/api-handler/src/services/mi-operations.ts b/lambdas/api-handler/src/services/mi-operations.ts index a2e3a18cd..6ad8ed8a7 100644 --- a/lambdas/api-handler/src/services/mi-operations.ts +++ b/lambdas/api-handler/src/services/mi-operations.ts @@ -3,13 +3,7 @@ import { GetMIResponse, IncomingMI, PostMIResponse } from "../contracts/mi"; import { mapToGetMIResponse, mapToPostMIResponse } from "../mappers/mi-mapper"; import { ApiErrorDetail } from "../contracts/errors"; import NotFoundError from "../errors/not-found-error"; - -function isNotFoundError(error: any) { - return ( - error instanceof Error && - /^Management Information with id \w+ not found for supplier \w+$/.test(error.message) - ); -} +import MiNotFoundError from "@internal/datastore/src/errors/letter-not-found-error"; export const postMI = async ( incomingMi: IncomingMI, @@ -28,7 +22,7 @@ export const getMI = async ( try { mi = await miRepo.getMI(miId, supplierId); } catch (error) { - if (isNotFoundError(error)) { + if (error instanceof MiNotFoundError) { throw new NotFoundError(ApiErrorDetail.NotFoundId); } throw error; From 162fa8603dd00ab17b35e08fb353036ebcf53ca3 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 14:15:18 +0000 Subject: [PATCH 45/53] fix notfound error handling for getMI --- internal/datastore/src/mi-repository.ts | 5 ++--- .../src/services/__tests__/mi-operations.test.ts | 13 ++++++++++++- lambdas/api-handler/src/services/mi-operations.ts | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/internal/datastore/src/mi-repository.ts b/internal/datastore/src/mi-repository.ts index f61bae665..5eec79f51 100644 --- a/internal/datastore/src/mi-repository.ts +++ b/internal/datastore/src/mi-repository.ts @@ -6,6 +6,7 @@ import { 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; @@ -53,9 +54,7 @@ export class MIRepository { ); if (!result.Item) { - throw new Error( - `Management Information with id ${miId} not found for supplier ${supplierId}`, - ); + throw new MiNotFoundError(supplierId, miId); } return MISchema.parse(result.Item); diff --git a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts index af82f306e..1769aebbf 100644 --- a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts +++ b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts @@ -38,8 +38,19 @@ describe("postMI function", () => { }, }); }); +}); +describe("getMI function", () => { it("retrieves the MI from the repository", async () => { + const incomingMi: IncomingMI = { + lineItem: "envelope-business-standard", + timestamp: "2023-11-17T14:27:51.413Z", + quantity: 22, + specificationId: "spec1", + groupId: "group1", + stockRemaining: 20_000, + supplierId: "supplier1", + }; const persistedMi = { id: "id1", ...incomingMi }; const mockRepo = { @@ -68,7 +79,7 @@ describe("postMI function", () => { const mockRepo = { getMI: jest .fn() - .mockRejectedValue(new MiNotFoundError("miId1", "supplier1")), + .mockRejectedValue(new MiNotFoundError("supplier1", "miId1")), }; await expect( diff --git a/lambdas/api-handler/src/services/mi-operations.ts b/lambdas/api-handler/src/services/mi-operations.ts index 6ad8ed8a7..5a3c803db 100644 --- a/lambdas/api-handler/src/services/mi-operations.ts +++ b/lambdas/api-handler/src/services/mi-operations.ts @@ -3,7 +3,7 @@ import { GetMIResponse, IncomingMI, PostMIResponse } from "../contracts/mi"; import { mapToGetMIResponse, mapToPostMIResponse } from "../mappers/mi-mapper"; import { ApiErrorDetail } from "../contracts/errors"; import NotFoundError from "../errors/not-found-error"; -import MiNotFoundError from "@internal/datastore/src/errors/letter-not-found-error"; +import MiNotFoundError from "@internal/datastore/src/errors/mi-not-found-error"; export const postMI = async ( incomingMi: IncomingMI, From 7325fa5a74273b9b9348168fb97b03e78148e160 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 14:23:31 +0000 Subject: [PATCH 46/53] fix test --- internal/datastore/src/__test__/mi-repository.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/datastore/src/__test__/mi-repository.test.ts b/internal/datastore/src/__test__/mi-repository.test.ts index 675155614..e44bee98a 100644 --- a/internal/datastore/src/__test__/mi-repository.test.ts +++ b/internal/datastore/src/__test__/mi-repository.test.ts @@ -68,7 +68,7 @@ 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 with id XXX not found for supplier supplier1", + "Management information not found: supplierId=supplier1, miId=XXX", ); }); From 20a7afa96fc6e480e73d22e84fa7faabbdd11fe3 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 14:34:09 +0000 Subject: [PATCH 47/53] lint fix --- .../services/__tests__/mi-operations.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts index 1769aebbf..20ef858e6 100644 --- a/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts +++ b/lambdas/api-handler/src/services/__tests__/mi-operations.test.ts @@ -1,7 +1,6 @@ +import MiNotFoundError from "@internal/datastore/src/errors/mi-not-found-error"; import { IncomingMI } from "../../contracts/mi"; import { getMI, postMI } from "../mi-operations"; -import MiNotFoundError from "@internal/datastore/src/errors/mi-not-found-error"; - describe("postMI function", () => { const incomingMi: IncomingMI = { @@ -41,7 +40,6 @@ describe("postMI function", () => { }); describe("getMI function", () => { - it("retrieves the MI from the repository", async () => { const incomingMi: IncomingMI = { lineItem: "envelope-business-standard", timestamp: "2023-11-17T14:27:51.413Z", @@ -51,6 +49,7 @@ describe("getMI function", () => { stockRemaining: 20_000, supplierId: "supplier1", }; + it("retrieves the MI from the repository", async () => { const persistedMi = { id: "id1", ...incomingMi }; const mockRepo = { @@ -82,9 +81,9 @@ describe("getMI function", () => { .mockRejectedValue(new MiNotFoundError("supplier1", "miId1")), }; - await expect( - getMI("miId1", "supplier1", mockRepo as any), - ).rejects.toThrow("No resource found with that ID"); + await expect(getMI("miId1", "supplier1", mockRepo as any)).rejects.toThrow( + "No resource found with that ID", + ); }); it("should throw unexpected error", async () => { @@ -92,8 +91,8 @@ describe("getMI function", () => { getMI: jest.fn().mockRejectedValue(new Error("unexpected error")), }; - await expect( - getMI("miId1", "supplier1", mockRepo as any), - ).rejects.toThrow("unexpected error"); + await expect(getMI("miId1", "supplier1", mockRepo as any)).rejects.toThrow( + "unexpected error", + ); }); }); From 6b36c2a5b67e3e1cc83f54aba73e93b4ad5a4a34 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 14:41:50 +0000 Subject: [PATCH 48/53] more lint fixes --- internal/datastore/src/errors/mi-not-found-error.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/datastore/src/errors/mi-not-found-error.ts b/internal/datastore/src/errors/mi-not-found-error.ts index 70ed40a0c..f41386267 100644 --- a/internal/datastore/src/errors/mi-not-found-error.ts +++ b/internal/datastore/src/errors/mi-not-found-error.ts @@ -6,7 +6,9 @@ export default class MiNotFoundError extends Error { public readonly supplierId: string, public readonly miId: string, ) { - super(`Management information not found: supplierId=${supplierId}, miId=${miId}`); + super( + `Management information not found: supplierId=${supplierId}, miId=${miId}`, + ); this.name = "MiNotFoundError"; } } From 3a701c231347334bd75983d5442c8d8a94d7cbb2 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 15:18:22 +0000 Subject: [PATCH 49/53] yet more linting --- lambdas/api-handler/src/services/mi-operations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambdas/api-handler/src/services/mi-operations.ts b/lambdas/api-handler/src/services/mi-operations.ts index 5a3c803db..6f3e53816 100644 --- a/lambdas/api-handler/src/services/mi-operations.ts +++ b/lambdas/api-handler/src/services/mi-operations.ts @@ -1,9 +1,9 @@ +import MiNotFoundError from "@internal/datastore/src/errors/mi-not-found-error"; import { MIRepository } from "@internal/datastore/src/mi-repository"; import { GetMIResponse, IncomingMI, PostMIResponse } from "../contracts/mi"; import { mapToGetMIResponse, mapToPostMIResponse } from "../mappers/mi-mapper"; import { ApiErrorDetail } from "../contracts/errors"; import NotFoundError from "../errors/not-found-error"; -import MiNotFoundError from "@internal/datastore/src/errors/mi-not-found-error"; export const postMI = async ( incomingMi: IncomingMI, From 72d06a4c47cd8a01acfc3cecd92feb0287d922e0 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 15:42:19 +0000 Subject: [PATCH 50/53] rename NOTIFY_LETTER_NOT_FOUND to NOTIFY_NOT_FOUND --- lambdas/api-handler/src/contracts/errors.ts | 2 +- lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts | 2 +- sandbox/api/openapi.yaml | 2 +- tests/helpers/common-types.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lambdas/api-handler/src/contracts/errors.ts b/lambdas/api-handler/src/contracts/errors.ts index e9619b331..ceca38814 100644 --- a/lambdas/api-handler/src/contracts/errors.ts +++ b/lambdas/api-handler/src/contracts/errors.ts @@ -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 { diff --git a/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts b/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts index 83f2d6225..862a9566d 100644 --- a/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts +++ b/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts @@ -46,7 +46,7 @@ describe("processError", () => { expect(JSON.parse(res.body)).toEqual({ errors: [ { - code: "NOTIFY_LETTER_NOT_FOUND", + code: "NOTIFY_NOT_FOUND", detail: "No resource found with that ID", id: expect.any(String), links: { diff --git a/sandbox/api/openapi.yaml b/sandbox/api/openapi.yaml index 0f80ee80b..67d963a45 100644 --- a/sandbox/api/openapi.yaml +++ b/sandbox/api/openapi.yaml @@ -1455,7 +1455,7 @@ components: enum: - NOTIFY_INTERNAL_SERVER_ERROR - NOTIFY_INVALID_REQUEST - - NOTIFY_LETTER_NOT_FOUND + - NOTIFY_NOT_FOUND type: string links: $ref: "#/components/schemas/listLetters_400_response_errors_inner_links" diff --git a/tests/helpers/common-types.ts b/tests/helpers/common-types.ts index c730159f6..2b3f23a8d 100644 --- a/tests/helpers/common-types.ts +++ b/tests/helpers/common-types.ts @@ -34,7 +34,7 @@ export function error404ResponseBody(): ErrorMessageBody { errors: [ { id: "12345", - code: "NOTIFY_LETTER_NOT_FOUND", + code: "NOTIFY_NOT_FOUND", links: { about: "https://digital.nhs.uk/developer/api-catalogue/nhs-notify-supplier", From d883564bf5b842b7cc5e72f40f8a7aeb11dccf02 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Thu, 9 Apr 2026 16:14:46 +0000 Subject: [PATCH 51/53] missed rename --- specification/api/components/schemas/errorItem.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/api/components/schemas/errorItem.yml b/specification/api/components/schemas/errorItem.yml index 05b2ae4d4..1a1e9251e 100644 --- a/specification/api/components/schemas/errorItem.yml +++ b/specification/api/components/schemas/errorItem.yml @@ -7,7 +7,7 @@ properties: enum: - NOTIFY_INTERNAL_SERVER_ERROR - NOTIFY_INVALID_REQUEST - - NOTIFY_LETTER_NOT_FOUND + - NOTIFY_NOT_FOUND links: type: object properties: From 9d329c666370bf93440c5848eb3c1b2061b975e0 Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Mon, 13 Apr 2026 13:02:19 +0100 Subject: [PATCH 52/53] add get management information to postman collection --- ...upplierAPI.sandbox.postman_collection.json | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/postman/NHSNotifySupplierAPI.sandbox.postman_collection.json b/postman/NHSNotifySupplierAPI.sandbox.postman_collection.json index a73203c45..b4cf5d77c 100644 --- a/postman/NHSNotifySupplierAPI.sandbox.postman_collection.json +++ b/postman/NHSNotifySupplierAPI.sandbox.postman_collection.json @@ -2044,6 +2044,157 @@ } }, "response": [] + }, + { + "item": [ + { + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.equal(200);", + "});", + "", + "const jsonData = pm.response.json();", + "pm.test(\"Response Body contains data field and status\", () => {", + " pm.expect(jsonData).to.have.property(\"data\");", + " pm.expect(jsonData.data.attributes).to.have.property(\"id\");", + "});" + ], + "packages": {}, + "requests": {}, + "type": "text/javascript" + } + } + ], + "name": "200 Get Management Information", + "request": { + "auth": { + "type": "noauth" + }, + "description": "Retrieve managaement information.", + "header": [ + { + "disabled": true, + "key": "Accept", + "type": "default", + "value": "application/vnd.api+json" + }, + { + "disabled": false, + "key": "Content-Type", + "type": "default", + "value": "application/vnd.api+json" + }, + { + "disabled": false, + "key": "X-Correlation-Id", + "type": "default", + "value": "{{correlation_id}}" + }, + { + "disabled": false, + "key": "X-Request-ID", + "type": "default", + "value": "{{x_request_id}}" + } + ], + "method": "GET", + "url": { + "host": [ + "{{baseUrl}}" + ], + "path": [ + "mi", + ":id" + ], + "protocol": "", + "query": [], + "raw": "{{baseUrl}}/mi/:id", + "variable": [ + { + "description": "(Required) Unique identifier of this resource", + "key": "id", + "value": "2AL5eYSWGzCHlGmzNxuqVusPxDg" + } + ] + } + } + }, + { + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 404\", () => {", + " pm.expect(pm.response.code).to.equal(404);", + "});", + "", + "const jsonData = pm.response.json();", + "pm.test(\"Response Body contains errors with correct code\", () => {", + " pm.expect(jsonData).to.have.property(\"errors\");", + " pm.expect(jsonData.errors[0].code).to.equal(\"NOTIFY_RESOURCE_NOT_FOUND\");", + "});" + ], + "packages": {}, + "requests": {}, + "type": "text/javascript" + } + } + ], + "name": "404 Not Found", + "request": { + "auth": { + "type": "noauth" + }, + "description": "Retrieve management information.", + "header": [ + { + "disabled": false, + "key": "Content-Type", + "type": "default", + "value": "application/vnd.api+json" + }, + { + "disabled": false, + "key": "X-Correlation-Id", + "type": "default", + "value": "{{correlation_id}}" + }, + { + "disabled": false, + "key": "X-Request-ID", + "type": "default", + "value": "{{x_request_id}}" + } + ], + "method": "GET", + "url": { + "host": [ + "{{baseUrl}}" + ], + "path": [ + "mi", + ":id" + ], + "protocol": "", + "query": [], + "raw": "{{baseUrl}}/mi/:id", + "variable": [ + { + "description": "(Required) Unique identifier of this resource", + "key": "id", + "value": "24L5eYSWGzCHlGmzNxuqVusP" + } + ] + } + } + } + ], + "name": "Get Management Information" } ], "name": "MI" From 5650d70aff2b5dc96a5b3b9a7940e82af8d2eead Mon Sep 17 00:00:00 2001 From: "ross.faulds2" Date: Tue, 14 Apr 2026 15:22:39 +0100 Subject: [PATCH 53/53] remove getMI from new endpoint --- specification/api/notify-supplier-phase1.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/api/notify-supplier-phase1.yml b/specification/api/notify-supplier-phase1.yml index 519b62f70..f5fdc8704 100644 --- a/specification/api/notify-supplier-phase1.yml +++ b/specification/api/notify-supplier-phase1.yml @@ -43,7 +43,7 @@ paths: - $ref: 'components/parameters/correlationId.yml' post: $ref: 'components/endpoints/createMI.yml' - '/mi/getMI/{id}': + '/mi/{id}': parameters: - $ref: 'components/parameters/authorization/authorization.yml' - $ref: 'components/parameters/requestId.yml'