Skip to content

Commit 9b57d8e

Browse files
committed
Add new endpoint to get mi information
1 parent 22f7fb5 commit 9b57d8e

10 files changed

Lines changed: 281 additions & 5 deletions

File tree

internal/datastore/src/__test__/mi-repository.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,45 @@ describe("MiRepository", () => {
6464
);
6565
});
6666
});
67+
68+
describe("getMi", () => {
69+
it("creates MI with id and timestamps", async () => {
70+
jest.useFakeTimers();
71+
// Month is zero-indexed in JS Date
72+
jest.setSystemTime(new Date(2020, 1, 1));
73+
const mi = {
74+
specificationId: "spec1",
75+
supplierId: "supplier1",
76+
groupId: "group1",
77+
lineItem: "item1",
78+
quantity: 12,
79+
timestamp: new Date().toISOString(),
80+
stockRemaining: 0,
81+
};
82+
83+
const persistedMi = await miRepository.putMI(mi);
84+
85+
expect(persistedMi).toEqual(
86+
expect.objectContaining({
87+
id: expect.any(String),
88+
createdAt: "2020-02-01T00:00:00.000Z",
89+
updatedAt: "2020-02-01T00:00:00.000Z",
90+
ttl: 1_580_518_800, // 2020-02-01T00:01:00.000Z, seconds since epoch
91+
...mi,
92+
}),
93+
);
94+
95+
const fetchedMi = await miRepository.getMI(persistedMi.id, persistedMi.supplierId);
96+
expect(fetchedMi).toEqual(
97+
expect.objectContaining({
98+
id: expect.any(String),
99+
createdAt: "2020-02-01T00:00:00.000Z",
100+
updatedAt: "2020-02-01T00:00:00.000Z",
101+
ttl: 1_580_518_800, // 2020-02-01T00:01:00.000Z, seconds since epoch
102+
...mi,
103+
})
104+
)
105+
});
106+
});
107+
67108
});

internal/datastore/src/mi-repository.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
1+
import { DynamoDBDocumentClient, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";
22
import { Logger } from "pino";
33
import { randomUUID } from "node:crypto";
44
import { MI, MISchema } from "./types";
@@ -36,4 +36,30 @@ export class MIRepository {
3636

3737
return MISchema.parse(miDb);
3838
}
39+
40+
// TODO should the miId and supplierId be encapsulated in a getMIRequest
41+
async getMI(
42+
miId: string,
43+
supplierId: string,
44+
): Promise<MI> {
45+
46+
const result = await this.ddbClient.send(
47+
new GetCommand({
48+
TableName: this.config.miTableName,
49+
Key: {
50+
id: miId,
51+
supplierId,
52+
},
53+
}),
54+
);
55+
56+
if (!result.Item) {
57+
throw new Error(
58+
`Management Information with id ${miId} not found for supplier ${supplierId}`,
59+
);
60+
}
61+
62+
return MISchema.parse(result.Item);
63+
}
64+
3965
}

lambdas/api-handler/src/contracts/mi.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
11
import z from "zod";
22
import { makeDocumentSchema } from "./json-api";
33

4+
// TODO this is exactly the same as the PostMIRequestResourceSchema
5+
// check if I should reuse this
6+
export const GetMIResponseResourceSchema = z
7+
.object({
8+
id: z.string(),
9+
type: z.literal("ManagementInformation"),
10+
attributes: z
11+
.object({
12+
lineItem: z.string(),
13+
timestamp: z.string(),
14+
quantity: z.string(),
15+
specificationId: z.string().optional(),
16+
groupId: z.string().optional(),
17+
stockRemaining: z.number().optional()
18+
})
19+
.strict(),
20+
})
21+
.strict();
22+
23+
export const GetMIResponseSchema = makeDocumentSchema(
24+
GetMIResponseResourceSchema,
25+
);
26+
27+
export type GetMIResponse = z.infer<typeof PostMIResponseSchema>;
28+
29+
430
export const PostMIRequestResourceSchema = z
531
.object({
632
type: z.literal("ManagementInformation"),
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { APIGatewayProxyHandler } from "aws-lambda";
2+
import { Unit } from "aws-embedded-metrics";
3+
import pino from "pino";
4+
import { MetricEntry, MetricStatus, buildEMFObject } from "@internal/helpers";
5+
import postMIOperation from "../services/mi-operations";
6+
import { ApiErrorDetail } from "../contracts/errors";
7+
import ValidationError from "../errors/validation-error";
8+
import { processError } from "../mappers/error-mapper";
9+
import { assertNotEmpty, validateIso8601Timestamp } from "../utils/validation";
10+
import { extractCommonIds } from "../utils/common-ids";
11+
import { PostMIRequest, PostMIRequestSchema } from "../contracts/mi";
12+
import { mapToMI } from "../mappers/mi-mapper";
13+
import { Deps } from "../config/deps";
14+
15+
export default function createPostMIHandler(
16+
deps: Deps,
17+
): APIGatewayProxyHandler {
18+
return async (event) => {
19+
const commonIds = extractCommonIds(
20+
event.headers,
21+
event.requestContext,
22+
deps,
23+
);
24+
25+
if (!commonIds.ok) {
26+
return processError(
27+
commonIds.error,
28+
commonIds.correlationId,
29+
deps.logger,
30+
);
31+
}
32+
33+
const { supplierId } = commonIds.value;
34+
try {
35+
const body = assertNotEmpty(
36+
event.body,
37+
new ValidationError(ApiErrorDetail.InvalidRequestMissingBody),
38+
);
39+
40+
let postMIRequest: PostMIRequest;
41+
42+
try {
43+
postMIRequest = PostMIRequestSchema.parse(JSON.parse(body));
44+
} catch (error) {
45+
emitErrorMetric(supplierId, deps.logger);
46+
const typedError =
47+
error instanceof Error
48+
? new ValidationError(ApiErrorDetail.InvalidRequestBody, {
49+
cause: error,
50+
})
51+
: error;
52+
throw typedError;
53+
}
54+
validateIso8601Timestamp(postMIRequest.data.attributes.timestamp);
55+
56+
const result = await postMIOperation(
57+
mapToMI(postMIRequest, supplierId),
58+
deps.miRepo,
59+
);
60+
61+
deps.logger.info({
62+
description: "Posted management information",
63+
supplierId: commonIds.value.supplierId,
64+
correlationId: commonIds.value.correlationId,
65+
});
66+
67+
// metric with count 1 specifying the supplier
68+
const dimensions: Record<string, string> = { supplier: supplierId };
69+
const metric: MetricEntry = {
70+
key: MetricStatus.Success,
71+
value: 1,
72+
unit: Unit.Count,
73+
};
74+
let emf = buildEMFObject("postMi", dimensions, metric);
75+
deps.logger.info(emf);
76+
77+
// metric displaying the type/number of lineItems posted per supplier
78+
dimensions.lineItem = postMIRequest.data.attributes.lineItem;
79+
metric.key = "LineItem per supplier";
80+
metric.value = postMIRequest.data.attributes.quantity;
81+
emf = buildEMFObject("postMi", dimensions, metric);
82+
deps.logger.info(emf);
83+
84+
return {
85+
statusCode: 201,
86+
body: JSON.stringify(result, null, 2),
87+
};
88+
} catch (error) {
89+
emitErrorMetric(supplierId, deps.logger);
90+
return processError(error, commonIds.value.correlationId, deps.logger);
91+
}
92+
};
93+
}
94+
95+
function emitErrorMetric(supplierId: string, logger: pino.Logger) {
96+
const dimensions: Record<string, string> = { supplier: supplierId };
97+
const metric: MetricEntry = {
98+
key: MetricStatus.Failure,
99+
value: 1,
100+
unit: Unit.Count,
101+
};
102+
const emf = buildEMFObject("postMi", dimensions, metric);
103+
logger.info(emf);
104+
}

lambdas/api-handler/src/mappers/mi-mapper.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
PostMIRequest,
55
PostMIResponse,
66
PostMIResponseSchema,
7+
GetMIResponse,
8+
GetMIResponseResourceSchema
79
} from "../contracts/mi";
810

911
export function mapToMI(
@@ -32,3 +34,20 @@ export function mapToPostMIResponse(mi: MIBase): PostMIResponse {
3234
},
3335
});
3436
}
37+
38+
export function mapToGetMIResponse(mi: MIBase): GetMIResponse {
39+
return GetMIResponseResourceSchema.parse({
40+
data: {
41+
id: mi.id,
42+
type: "ManagementInformation",
43+
attributes: {
44+
lineItem: mi.lineItem,
45+
timestamp: mi.timestamp,
46+
quantity: mi.quantity,
47+
specificationId: mi.specificationId,
48+
groupId: mi.groupId,
49+
stockRemaining: mi.stockRemaining,
50+
},
51+
},
52+
})
53+
}
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { MIRepository } from "@internal/datastore/src/mi-repository";
2-
import { IncomingMI, PostMIResponse } from "../contracts/mi";
3-
import { mapToPostMIResponse } from "../mappers/mi-mapper";
2+
import { IncomingMI, PostMIResponse, GetMIResponse } from "../contracts/mi";
3+
import { mapToPostMIResponse, mapToGetMIResponse } from "../mappers/mi-mapper";
44

5-
const postMI = async (
5+
export const postMI = async (
66
incomingMi: IncomingMI,
77
miRepo: MIRepository,
88
): Promise<PostMIResponse> => {
99
return mapToPostMIResponse(await miRepo.putMI(incomingMi));
1010
};
1111

12-
export default postMI;
12+
export const getMI = async (
13+
miId: string,
14+
supplierId: string,
15+
miRepo: MIRepository,
16+
): Promise<GetMIResponse> => {
17+
return mapToGetMIResponse(await miRepo.getMI(miId, supplierId));
18+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"data": {
3+
"attributes": {
4+
"groupId": "abc123",
5+
"lineItem": "envelope-business-standard",
6+
"quantity": 22,
7+
"specificationId": "2WL5eYSWGzCHlGmzNxuqVusPxDg",
8+
"stockRemaining": 2000,
9+
"timestamp": "2023-11-17T14:27:51.413Z"
10+
},
11+
"id": "2WL5eYSWGzCHlGmzNxuqVusPxDg",
12+
"type": "ManagementInformation"
13+
}
14+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
summary: Fetch an existing MI record
2+
description:
3+
$ref: '../documentation/getMI.md'
4+
operationId: getMI
5+
tags:
6+
- mi
7+
requestBody:
8+
$ref: '../requests/getMIRequest.yml'
9+
responses:
10+
"200":
11+
$ref: "../responses/getMI200.yml"
12+
"404":
13+
$ref: "../responses/errors/resourceNotFound.yml"
14+
"429":
15+
$ref: "../responses/errors/tooManyRequests.yml"
16+
"500":
17+
$ref: "../responses/errors/serverError.yml"
18+
"502":
19+
$ref: "../responses/errors/badGateway.yml"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
description: "Management information returned successfully"
2+
headers:
3+
X-Request-ID:
4+
$ref: "../responseHeaders/xRequestId.yml"
5+
X-Correlation-ID:
6+
$ref: "../responseHeaders/xCorrelationId.yml"
7+
content:
8+
application/vnd.api+json:
9+
schema:
10+
$ref: "../schemas/miResponse.yml"
11+
examples:
12+
get-mi-response:
13+
$ref: "../examples/getMI/responses/getMI_2WL5eYSWGzCHlGmzNxuqVusPxDg.json"

specification/api/notify-supplier-phase1.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ paths:
4343
- $ref: 'components/parameters/correlationId.yml'
4444
post:
4545
$ref: 'components/endpoints/createMI.yml'
46+
'/mi/getMI/{id}':
47+
parameters:
48+
- $ref: 'components/parameters/authorization/authorization.yml'
49+
- $ref: 'components/parameters/requestId.yml'
50+
- $ref: 'components/parameters/correlationId.yml'
51+
- $ref: 'components/parameters/resourceId.yml'
52+
get:
53+
$ref: 'components/endpoints/getMI.yml'
4654
/_status:
4755
get:
4856
description: Returns 200 OK if the service is up.

0 commit comments

Comments
 (0)