Skip to content

Commit 022b99b

Browse files
committed
feat: [AB#16129] crtk business activities email
1 parent a3d1d3f commit 022b99b

File tree

58 files changed

+3322
-896
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+3322
-896
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
22

3+
web/next-env.d.ts
4+
35
# dependencies
46
web/node_modules
57
/.pnp
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { crtkEmailRouter } from "@api/crtkEmailRouter";
2+
import { CrtkEmailMetadata } from "@businessnjgovnavigator/shared";
3+
import { PowerAutomateEmailClient } from "@domain/types";
4+
import { setupExpress } from "@libs/express";
5+
import { DummyLogWriter, LogWriterType } from "@libs/logWriter";
6+
import { Express } from "express";
7+
import { StatusCodes } from "http-status-codes";
8+
import request from "supertest";
9+
10+
describe("crtkEmailRouter", () => {
11+
let app: Express;
12+
let logger: LogWriterType;
13+
let stubCrtkEmailClient: jest.Mocked<PowerAutomateEmailClient>;
14+
15+
beforeEach(() => {
16+
jest.resetAllMocks();
17+
logger = DummyLogWriter;
18+
stubCrtkEmailClient = {
19+
sendEmail: jest.fn(),
20+
health: jest.fn(),
21+
};
22+
23+
app = setupExpress(false);
24+
app.use(crtkEmailRouter(stubCrtkEmailClient, logger));
25+
});
26+
27+
const emailMetaData: CrtkEmailMetadata = {
28+
username: "test user",
29+
email: "test@example.com",
30+
businessName: "Test Business",
31+
businessStatus: "Operating",
32+
businessAddress: "123 Main Street",
33+
industry: "Test Industry",
34+
ein: "123456789",
35+
naicsCode: "123456",
36+
businessActivities: "These are my business activities. We do a lot of things.",
37+
materialOrProducts: "These are my materials and products.",
38+
};
39+
40+
it("sends a request with emailMetaData and returns a success response", async () => {
41+
stubCrtkEmailClient.sendEmail.mockResolvedValue({
42+
statusCode: StatusCodes.OK,
43+
message: "Email confirmation successfully sent",
44+
});
45+
46+
const response = await request(app).post("/crtk-email").send({ emailMetaData });
47+
48+
expect(stubCrtkEmailClient.sendEmail).toHaveBeenCalledWith(emailMetaData);
49+
expect(response.status).toEqual(StatusCodes.OK);
50+
expect(response.body).toEqual("SUCCESS");
51+
});
52+
53+
it("throws an error if status is not 200", async () => {
54+
stubCrtkEmailClient.sendEmail.mockResolvedValue({
55+
statusCode: StatusCodes.FORBIDDEN,
56+
message: "Auth Failed",
57+
});
58+
59+
const response = await request(app).post("/crtk-email").send({ emailMetaData });
60+
61+
expect(stubCrtkEmailClient.sendEmail).toHaveBeenCalledWith(emailMetaData);
62+
expect(response.status).toEqual(StatusCodes.INTERNAL_SERVER_ERROR);
63+
expect(response.body).toEqual("FAILED");
64+
});
65+
66+
it("handles error if request fails", async () => {
67+
const errorMessage = "Failed to send email";
68+
stubCrtkEmailClient.sendEmail.mockRejectedValue(new Error(errorMessage));
69+
70+
const response = await request(app).post("/crtk-email").send({ emailMetaData });
71+
72+
expect(stubCrtkEmailClient.sendEmail).toHaveBeenCalledWith(emailMetaData);
73+
expect(response.status).toEqual(StatusCodes.INTERNAL_SERVER_ERROR);
74+
expect(response.body).toEqual("FAILED");
75+
});
76+
});

api/src/api/crtkEmailRouter.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { PowerAutomateEmailClient } from "@domain/types";
2+
import type { LogWriterType } from "@libs/logWriter";
3+
import { Router } from "express";
4+
import { StatusCodes } from "http-status-codes";
5+
6+
export const crtkEmailRouter = (
7+
powerAutomateClient: PowerAutomateEmailClient,
8+
logger: LogWriterType,
9+
): Router => {
10+
const router = Router();
11+
12+
router.post("/crtk-email", async (req, res) => {
13+
const { emailMetaData } = req.body;
14+
15+
try {
16+
const response = await powerAutomateClient.sendEmail(emailMetaData);
17+
logger.LogInfo(response.message);
18+
if (response.statusCode === 200) {
19+
const status = StatusCodes.OK;
20+
res.status(status).json("SUCCESS");
21+
} else {
22+
const status = StatusCodes.INTERNAL_SERVER_ERROR;
23+
logger.LogError(`Failed to send CRTK email: ${response.message}`);
24+
res.status(status).json("FAILED");
25+
}
26+
} catch (error) {
27+
const status = StatusCodes.INTERNAL_SERVER_ERROR;
28+
const errorMessage = error instanceof Error ? error.message : String(error);
29+
logger.LogError(errorMessage);
30+
res.status(status).json("FAILED");
31+
}
32+
});
33+
34+
return router;
35+
};

api/src/api/crtkRouter.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { getSignedInUserId } from "@api/userRouter";
2-
import type { DatabaseClient, UpdateCRTK } from "@domain/types";
2+
import { CrtkBusinessDetails } from "@businessnjgovnavigator/shared";
3+
import type { DatabaseClient, UpdateCrtk } from "@domain/types";
34
import { getDurationMs } from "@libs/logUtils";
45
import type { LogWriterType } from "@libs/logWriter";
5-
import { CRTKBusinessDetails } from "@shared/crtk";
66
import type { UserData } from "@shared/userData";
77
import { Router } from "express";
88
import { StatusCodes } from "http-status-codes";
99

1010
export const crtkLookupRouterFactory = (
11-
updateCRTKUser: UpdateCRTK,
11+
updateCrtkUser: UpdateCrtk,
1212
databaseClient: DatabaseClient,
1313
logger: LogWriterType,
1414
): Router => {
@@ -18,11 +18,11 @@ export const crtkLookupRouterFactory = (
1818
const method = req.method;
1919
const endpoint = req.originalUrl;
2020
const requestStart = Date.now();
21-
const businessDetails: CRTKBusinessDetails = req.body.CRTKbusinessDetails;
21+
const businessDetails: CrtkBusinessDetails = req.body.crtkBusinessDetails;
2222
logger.LogInfo(`[START] ${method} ${endpoint} - userId: ${userId}`);
2323

2424
const userData = await databaseClient.get(userId);
25-
updateCRTKUser(userData, businessDetails)
25+
updateCrtkUser(userData, businessDetails)
2626
.then(async (response: UserData) => {
2727
const status = StatusCodes.OK;
2828
await databaseClient.put(response);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { EmailConfirmationResponse } from "@businessnjgovnavigator/shared";
2+
import { ApiPowerAutomateClientFactory } from "@client/ApiPowerAutomateClientFactory";
3+
import { CrtkEmailClient } from "@client/CrtkEmailClient";
4+
import { PowerAutomateClient } from "@domain/types";
5+
import { DummyLogWriter } from "@libs/logWriter";
6+
import { getConfigValue } from "@libs/ssmUtils";
7+
import { generateEmailConfirmationSubmission } from "@shared/test";
8+
import { StatusCodes } from "http-status-codes";
9+
10+
jest.mock("@libs/ssmUtils", () => ({
11+
getConfigValue: jest.fn(),
12+
}));
13+
14+
jest.mock("@client/ApiPowerAutomateClientFactory", () => ({
15+
ApiPowerAutomateClientFactory: jest.fn(),
16+
}));
17+
18+
const mockSuccessEmailResponse: EmailConfirmationResponse = {
19+
statusCode: 200,
20+
message: "Email confirmation successfully sent",
21+
};
22+
23+
const mockErrorEmailResponse: EmailConfirmationResponse = {
24+
statusCode: 500,
25+
message: "Failed to send email confirmation",
26+
};
27+
28+
const mockPowerAutomateClient = ApiPowerAutomateClientFactory as jest.MockedFunction<
29+
typeof ApiPowerAutomateClientFactory
30+
>;
31+
const getConfigValueMock = getConfigValue as jest.MockedFunction<typeof getConfigValue>;
32+
const startWorkflow = jest.fn();
33+
34+
describe("CRTK Email Client", () => {
35+
const emailClient = CrtkEmailClient(DummyLogWriter);
36+
37+
beforeEach(() => {
38+
jest.resetAllMocks();
39+
40+
getConfigValueMock.mockImplementation(async (key: string) => {
41+
if (key === "crtk_email_url") return "https://example.com/flow";
42+
if (key === "crtk_email_key") return "api-key-xyz";
43+
throw new Error("unexpected key");
44+
});
45+
46+
mockPowerAutomateClient.mockReturnValue({
47+
startWorkflow,
48+
health: jest.fn(),
49+
} as PowerAutomateClient);
50+
});
51+
52+
describe("sendEmail", () => {
53+
it("returns a successful response when email is sent successfully", async () => {
54+
startWorkflow.mockResolvedValue({
55+
status: StatusCodes.OK,
56+
data: { message: mockSuccessEmailResponse.message },
57+
});
58+
const postBody = generateEmailConfirmationSubmission({});
59+
const response = await emailClient.sendEmail(postBody);
60+
61+
expect(response).toEqual({
62+
statusCode: StatusCodes.OK,
63+
message: "Email confirmation successfully sent",
64+
});
65+
});
66+
67+
it("returns an error response when email cannot be sent", async () => {
68+
startWorkflow.mockRejectedValue({
69+
message: mockErrorEmailResponse.message,
70+
});
71+
const postBody = generateEmailConfirmationSubmission({});
72+
const response = await emailClient.sendEmail(postBody);
73+
74+
expect(response).toEqual({
75+
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
76+
message: "Failed to send email confirmation",
77+
});
78+
});
79+
});
80+
});

api/src/client/CrtkEmailClient.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
EmailConfirmationResponse,
3+
EmailConfirmationSubmission,
4+
} from "@businessnjgovnavigator/shared";
5+
import { ApiPowerAutomateClientFactory } from "@client/ApiPowerAutomateClientFactory";
6+
import { HealthCheckMetadata, PowerAutomateEmailClient } from "@domain/types";
7+
import { LogWriterType } from "@libs/logWriter";
8+
import { getConfigValue } from "@libs/ssmUtils";
9+
import { ReasonPhrases, StatusCodes } from "http-status-codes";
10+
11+
const getConfig = async (): Promise<{
12+
emailUrl: string;
13+
emailKey: string;
14+
}> => {
15+
return {
16+
emailUrl: await getConfigValue("crtk_email_url"),
17+
emailKey: await getConfigValue("crtk_email_key"),
18+
};
19+
};
20+
21+
export const CrtkEmailClient = (logger: LogWriterType): PowerAutomateEmailClient => {
22+
const logId = logger.GetId();
23+
24+
const sendEmail = async (
25+
postBody: EmailConfirmationSubmission,
26+
): Promise<EmailConfirmationResponse> => {
27+
const config = await getConfig();
28+
29+
const crtkPowerAutomateClient = ApiPowerAutomateClientFactory({
30+
baseUrl: config.emailUrl,
31+
apiKey: config.emailKey,
32+
logger,
33+
});
34+
35+
logger.LogInfo(
36+
`CRTK Email Client - Id:${logId} - Sending request to ${
37+
config.emailUrl
38+
} data: ${JSON.stringify(postBody)}`,
39+
);
40+
return crtkPowerAutomateClient
41+
.startWorkflow({ body: postBody })
42+
.then((response) => {
43+
logger.LogInfo(
44+
`CRTK Email Client - Id:${logId} - Response received: ${JSON.stringify(response.data)}`,
45+
);
46+
47+
const successResponse: EmailConfirmationResponse = {
48+
statusCode: response.status,
49+
message: response.data.message,
50+
};
51+
return successResponse;
52+
})
53+
.catch((error) => {
54+
logger.LogError(
55+
`CRTK Email Client - Id:${logId} - Error received: ${JSON.stringify(error)}`,
56+
);
57+
const errorResponse: EmailConfirmationResponse = {
58+
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
59+
message: error.message,
60+
};
61+
62+
return errorResponse;
63+
});
64+
};
65+
66+
const health = async (): Promise<HealthCheckMetadata> => {
67+
const config = await getConfig();
68+
const crtkPowerAutomateClient = ApiPowerAutomateClientFactory({
69+
baseUrl: config.emailUrl,
70+
apiKey: config.emailKey,
71+
logger,
72+
});
73+
74+
return crtkPowerAutomateClient
75+
.health()
76+
.then(() => {
77+
logger.LogInfo(`CRTK Email Health Check - Id:${logId} - Successful response received.`);
78+
return {
79+
success: true,
80+
data: {
81+
message: ReasonPhrases.OK,
82+
},
83+
};
84+
})
85+
.catch(() => {
86+
logger.LogError(`CRTK Email Health Check - Id:${logId} - Error received.`);
87+
return {
88+
success: false,
89+
data: {
90+
message: ReasonPhrases.INTERNAL_SERVER_ERROR,
91+
},
92+
};
93+
});
94+
};
95+
96+
return {
97+
sendEmail,
98+
health,
99+
};
100+
};

0 commit comments

Comments
 (0)