From 5972e655b0266bb699e74bddf559bc970af30e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Fri, 29 Sep 2023 22:50:05 +0100 Subject: [PATCH 01/14] refactor: Update sendSlackNotification to modular structure --- api.planx.uk/modules/webhooks/schema.ts | 62 ++++++ .../webhooks/service/sendNotification.test.ts | 200 ++++++++++++++++++ .../webhooks/service/sendNotification.ts | 35 +++ api.planx.uk/modules/webhooks/types.ts | 33 +++ 4 files changed, 330 insertions(+) create mode 100644 api.planx.uk/modules/webhooks/schema.ts create mode 100644 api.planx.uk/modules/webhooks/service/sendNotification.test.ts create mode 100644 api.planx.uk/modules/webhooks/service/sendNotification.ts create mode 100644 api.planx.uk/modules/webhooks/types.ts diff --git a/api.planx.uk/modules/webhooks/schema.ts b/api.planx.uk/modules/webhooks/schema.ts new file mode 100644 index 0000000000..1aac88ce16 --- /dev/null +++ b/api.planx.uk/modules/webhooks/schema.ts @@ -0,0 +1,62 @@ +import { z } from "zod"; + +export const bopsSubmissionSchema = z.object({ + body: z.object({ + event: z.object({ + data: z.object({ + new: z.object({ + bops_id: z.string(), + destination_url: z.string(), + }), + }), + }), + }), + query: z.object({ + type: z.literal("bops-submission"), + }), +}); + +export const uniformSubmissionSchema = z.object({ + body: z.object({ + event: z.object({ + data: z.object({ + new: z.object({ + submission_reference: z.string(), + response: z.object({ + organisation: z.string(), + }), + }), + }), + }), + }), + query: z.object({ + type: z.literal("uniform-submission"), + }), +}); + +export const emailSubmissionSchema = z.object({ + body: z.object({ + event: z.object({ + data: z.object({ + new: z.object({ + session_id: z.string(), + team_slug: z.string(), + request: z.object({ + personalisation: z.object({ + serviceName: z.string(), + }), + }), + }), + }), + }), + }), + query: z.object({ + type: z.literal("bops-submission"), + }), +}); + +export const sendSlackNotificationSchema = z.union([ + bopsSubmissionSchema, + uniformSubmissionSchema, + emailSubmissionSchema, +]); diff --git a/api.planx.uk/modules/webhooks/service/sendNotification.test.ts b/api.planx.uk/modules/webhooks/service/sendNotification.test.ts new file mode 100644 index 0000000000..b9613d3c3a --- /dev/null +++ b/api.planx.uk/modules/webhooks/service/sendNotification.test.ts @@ -0,0 +1,200 @@ +import supertest from "supertest"; +import app from "../../../server"; +import SlackNotify from "slack-notify"; +import { BOPSBody, UniformBody } from "../types"; + +const ENDPOINT = "/webhooks/hasura/send-slack-notification"; + +const mockSend = jest.fn(); +jest.mock("slack-notify", () => + jest.fn().mockImplementation(() => { + return { send: mockSend }; + }), +); + +const { post } = supertest(app); + +describe("Send Slack notifications endpoint", () => { + const ORIGINAL_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...ORIGINAL_ENV }; + }); + + afterAll(() => { + process.env = ORIGINAL_ENV; + }); + + describe("authentication and validation", () => { + it("fails without correct authentication", async () => { + await post(ENDPOINT) + .expect(401) + .then((response) => { + expect(response.body).toEqual({ + error: "Unauthorised", + }); + }); + }); + + it("returns a 400 if 'type' is missing", async () => { + const body = { event: {} }; + await post(ENDPOINT) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(400) + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); + }); + + it("returns a 400 if 'type' is incorrect", async () => { + const body = { event: {} }; + await post(ENDPOINT + "?type=test-submission") + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(400) + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); + }); + + it("returns a 400 if 'event' is missing", async () => { + await post(ENDPOINT + "?type=bops-submission") + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .expect(400) + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); + }); + }); + + describe("BOPS notifications", () => { + afterEach(() => jest.clearAllMocks()); + + const body: BOPSBody = { + event: { + data: { + new: { + bops_id: "abc123", + destination_url: "https://www.bops-production.com", + }, + }, + }, + }; + + it("skips the staging environment", async () => { + process.env.APP_ENVIRONMENT = "staging"; + await post(ENDPOINT) + .query({ type: "bops-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(response.body.message).toMatch(/skipping Slack notification/); + }); + }); + + it("posts to Slack on success", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockSend.mockResolvedValue("Success!"); + + await post(ENDPOINT) + .query({ type: "bops-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(SlackNotify).toHaveBeenCalledWith( + process.env.SLACK_WEBHOOK_URL, + ); + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.message).toBe("Posted to Slack"); + expect(response.body.data).toMatch(/abc123/); + expect(response.body.data).toMatch(/www.bops-production.com/); + }); + }); + + it("returns error when Slack fails", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockSend.mockRejectedValue("Fail!"); + + await post(ENDPOINT) + .query({ type: "bops-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(500) + .then((response) => { + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.error).toMatch(/Failed to send/); + }); + }); + }); + + describe("Uniform notifications", () => { + afterEach(() => jest.clearAllMocks()); + + const body: UniformBody = { + event: { + data: { + new: { + submission_reference: "abc123", + response: { + organisation: "test-council", + }, + }, + }, + }, + }; + + it("skips the staging environment", async () => { + process.env.APP_ENVIRONMENT = "staging"; + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(response.body.message).toMatch(/skipping Slack notification/); + }); + }); + + it("posts to Slack on success", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockSend.mockResolvedValue("Success!"); + + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(SlackNotify).toHaveBeenCalledWith( + process.env.SLACK_WEBHOOK_URL, + ); + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.message).toBe("Posted to Slack"); + expect(response.body.data).toMatch(/abc123/); + expect(response.body.data).toMatch(/test-council/); + }); + }); + + it("returns error when Slack fails", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockSend.mockRejectedValue("Fail!"); + + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(500) + .then((response) => { + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.error).toMatch(/Failed to send/); + }); + }); + }); +}); diff --git a/api.planx.uk/modules/webhooks/service/sendNotification.ts b/api.planx.uk/modules/webhooks/service/sendNotification.ts new file mode 100644 index 0000000000..4acba0849a --- /dev/null +++ b/api.planx.uk/modules/webhooks/service/sendNotification.ts @@ -0,0 +1,35 @@ +import SlackNotify from "slack-notify"; +import { + BOPSEventData, + EmailEventData, + EventData, + EventType, + UniformEventData, +} from "../types"; + +export const sendSlackNotification = async ( + data: EventData, + type: EventType, +) => { + // TODO: Get SessionByID, read passport, get exemption status + // TODO: += exemption status to message + + // hook into the #planx-notifications channel + const slack = SlackNotify(process.env.SLACK_WEBHOOK_URL!); + + let message = ""; + + if (type === "bops-submission") { + const { bops_id, destination_url } = data as BOPSEventData; + message = `:incoming_envelope: New BOPS submission *${bops_id}* [${destination_url}]`; + } else if (type === "uniform-submission") { + const { submission_reference, response } = data as UniformEventData; + message = `:incoming_envelope: New Uniform submission *${submission_reference}* [${response.organisation}]`; + } else if (type === "email-submission") { + const { request, session_id, team_slug } = data as EmailEventData; + message = `:incoming_envelope: New email submission "${request.personalisation.serviceName}" *${session_id}* [${team_slug}]`; + } + + await slack.send(message); + return message; +}; diff --git a/api.planx.uk/modules/webhooks/types.ts b/api.planx.uk/modules/webhooks/types.ts new file mode 100644 index 0000000000..2f01bb1e45 --- /dev/null +++ b/api.planx.uk/modules/webhooks/types.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import { ValidatedRequestHandler } from "../../shared/middleware/validate"; +import { + bopsSubmissionSchema, + emailSubmissionSchema, + sendSlackNotificationSchema, + uniformSubmissionSchema, +} from "./schema"; + +interface SendSlackNotificationResponse { + message: string; + data?: string; +} + +export type EventType = z.infer< + typeof sendSlackNotificationSchema +>["query"]["type"]; + +export type BOPSBody = z.infer["body"]; +export type BOPSEventData = BOPSBody["event"]["data"]["new"]; + +export type UniformBody = z.infer["body"]; +export type UniformEventData = UniformBody["event"]["data"]["new"]; + +export type EmailBody = z.infer["body"]; +export type EmailEventData = EmailBody["event"]["data"]["new"]; + +export type EventData = BOPSEventData | UniformEventData | EmailEventData; + +export type SendSlackNotification = ValidatedRequestHandler< + typeof sendSlackNotificationSchema, + SendSlackNotificationResponse +>; From 07575857de48156404171db57e4895bd1f3a58fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Fri, 29 Sep 2023 22:58:43 +0100 Subject: [PATCH 02/14] test: Add email-submission tests --- api.planx.uk/modules/webhooks/schema.ts | 2 +- .../webhooks/service/sendNotification.test.ts | 70 ++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/api.planx.uk/modules/webhooks/schema.ts b/api.planx.uk/modules/webhooks/schema.ts index 1aac88ce16..f8d9044bbe 100644 --- a/api.planx.uk/modules/webhooks/schema.ts +++ b/api.planx.uk/modules/webhooks/schema.ts @@ -51,7 +51,7 @@ export const emailSubmissionSchema = z.object({ }), }), query: z.object({ - type: z.literal("bops-submission"), + type: z.literal("email-submission"), }), }); diff --git a/api.planx.uk/modules/webhooks/service/sendNotification.test.ts b/api.planx.uk/modules/webhooks/service/sendNotification.test.ts index b9613d3c3a..ed3438218c 100644 --- a/api.planx.uk/modules/webhooks/service/sendNotification.test.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification.test.ts @@ -1,7 +1,7 @@ import supertest from "supertest"; import app from "../../../server"; import SlackNotify from "slack-notify"; -import { BOPSBody, UniformBody } from "../types"; +import { BOPSBody, EmailBody, UniformBody } from "../types"; const ENDPOINT = "/webhooks/hasura/send-slack-notification"; @@ -197,4 +197,72 @@ describe("Send Slack notifications endpoint", () => { }); }); }); + + describe("Email notifications", () => { + afterEach(() => jest.clearAllMocks()); + + const body: EmailBody = { + event: { + data: { + new: { + session_id: "abc123", + team_slug: "testTeam", + request: { + personalisation: { + serviceName: "testServiceName", + }, + }, + }, + }, + }, + }; + + it("skips the staging environment", async () => { + process.env.APP_ENVIRONMENT = "staging"; + await post(ENDPOINT) + .query({ type: "email-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(response.body.message).toMatch(/skipping Slack notification/); + }); + }); + + it("posts to Slack on success", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockSend.mockResolvedValue("Success!"); + + await post(ENDPOINT) + .query({ type: "email-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(SlackNotify).toHaveBeenCalledWith( + process.env.SLACK_WEBHOOK_URL, + ); + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.message).toBe("Posted to Slack"); + expect(response.body.data).toMatch(/abc123/); + expect(response.body.data).toMatch(/testTeam/); + expect(response.body.data).toMatch(/testServiceName/); + }); + }); + + it("returns error when Slack fails", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockSend.mockRejectedValue("Fail!"); + + await post(ENDPOINT) + .query({ type: "email-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(500) + .then((response) => { + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.error).toMatch(/Failed to send/); + }); + }); + }); }); From 69be7ce1696fdf04688959156e336dd7c0cf4a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Fri, 29 Sep 2023 23:02:02 +0100 Subject: [PATCH 03/14] refactor: Rename and restructure folders, simplify service --- api.planx.uk/modules/webhooks/schema.ts | 62 ---- .../webhooks/sendNotification/index.test.ts | 3 - .../webhooks/service/sendNotification.test.ts | 268 ------------------ .../webhooks/service/sendNotification.ts | 35 --- api.planx.uk/modules/webhooks/types.ts | 33 --- 5 files changed, 401 deletions(-) delete mode 100644 api.planx.uk/modules/webhooks/schema.ts delete mode 100644 api.planx.uk/modules/webhooks/service/sendNotification.test.ts delete mode 100644 api.planx.uk/modules/webhooks/service/sendNotification.ts delete mode 100644 api.planx.uk/modules/webhooks/types.ts diff --git a/api.planx.uk/modules/webhooks/schema.ts b/api.planx.uk/modules/webhooks/schema.ts deleted file mode 100644 index f8d9044bbe..0000000000 --- a/api.planx.uk/modules/webhooks/schema.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { z } from "zod"; - -export const bopsSubmissionSchema = z.object({ - body: z.object({ - event: z.object({ - data: z.object({ - new: z.object({ - bops_id: z.string(), - destination_url: z.string(), - }), - }), - }), - }), - query: z.object({ - type: z.literal("bops-submission"), - }), -}); - -export const uniformSubmissionSchema = z.object({ - body: z.object({ - event: z.object({ - data: z.object({ - new: z.object({ - submission_reference: z.string(), - response: z.object({ - organisation: z.string(), - }), - }), - }), - }), - }), - query: z.object({ - type: z.literal("uniform-submission"), - }), -}); - -export const emailSubmissionSchema = z.object({ - body: z.object({ - event: z.object({ - data: z.object({ - new: z.object({ - session_id: z.string(), - team_slug: z.string(), - request: z.object({ - personalisation: z.object({ - serviceName: z.string(), - }), - }), - }), - }), - }), - }), - query: z.object({ - type: z.literal("email-submission"), - }), -}); - -export const sendSlackNotificationSchema = z.union([ - bopsSubmissionSchema, - uniformSubmissionSchema, - emailSubmissionSchema, -]); diff --git a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts b/api.planx.uk/modules/webhooks/sendNotification/index.test.ts index e73c115371..3ddd8a93f3 100644 --- a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts +++ b/api.planx.uk/modules/webhooks/sendNotification/index.test.ts @@ -208,11 +208,8 @@ describe("Send Slack notifications endpoint", () => { ); expect(mockSend).toHaveBeenCalledTimes(1); expect(mockAdmin.session.find).toHaveBeenCalledTimes(1); - expect(response.body.message).toBe("Posted to Slack"); expect(response.body.data).toMatch(/abc123/); - expect(response.body.data).toMatch(/test-council/); - }); }); it("adds a status to the Slack message for a disability exemption", async () => { diff --git a/api.planx.uk/modules/webhooks/service/sendNotification.test.ts b/api.planx.uk/modules/webhooks/service/sendNotification.test.ts deleted file mode 100644 index ed3438218c..0000000000 --- a/api.planx.uk/modules/webhooks/service/sendNotification.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -import supertest from "supertest"; -import app from "../../../server"; -import SlackNotify from "slack-notify"; -import { BOPSBody, EmailBody, UniformBody } from "../types"; - -const ENDPOINT = "/webhooks/hasura/send-slack-notification"; - -const mockSend = jest.fn(); -jest.mock("slack-notify", () => - jest.fn().mockImplementation(() => { - return { send: mockSend }; - }), -); - -const { post } = supertest(app); - -describe("Send Slack notifications endpoint", () => { - const ORIGINAL_ENV = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...ORIGINAL_ENV }; - }); - - afterAll(() => { - process.env = ORIGINAL_ENV; - }); - - describe("authentication and validation", () => { - it("fails without correct authentication", async () => { - await post(ENDPOINT) - .expect(401) - .then((response) => { - expect(response.body).toEqual({ - error: "Unauthorised", - }); - }); - }); - - it("returns a 400 if 'type' is missing", async () => { - const body = { event: {} }; - await post(ENDPOINT) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(400) - .then((response) => { - expect(response.body).toHaveProperty("issues"); - expect(response.body).toHaveProperty("name", "ZodError"); - }); - }); - - it("returns a 400 if 'type' is incorrect", async () => { - const body = { event: {} }; - await post(ENDPOINT + "?type=test-submission") - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(400) - .then((response) => { - expect(response.body).toHaveProperty("issues"); - expect(response.body).toHaveProperty("name", "ZodError"); - }); - }); - - it("returns a 400 if 'event' is missing", async () => { - await post(ENDPOINT + "?type=bops-submission") - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .expect(400) - .then((response) => { - expect(response.body).toHaveProperty("issues"); - expect(response.body).toHaveProperty("name", "ZodError"); - }); - }); - }); - - describe("BOPS notifications", () => { - afterEach(() => jest.clearAllMocks()); - - const body: BOPSBody = { - event: { - data: { - new: { - bops_id: "abc123", - destination_url: "https://www.bops-production.com", - }, - }, - }, - }; - - it("skips the staging environment", async () => { - process.env.APP_ENVIRONMENT = "staging"; - await post(ENDPOINT) - .query({ type: "bops-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(200) - .then((response) => { - expect(response.body.message).toMatch(/skipping Slack notification/); - }); - }); - - it("posts to Slack on success", async () => { - process.env.APP_ENVIRONMENT = "production"; - mockSend.mockResolvedValue("Success!"); - - await post(ENDPOINT) - .query({ type: "bops-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(200) - .then((response) => { - expect(SlackNotify).toHaveBeenCalledWith( - process.env.SLACK_WEBHOOK_URL, - ); - expect(mockSend).toHaveBeenCalledTimes(1); - expect(response.body.message).toBe("Posted to Slack"); - expect(response.body.data).toMatch(/abc123/); - expect(response.body.data).toMatch(/www.bops-production.com/); - }); - }); - - it("returns error when Slack fails", async () => { - process.env.APP_ENVIRONMENT = "production"; - mockSend.mockRejectedValue("Fail!"); - - await post(ENDPOINT) - .query({ type: "bops-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(500) - .then((response) => { - expect(mockSend).toHaveBeenCalledTimes(1); - expect(response.body.error).toMatch(/Failed to send/); - }); - }); - }); - - describe("Uniform notifications", () => { - afterEach(() => jest.clearAllMocks()); - - const body: UniformBody = { - event: { - data: { - new: { - submission_reference: "abc123", - response: { - organisation: "test-council", - }, - }, - }, - }, - }; - - it("skips the staging environment", async () => { - process.env.APP_ENVIRONMENT = "staging"; - await post(ENDPOINT) - .query({ type: "uniform-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(200) - .then((response) => { - expect(response.body.message).toMatch(/skipping Slack notification/); - }); - }); - - it("posts to Slack on success", async () => { - process.env.APP_ENVIRONMENT = "production"; - mockSend.mockResolvedValue("Success!"); - - await post(ENDPOINT) - .query({ type: "uniform-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(200) - .then((response) => { - expect(SlackNotify).toHaveBeenCalledWith( - process.env.SLACK_WEBHOOK_URL, - ); - expect(mockSend).toHaveBeenCalledTimes(1); - expect(response.body.message).toBe("Posted to Slack"); - expect(response.body.data).toMatch(/abc123/); - expect(response.body.data).toMatch(/test-council/); - }); - }); - - it("returns error when Slack fails", async () => { - process.env.APP_ENVIRONMENT = "production"; - mockSend.mockRejectedValue("Fail!"); - - await post(ENDPOINT) - .query({ type: "uniform-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(500) - .then((response) => { - expect(mockSend).toHaveBeenCalledTimes(1); - expect(response.body.error).toMatch(/Failed to send/); - }); - }); - }); - - describe("Email notifications", () => { - afterEach(() => jest.clearAllMocks()); - - const body: EmailBody = { - event: { - data: { - new: { - session_id: "abc123", - team_slug: "testTeam", - request: { - personalisation: { - serviceName: "testServiceName", - }, - }, - }, - }, - }, - }; - - it("skips the staging environment", async () => { - process.env.APP_ENVIRONMENT = "staging"; - await post(ENDPOINT) - .query({ type: "email-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(200) - .then((response) => { - expect(response.body.message).toMatch(/skipping Slack notification/); - }); - }); - - it("posts to Slack on success", async () => { - process.env.APP_ENVIRONMENT = "production"; - mockSend.mockResolvedValue("Success!"); - - await post(ENDPOINT) - .query({ type: "email-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(200) - .then((response) => { - expect(SlackNotify).toHaveBeenCalledWith( - process.env.SLACK_WEBHOOK_URL, - ); - expect(mockSend).toHaveBeenCalledTimes(1); - expect(response.body.message).toBe("Posted to Slack"); - expect(response.body.data).toMatch(/abc123/); - expect(response.body.data).toMatch(/testTeam/); - expect(response.body.data).toMatch(/testServiceName/); - }); - }); - - it("returns error when Slack fails", async () => { - process.env.APP_ENVIRONMENT = "production"; - mockSend.mockRejectedValue("Fail!"); - - await post(ENDPOINT) - .query({ type: "email-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(500) - .then((response) => { - expect(mockSend).toHaveBeenCalledTimes(1); - expect(response.body.error).toMatch(/Failed to send/); - }); - }); - }); -}); diff --git a/api.planx.uk/modules/webhooks/service/sendNotification.ts b/api.planx.uk/modules/webhooks/service/sendNotification.ts deleted file mode 100644 index 4acba0849a..0000000000 --- a/api.planx.uk/modules/webhooks/service/sendNotification.ts +++ /dev/null @@ -1,35 +0,0 @@ -import SlackNotify from "slack-notify"; -import { - BOPSEventData, - EmailEventData, - EventData, - EventType, - UniformEventData, -} from "../types"; - -export const sendSlackNotification = async ( - data: EventData, - type: EventType, -) => { - // TODO: Get SessionByID, read passport, get exemption status - // TODO: += exemption status to message - - // hook into the #planx-notifications channel - const slack = SlackNotify(process.env.SLACK_WEBHOOK_URL!); - - let message = ""; - - if (type === "bops-submission") { - const { bops_id, destination_url } = data as BOPSEventData; - message = `:incoming_envelope: New BOPS submission *${bops_id}* [${destination_url}]`; - } else if (type === "uniform-submission") { - const { submission_reference, response } = data as UniformEventData; - message = `:incoming_envelope: New Uniform submission *${submission_reference}* [${response.organisation}]`; - } else if (type === "email-submission") { - const { request, session_id, team_slug } = data as EmailEventData; - message = `:incoming_envelope: New email submission "${request.personalisation.serviceName}" *${session_id}* [${team_slug}]`; - } - - await slack.send(message); - return message; -}; diff --git a/api.planx.uk/modules/webhooks/types.ts b/api.planx.uk/modules/webhooks/types.ts deleted file mode 100644 index 2f01bb1e45..0000000000 --- a/api.planx.uk/modules/webhooks/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { z } from "zod"; -import { ValidatedRequestHandler } from "../../shared/middleware/validate"; -import { - bopsSubmissionSchema, - emailSubmissionSchema, - sendSlackNotificationSchema, - uniformSubmissionSchema, -} from "./schema"; - -interface SendSlackNotificationResponse { - message: string; - data?: string; -} - -export type EventType = z.infer< - typeof sendSlackNotificationSchema ->["query"]["type"]; - -export type BOPSBody = z.infer["body"]; -export type BOPSEventData = BOPSBody["event"]["data"]["new"]; - -export type UniformBody = z.infer["body"]; -export type UniformEventData = UniformBody["event"]["data"]["new"]; - -export type EmailBody = z.infer["body"]; -export type EmailEventData = EmailBody["event"]["data"]["new"]; - -export type EventData = BOPSEventData | UniformEventData | EmailEventData; - -export type SendSlackNotification = ValidatedRequestHandler< - typeof sendSlackNotificationSchema, - SendSlackNotificationResponse ->; From ced39ca2aae3bc72740a1897428092fde90957df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Sat, 30 Sep 2023 11:23:45 +0100 Subject: [PATCH 04/14] feat: Add exemption status to notification --- .../webhooks/sendNotification/index.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts b/api.planx.uk/modules/webhooks/sendNotification/index.test.ts index 3ddd8a93f3..2eaa93685a 100644 --- a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts +++ b/api.planx.uk/modules/webhooks/sendNotification/index.test.ts @@ -259,6 +259,39 @@ describe("Send Slack notifications endpoint", () => { }); }); + it("adds an exemption status if there's no fee for the session", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockAdmin.session.find = jest + .fn() + .mockResolvedValue(mockSessionWithoutFee); + + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(response.body.data).toMatch(/abc123/); + expect(response.body.data).toMatch(/test-council/); + expect(response.body.data).toMatch(/[Exempt]/); + }); + }); + + it("handles missing sessions", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockAdmin.session.find = jest.fn().mockResolvedValueOnce(null); + + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(500) + .then((response) => { + expect(mockAdmin.session.find).toHaveBeenCalledTimes(1); + expect(response.body.error).toMatch(/Failed to send/); + }); + }); + it("returns error when Slack fails", async () => { process.env.APP_ENVIRONMENT = "production"; mockSend.mockRejectedValue("Fail!"); From 915cf6c4d1ad2b4d55db871f9c4b07a96e7836c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Sat, 30 Sep 2023 21:28:12 +0100 Subject: [PATCH 05/14] feat: CreatePaymentInvitation --- api.planx.uk/modules/webhooks/controller.ts | 17 ++++++ .../index.test.ts} | 10 ++-- .../webhooks/paymentRequestEvents/schema.ts | 23 ++++++++ .../service.ts} | 53 +++++++------------ api.planx.uk/modules/webhooks/routes.ts | 16 +++--- api.planx.uk/shared/middleware/validate.ts | 1 + 6 files changed, 77 insertions(+), 43 deletions(-) rename api.planx.uk/modules/webhooks/{_old/paymentRequestEvents.test.ts => paymentRequestEvents/index.test.ts} (97%) create mode 100644 api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts rename api.planx.uk/modules/webhooks/{_old/paymentRequestEvents.ts => paymentRequestEvents/service.ts} (76%) diff --git a/api.planx.uk/modules/webhooks/controller.ts b/api.planx.uk/modules/webhooks/controller.ts index 8b98358f89..0a9a11db56 100644 --- a/api.planx.uk/modules/webhooks/controller.ts +++ b/api.planx.uk/modules/webhooks/controller.ts @@ -1,4 +1,6 @@ import { ServerError } from "../../errors"; +import { CreatePaymentInvitationEvents } from "./paymentRequestEvents/schema"; +import { createPaymentInvitationEvents } from "./paymentRequestEvents/service"; import { sendSlackNotification } from "./sendNotification/service"; import { SendSlackNotification } from "./sendNotification/types"; @@ -29,3 +31,18 @@ export const sendSlackNotificationController: SendSlackNotification = async ( ); } }; + +export const createPaymentInvitationEventsController: CreatePaymentInvitationEvents = + async (req, res, next) => { + try { + const response = await createPaymentInvitationEvents(req.body); + res.json(response); + } catch (error) { + return next( + new ServerError({ + message: `Failed to create payment invitation events`, + cause: error, + }), + ); + } + }; diff --git a/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.test.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts similarity index 97% rename from api.planx.uk/modules/webhooks/_old/paymentRequestEvents.test.ts rename to api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts index 282ca011a5..026fdaf5eb 100644 --- a/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.test.ts +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts @@ -1,6 +1,7 @@ import supertest from "supertest"; import app from "../../../server"; import { createScheduledEvent } from "../../../hasura/metadata"; +import { CreatePaymentInvitation } from "./schema"; const { post } = supertest(app); @@ -33,14 +34,15 @@ describe("Create payment invitation events webhook", () => { .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(400) - .then((response) => - expect(response.body.error).toEqual("Required value missing"), - ); + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); } }); it("returns a 200 on successful event setup", async () => { - const body = { + const body: CreatePaymentInvitation = { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts new file mode 100644 index 0000000000..c70790a187 --- /dev/null +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; +import { ValidatedRequestHandler } from "../../../shared/middleware/validate"; + +// TODO: Make this better +type Response = [any, any]; + +export const CreatePaymentInvitationEventsSchema = z.object({ + body: z.object({ + createdAt: z.string().transform((val) => new Date(val)), + payload: z.object({ + paymentRequestId: z.string(), + }), + }), +}); + +export type CreatePaymentInvitation = z.infer< + typeof CreatePaymentInvitationEventsSchema +>["body"]; + +export type CreatePaymentInvitationEvents = ValidatedRequestHandler< + typeof CreatePaymentInvitationEventsSchema, + Response +>; diff --git a/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts similarity index 76% rename from api.planx.uk/modules/webhooks/_old/paymentRequestEvents.ts rename to api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts index 30a387b228..6a755d8b44 100644 --- a/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.ts +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts @@ -6,43 +6,30 @@ import { DAYS_UNTIL_EXPIRY, REMINDER_DAYS_FROM_EXPIRY, } from "../../../saveAndReturn/utils"; +import { CreatePaymentInvitation } from "./schema"; /** * Create two "invitation" events for a payments_request record: one for the nominee and one for the agent */ -const createPaymentInvitationEvents = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { createdAt, payload } = req.body; - if (!createdAt || !payload) - return next({ - status: 400, - message: "Required value missing", - }); - const response = await Promise.all([ - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/invite-to-pay", - schedule_at: createdAt, - payload: payload, - comment: `payment_invitation_${payload.paymentRequestId}`, - }), - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/invite-to-pay-agent", - schedule_at: createdAt, - payload: payload, - comment: `payment_invitation_agent_${payload.paymentRequestId}`, - }), - ]); - res.json(response); - } catch (error) { - return next({ - error, - message: `Failed to create payment invitation events. Error: ${error}`, - }); - } +const createPaymentInvitationEvents = async ({ + createdAt, + payload, +}: CreatePaymentInvitation) => { + const response = await Promise.all([ + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/invite-to-pay", + schedule_at: createdAt, + payload: payload, + comment: `payment_invitation_${payload.paymentRequestId}`, + }), + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/invite-to-pay-agent", + schedule_at: createdAt, + payload: payload, + comment: `payment_invitation_agent_${payload.paymentRequestId}`, + }), + ]); + return response; }; /** diff --git a/api.planx.uk/modules/webhooks/routes.ts b/api.planx.uk/modules/webhooks/routes.ts index c98421b7e3..5479d49b55 100644 --- a/api.planx.uk/modules/webhooks/routes.ts +++ b/api.planx.uk/modules/webhooks/routes.ts @@ -6,14 +6,17 @@ import { createExpiryEvent, createReminderEvent, } from "./_old/lowcalSessionEvents"; +import { validate } from "../../shared/middleware/validate"; +import { + createPaymentInvitationEventsController, + sendSlackNotificationController, +} from "./controller"; +import { sendSlackNotificationSchema } from "./sendNotification/schema"; import { createPaymentExpiryEvents, - createPaymentInvitationEvents, createPaymentReminderEvents, -} from "./_old/paymentRequestEvents"; -import { validate } from "../../shared/middleware/validate"; -import { sendSlackNotificationController } from "./controller"; -import { sendSlackNotificationSchema } from "./sendNotification/schema"; +} from "./paymentRequestEvents/service"; +import { CreatePaymentInvitationEventsSchema } from "./paymentRequestEvents/schema"; const router = Router(); @@ -22,7 +25,8 @@ router.post("/hasura/create-reminder-event", createReminderEvent); router.post("/hasura/create-expiry-event", createExpiryEvent); router.post( "/hasura/create-payment-invitation-events", - createPaymentInvitationEvents, + validate(CreatePaymentInvitationEventsSchema), + createPaymentInvitationEventsController, ); router.post( "/hasura/create-payment-reminder-events", diff --git a/api.planx.uk/shared/middleware/validate.ts b/api.planx.uk/shared/middleware/validate.ts index ce647f2f16..854a37938e 100644 --- a/api.planx.uk/shared/middleware/validate.ts +++ b/api.planx.uk/shared/middleware/validate.ts @@ -20,6 +20,7 @@ export const validate = }); return next(); } catch (error) { + console.error(error); return res.status(400).json(error); } }; From a99ba919862196be9a79cb6acc156b4ec801b70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Sat, 30 Sep 2023 21:43:34 +0100 Subject: [PATCH 06/14] feat: CreatePaymentReminderEvents --- api.planx.uk/modules/webhooks/controller.ts | 24 ++++++- .../paymentRequestEvents/index.test.ts | 11 +-- .../webhooks/paymentRequestEvents/schema.ts | 12 ++-- .../webhooks/paymentRequestEvents/service.ts | 68 ++++++++----------- api.planx.uk/modules/webhooks/routes.ts | 13 ++-- 5 files changed, 66 insertions(+), 62 deletions(-) diff --git a/api.planx.uk/modules/webhooks/controller.ts b/api.planx.uk/modules/webhooks/controller.ts index 0a9a11db56..3ebf82a1c7 100644 --- a/api.planx.uk/modules/webhooks/controller.ts +++ b/api.planx.uk/modules/webhooks/controller.ts @@ -1,6 +1,9 @@ import { ServerError } from "../../errors"; -import { CreatePaymentInvitationEvents } from "./paymentRequestEvents/schema"; -import { createPaymentInvitationEvents } from "./paymentRequestEvents/service"; +import { CreatePaymentEventController } from "./paymentRequestEvents/schema"; +import { + createPaymentInvitationEvents, + createPaymentReminderEvents, +} from "./paymentRequestEvents/service"; import { sendSlackNotification } from "./sendNotification/service"; import { SendSlackNotification } from "./sendNotification/types"; @@ -32,7 +35,7 @@ export const sendSlackNotificationController: SendSlackNotification = async ( } }; -export const createPaymentInvitationEventsController: CreatePaymentInvitationEvents = +export const createPaymentInvitationEventsController: CreatePaymentEventController = async (req, res, next) => { try { const response = await createPaymentInvitationEvents(req.body); @@ -46,3 +49,18 @@ export const createPaymentInvitationEventsController: CreatePaymentInvitationEve ); } }; + +export const createPaymentReminderEventsController: CreatePaymentEventController = + async (req, res, next) => { + try { + const response = await createPaymentReminderEvents(req.body); + res.json(response); + } catch (error) { + return next( + new ServerError({ + message: "Failed to create payment reminder events", + cause: error, + }), + ); + } + }; diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts index 026fdaf5eb..16d61665b2 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts @@ -1,7 +1,7 @@ import supertest from "supertest"; import app from "../../../server"; import { createScheduledEvent } from "../../../hasura/metadata"; -import { CreatePaymentInvitation } from "./schema"; +import { CreatePaymentEvent } from "./schema"; const { post } = supertest(app); @@ -42,7 +42,7 @@ describe("Create payment invitation events webhook", () => { }); it("returns a 200 on successful event setup", async () => { - const body: CreatePaymentInvitation = { + const body: CreatePaymentEvent = { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; @@ -137,9 +137,10 @@ describe("Create payment reminder events webhook", () => { .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(400) - .then((response) => - expect(response.body.error).toEqual("Required value missing"), - ); + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); } }); diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts index c70790a187..fa3097495e 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts @@ -2,9 +2,9 @@ import { z } from "zod"; import { ValidatedRequestHandler } from "../../../shared/middleware/validate"; // TODO: Make this better -type Response = [any, any]; +type Response = [any, any] | any[]; -export const CreatePaymentInvitationEventsSchema = z.object({ +export const CreatePaymentEventSchema = z.object({ body: z.object({ createdAt: z.string().transform((val) => new Date(val)), payload: z.object({ @@ -13,11 +13,11 @@ export const CreatePaymentInvitationEventsSchema = z.object({ }), }); -export type CreatePaymentInvitation = z.infer< - typeof CreatePaymentInvitationEventsSchema +export type CreatePaymentEvent = z.infer< + typeof CreatePaymentEventSchema >["body"]; -export type CreatePaymentInvitationEvents = ValidatedRequestHandler< - typeof CreatePaymentInvitationEventsSchema, +export type CreatePaymentEventController = ValidatedRequestHandler< + typeof CreatePaymentEventSchema, Response >; diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts index 6a755d8b44..7c52e33cc9 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts @@ -6,7 +6,7 @@ import { DAYS_UNTIL_EXPIRY, REMINDER_DAYS_FROM_EXPIRY, } from "../../../saveAndReturn/utils"; -import { CreatePaymentInvitation } from "./schema"; +import { CreatePaymentEvent } from "./schema"; /** * Create two "invitation" events for a payments_request record: one for the nominee and one for the agent @@ -14,7 +14,7 @@ import { CreatePaymentInvitation } from "./schema"; const createPaymentInvitationEvents = async ({ createdAt, payload, -}: CreatePaymentInvitation) => { +}: CreatePaymentEvent) => { const response = await Promise.all([ createScheduledEvent({ webhook: "{{HASURA_PLANX_API_URL}}/send-email/invite-to-pay", @@ -35,45 +35,31 @@ const createPaymentInvitationEvents = async ({ /** * Create "reminder" events for a payment_requests record: one for the nominee and one for the agent */ -const createPaymentReminderEvents = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { createdAt, payload } = req.body; - if (!createdAt || !payload) - return next({ - status: 400, - message: "Required value missing", - }); - const applicantResponse = await Promise.all( - REMINDER_DAYS_FROM_EXPIRY.map((day: number) => - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-reminder", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY - day), - payload: payload, - comment: `payment_reminder_${payload.paymentRequestId}_${day}day`, - }), - ), - ); - const agentResponse = await Promise.all( - REMINDER_DAYS_FROM_EXPIRY.map((day: number) => - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-reminder-agent", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY - day), - payload: payload, - comment: `payment_reminder_agent_${payload.paymentRequestId}_${day}day`, - }), - ), - ); - res.json([...applicantResponse, ...agentResponse]); - } catch (error) { - return next({ - error, - message: `Failed to create payment reminder events. Error: ${error}`, - }); - } +const createPaymentReminderEvents = async ({ + createdAt, + payload, +}: CreatePaymentEvent) => { + const applicantResponse = await Promise.all( + REMINDER_DAYS_FROM_EXPIRY.map((day: number) => + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-reminder", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY - day), + payload: payload, + comment: `payment_reminder_${payload.paymentRequestId}_${day}day`, + }), + ), + ); + const agentResponse = await Promise.all( + REMINDER_DAYS_FROM_EXPIRY.map((day: number) => + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-reminder-agent", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY - day), + payload: payload, + comment: `payment_reminder_agent_${payload.paymentRequestId}_${day}day`, + }), + ), + ); + return [...applicantResponse, ...agentResponse]; }; /** diff --git a/api.planx.uk/modules/webhooks/routes.ts b/api.planx.uk/modules/webhooks/routes.ts index 5479d49b55..77a9483cc6 100644 --- a/api.planx.uk/modules/webhooks/routes.ts +++ b/api.planx.uk/modules/webhooks/routes.ts @@ -9,14 +9,12 @@ import { import { validate } from "../../shared/middleware/validate"; import { createPaymentInvitationEventsController, + createPaymentReminderEventsController, sendSlackNotificationController, } from "./controller"; import { sendSlackNotificationSchema } from "./sendNotification/schema"; -import { - createPaymentExpiryEvents, - createPaymentReminderEvents, -} from "./paymentRequestEvents/service"; -import { CreatePaymentInvitationEventsSchema } from "./paymentRequestEvents/schema"; +import { createPaymentExpiryEvents } from "./paymentRequestEvents/service"; +import { CreatePaymentEventSchema } from "./paymentRequestEvents/schema"; const router = Router(); @@ -25,12 +23,13 @@ router.post("/hasura/create-reminder-event", createReminderEvent); router.post("/hasura/create-expiry-event", createExpiryEvent); router.post( "/hasura/create-payment-invitation-events", - validate(CreatePaymentInvitationEventsSchema), + validate(CreatePaymentEventSchema), createPaymentInvitationEventsController, ); router.post( "/hasura/create-payment-reminder-events", - createPaymentReminderEvents, + validate(CreatePaymentEventSchema), + createPaymentReminderEventsController, ); router.post("/hasura/create-payment-expiry-events", createPaymentExpiryEvents); router.post("/hasura/create-payment-send-events", createPaymentSendEvents); From 0849782a3655bd9be87311eadedb3f356845f8e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Sat, 30 Sep 2023 21:54:51 +0100 Subject: [PATCH 07/14] feat: CreatePaymentExpiryEvents --- api.planx.uk/modules/webhooks/controller.ts | 16 +++++ .../paymentRequestEvents/index.test.ts | 9 +-- .../webhooks/paymentRequestEvents/service.ts | 65 ++++++------------- api.planx.uk/modules/webhooks/routes.ts | 8 ++- 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/api.planx.uk/modules/webhooks/controller.ts b/api.planx.uk/modules/webhooks/controller.ts index 3ebf82a1c7..26b51612f3 100644 --- a/api.planx.uk/modules/webhooks/controller.ts +++ b/api.planx.uk/modules/webhooks/controller.ts @@ -1,6 +1,7 @@ import { ServerError } from "../../errors"; import { CreatePaymentEventController } from "./paymentRequestEvents/schema"; import { + createPaymentExpiryEvents, createPaymentInvitationEvents, createPaymentReminderEvents, } from "./paymentRequestEvents/service"; @@ -64,3 +65,18 @@ export const createPaymentReminderEventsController: CreatePaymentEventController ); } }; + +export const createPaymentExpiryEventsController: CreatePaymentEventController = + async (req, res, next) => { + try { + const response = await createPaymentExpiryEvents(req.body); + res.json(response); + } catch (error) { + return next( + new ServerError({ + message: "Failed to create payment expiry events", + cause: error, + }), + ); + } + }; diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts index 16d61665b2..de5eb3b652 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts @@ -258,9 +258,10 @@ describe("Create payment expiry events webhook", () => { .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(400) - .then((response) => - expect(response.body.error).toEqual("Required value missing"), - ); + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); } }); @@ -276,7 +277,7 @@ describe("Create payment expiry events webhook", () => { .send(body) .expect(200) .then((response) => { - // it's queued up x2 expirys: one for the payee and one for the agent + // it's queued up x2 expiries: one for the payee and one for the agent expect(response.body).toHaveLength(2); expect(response.body).toStrictEqual(["test", "test"]); }); diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts index 7c52e33cc9..d60822d036 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts @@ -1,5 +1,4 @@ import { addDays } from "date-fns"; -import { Request, Response, NextFunction } from "express"; import { createScheduledEvent } from "../../../hasura/metadata"; import { @@ -11,7 +10,7 @@ import { CreatePaymentEvent } from "./schema"; /** * Create two "invitation" events for a payments_request record: one for the nominee and one for the agent */ -const createPaymentInvitationEvents = async ({ +export const createPaymentInvitationEvents = async ({ createdAt, payload, }: CreatePaymentEvent) => { @@ -35,7 +34,7 @@ const createPaymentInvitationEvents = async ({ /** * Create "reminder" events for a payment_requests record: one for the nominee and one for the agent */ -const createPaymentReminderEvents = async ({ +export const createPaymentReminderEvents = async ({ createdAt, payload, }: CreatePaymentEvent) => { @@ -65,45 +64,23 @@ const createPaymentReminderEvents = async ({ /** * Create two "expiry" events for a payment_requests record: one for the nominee and one for the agent */ -const createPaymentExpiryEvents = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { createdAt, payload } = req.body; - if (!createdAt || !payload) - return next({ - status: 400, - message: "Required value missing", - }); - const response = await Promise.all([ - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-expiry", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY), - payload: payload, - comment: `payment_expiry_${payload.paymentRequestId}`, - }), - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-expiry-agent", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY), - payload: payload, - comment: `payment_expiry_agent_${payload.paymentRequestId}`, - }), - ]); - res.json(response); - } catch (error) { - return next({ - error, - message: `Failed to create payment expiry events. Error: ${ - (error as Error).message - }`, - }); - } -}; - -export { - createPaymentInvitationEvents, - createPaymentReminderEvents, - createPaymentExpiryEvents, +export const createPaymentExpiryEvents = async ({ + createdAt, + payload, +}: CreatePaymentEvent) => { + const response = await Promise.all([ + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-expiry", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY), + payload: payload, + comment: `payment_expiry_${payload.paymentRequestId}`, + }), + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-expiry-agent", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY), + payload: payload, + comment: `payment_expiry_agent_${payload.paymentRequestId}`, + }), + ]); + return response; }; diff --git a/api.planx.uk/modules/webhooks/routes.ts b/api.planx.uk/modules/webhooks/routes.ts index 77a9483cc6..b5ff2405ac 100644 --- a/api.planx.uk/modules/webhooks/routes.ts +++ b/api.planx.uk/modules/webhooks/routes.ts @@ -8,12 +8,12 @@ import { } from "./_old/lowcalSessionEvents"; import { validate } from "../../shared/middleware/validate"; import { + createPaymentExpiryEventsController, createPaymentInvitationEventsController, createPaymentReminderEventsController, sendSlackNotificationController, } from "./controller"; import { sendSlackNotificationSchema } from "./sendNotification/schema"; -import { createPaymentExpiryEvents } from "./paymentRequestEvents/service"; import { CreatePaymentEventSchema } from "./paymentRequestEvents/schema"; const router = Router(); @@ -31,7 +31,11 @@ router.post( validate(CreatePaymentEventSchema), createPaymentReminderEventsController, ); -router.post("/hasura/create-payment-expiry-events", createPaymentExpiryEvents); +router.post( + "/hasura/create-payment-expiry-events", + validate(CreatePaymentEventSchema), + createPaymentExpiryEventsController, +); router.post("/hasura/create-payment-send-events", createPaymentSendEvents); router.post( "/hasura/send-slack-notification", From 7b582743e2fe189c92bbb59a513867a038f47ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Sat, 30 Sep 2023 22:31:23 +0100 Subject: [PATCH 08/14] docs: Swagger docs for payment event endpoints --- api.planx.uk/modules/webhooks/docs.yaml | 74 +++++++++++++++++-- .../webhooks/paymentRequestEvents/schema.ts | 6 +- api.planx.uk/modules/webhooks/routes.ts | 16 ++-- 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/api.planx.uk/modules/webhooks/docs.yaml b/api.planx.uk/modules/webhooks/docs.yaml index 837e64a8ea..2549552666 100644 --- a/api.planx.uk/modules/webhooks/docs.yaml +++ b/api.planx.uk/modules/webhooks/docs.yaml @@ -14,7 +14,7 @@ components: type: string required: - sessionId - BopsSubmissionSchema: + BopsSubmission: type: object properties: body: @@ -37,7 +37,7 @@ components: type: string required: - body - UniformSubmissionSchema: + UniformSubmission: type: object properties: body: @@ -63,7 +63,7 @@ components: type: string required: - body - EmailSubmissionSchema: + EmailSubmission: type: object properties: body: @@ -92,11 +92,24 @@ components: type: string required: - body - SendSlackNotificationSchema: + SendSlackNotification: oneOf: - - $ref: "#/components/schemas/BopsSubmissionSchema" - - $ref: "#/components/schemas/UniformSubmissionSchema" - - $ref: "#/components/schemas/EmailSubmissionSchema" + - $ref: "#/components/schemas/BopsSubmission" + - $ref: "#/components/schemas/UniformSubmission" + - $ref: "#/components/schemas/EmailSubmission" + CreatePaymentEvent: + type: object + properties: + createdAt: + required: true + type: string + format: date-time + payload: + required: true + type: object + properties: + paymentRequestId: + type: string responses: SlackNotificationSuccessMessage: content: @@ -127,9 +140,54 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/SendSlackNotificationSchema" + $ref: "#/components/schemas/SendSlackNotification" responses: "200": $ref: "#/components/responses/SlackNotificationSuccessMessage" "500": $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-payment-invitation-events: + post: + tags: ["webhooks"] + summary: Create payment invitation events + description: Setup events which will trigger payment invitation emails to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePaymentEvent" + responses: + "200": + $ref: "#/components/responses/TODO" + "500": + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-payment-reminder-events: + post: + tags: ["webhooks"] + summary: Create payment reminder events + description: Setup events which will trigger payment reminder emails to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePaymentEvent" + responses: + "200": + $ref: "#/components/responses/TODO" + "500": + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-payment-expiry-events: + post: + tags: ["webhooks"] + summary: Create payment expiry events + description: Setup events which will trigger payment expiry emails to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePaymentEvent" + responses: + "200": + $ref: "#/components/responses/TODO" + "500": + $ref: "#/components/responses/ErrorMessage" \ No newline at end of file diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts index fa3097495e..4ad55670e0 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts @@ -4,7 +4,7 @@ import { ValidatedRequestHandler } from "../../../shared/middleware/validate"; // TODO: Make this better type Response = [any, any] | any[]; -export const CreatePaymentEventSchema = z.object({ +export const createPaymentEventSchema = z.object({ body: z.object({ createdAt: z.string().transform((val) => new Date(val)), payload: z.object({ @@ -14,10 +14,10 @@ export const CreatePaymentEventSchema = z.object({ }); export type CreatePaymentEvent = z.infer< - typeof CreatePaymentEventSchema + typeof createPaymentEventSchema >["body"]; export type CreatePaymentEventController = ValidatedRequestHandler< - typeof CreatePaymentEventSchema, + typeof createPaymentEventSchema, Response >; diff --git a/api.planx.uk/modules/webhooks/routes.ts b/api.planx.uk/modules/webhooks/routes.ts index b5ff2405ac..ce24eaf123 100644 --- a/api.planx.uk/modules/webhooks/routes.ts +++ b/api.planx.uk/modules/webhooks/routes.ts @@ -14,34 +14,36 @@ import { sendSlackNotificationController, } from "./controller"; import { sendSlackNotificationSchema } from "./sendNotification/schema"; -import { CreatePaymentEventSchema } from "./paymentRequestEvents/schema"; +import { createPaymentEventSchema } from "./paymentRequestEvents/schema"; const router = Router(); router.use("/hasura", useHasuraAuth); -router.post("/hasura/create-reminder-event", createReminderEvent); -router.post("/hasura/create-expiry-event", createExpiryEvent); router.post( "/hasura/create-payment-invitation-events", - validate(CreatePaymentEventSchema), + validate(createPaymentEventSchema), createPaymentInvitationEventsController, ); router.post( "/hasura/create-payment-reminder-events", - validate(CreatePaymentEventSchema), + validate(createPaymentEventSchema), createPaymentReminderEventsController, ); router.post( "/hasura/create-payment-expiry-events", - validate(CreatePaymentEventSchema), + validate(createPaymentEventSchema), createPaymentExpiryEventsController, ); -router.post("/hasura/create-payment-send-events", createPaymentSendEvents); router.post( "/hasura/send-slack-notification", validate(sendSlackNotificationSchema), sendSlackNotificationController, ); + +// TODO: Convert these routes to the new API module structure +router.post("/hasura/create-payment-send-events", createPaymentSendEvents); +router.post("/hasura/create-reminder-event", createReminderEvent); +router.post("/hasura/create-expiry-event", createExpiryEvent); router.post("/hasura/sanitise-application-data", sanitiseApplicationData); export default router; From 7279188df51e1f69c0b2e0b97092ddf67744f13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Sat, 30 Sep 2023 22:37:58 +0100 Subject: [PATCH 09/14] docs: Add response type --- api.planx.uk/hasura/metadata/index.ts | 7 ++- .../inviteToPay/createPaymentSendEvents.ts | 11 ++-- api.planx.uk/modules/webhooks/docs.yaml | 18 +++++-- .../paymentRequestEvents/index.test.ts | 51 ++++++++++++++----- api.planx.uk/send/createSendEvents.ts | 11 ++-- 5 files changed, 74 insertions(+), 24 deletions(-) diff --git a/api.planx.uk/hasura/metadata/index.ts b/api.planx.uk/hasura/metadata/index.ts index c9f7e79364..14829616b7 100644 --- a/api.planx.uk/hasura/metadata/index.ts +++ b/api.planx.uk/hasura/metadata/index.ts @@ -25,13 +25,18 @@ type RequiredScheduledEventArgs = Pick< "webhook" | "schedule_at" | "comment" | "payload" >; +export interface ScheduledEventResponse { + message: "success"; + event_id: string; +} + /** * POST a request to the Hasura Metadata API * https://hasura.io/docs/latest/graphql/core/api-reference/metadata-api/index/ */ const postToMetadataAPI = async ( body: ScheduledEvent, -): Promise> => { +): Promise> => { try { return await Axios.post( process.env.HASURA_METADATA_URL!, diff --git a/api.planx.uk/inviteToPay/createPaymentSendEvents.ts b/api.planx.uk/inviteToPay/createPaymentSendEvents.ts index 3b34947ec1..7e2b7e05ef 100644 --- a/api.planx.uk/inviteToPay/createPaymentSendEvents.ts +++ b/api.planx.uk/inviteToPay/createPaymentSendEvents.ts @@ -3,7 +3,10 @@ import { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; import { $admin } from "../client"; import { adminGraphQLClient as adminClient } from "../hasura"; -import { createScheduledEvent } from "../hasura/metadata"; +import { + ScheduledEventResponse, + createScheduledEvent, +} from "../hasura/metadata"; import { getMostRecentPublishedFlow } from "../helpers"; import { Flow, Node, Team } from "../types"; @@ -14,9 +17,9 @@ enum Destination { } interface CombinedResponse { - bops?: Record; - uniform?: Record; - email?: Record; + bops?: ScheduledEventResponse; + uniform?: ScheduledEventResponse; + email?: ScheduledEventResponse; } // Create "One-off Scheduled Events" in Hasura when a payment request is paid diff --git a/api.planx.uk/modules/webhooks/docs.yaml b/api.planx.uk/modules/webhooks/docs.yaml index 2549552666..da352b665a 100644 --- a/api.planx.uk/modules/webhooks/docs.yaml +++ b/api.planx.uk/modules/webhooks/docs.yaml @@ -124,6 +124,18 @@ components: message: type: string required: true + ScheduledEvent: + content: + application/json: + schema: + type: object + properties: + message: + type: string + enum: ["success"] + event_id: + type: string + description: Internal Hasura ID of the generated event paths: /webhooks/hasura/sendSlackNotification: post: @@ -158,7 +170,7 @@ paths: $ref: "#/components/schemas/CreatePaymentEvent" responses: "200": - $ref: "#/components/responses/TODO" + $ref: "#/components/responses/ScheduledEvent" "500": $ref: "#/components/responses/ErrorMessage" /webhooks/hasura/create-payment-reminder-events: @@ -173,7 +185,7 @@ paths: $ref: "#/components/schemas/CreatePaymentEvent" responses: "200": - $ref: "#/components/responses/TODO" + $ref: "#/components/responses/ScheduledEvent" "500": $ref: "#/components/responses/ErrorMessage" /webhooks/hasura/create-payment-expiry-events: @@ -188,6 +200,6 @@ paths: $ref: "#/components/schemas/CreatePaymentEvent" responses: "200": - $ref: "#/components/responses/TODO" + $ref: "#/components/responses/ScheduledEvent" "500": $ref: "#/components/responses/ErrorMessage" \ No newline at end of file diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts index de5eb3b652..e12216ea99 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts @@ -10,6 +10,11 @@ const mockedCreateScheduledEvent = createScheduledEvent as jest.MockedFunction< typeof createScheduledEvent >; +const mockScheduledEventResponse = { + message: "success", + event_id: "abc123", +} as const; + describe("Create payment invitation events webhook", () => { const ENDPOINT = "/webhooks/hasura/create-payment-invitation-events"; @@ -46,7 +51,7 @@ describe("Create payment invitation events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -55,7 +60,10 @@ describe("Create payment invitation events webhook", () => { .then((response) => { // it's queued up x2 invitations: one for the payee and one for the agent expect(response.body).toHaveLength(2); - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); }); @@ -64,7 +72,7 @@ describe("Create payment invitation events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -72,7 +80,10 @@ describe("Create payment invitation events webhook", () => { .expect(200) .then((response) => { expect(response.body).toHaveLength(2); - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; @@ -149,7 +160,7 @@ describe("Create payment reminder events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -158,7 +169,12 @@ describe("Create payment reminder events webhook", () => { .then((response) => { // it's queued up x4 reminders: 2 for the payee and 2 for the agent at 7 days and 1 day from expiry expect(response.body).toHaveLength(4); - expect(response.body).toStrictEqual(["test", "test", "test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); }); @@ -167,7 +183,7 @@ describe("Create payment reminder events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -175,7 +191,12 @@ describe("Create payment reminder events webhook", () => { .expect(200) .then((response) => { expect(response.body).toHaveLength(4); - expect(response.body).toStrictEqual(["test", "test", "test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; @@ -270,7 +291,7 @@ describe("Create payment expiry events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -279,7 +300,10 @@ describe("Create payment expiry events webhook", () => { .then((response) => { // it's queued up x2 expiries: one for the payee and one for the agent expect(response.body).toHaveLength(2); - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); }); @@ -288,7 +312,7 @@ describe("Create payment expiry events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -296,7 +320,10 @@ describe("Create payment expiry events webhook", () => { .expect(200) .then((response) => { expect(response.body).toHaveLength(2); - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; diff --git a/api.planx.uk/send/createSendEvents.ts b/api.planx.uk/send/createSendEvents.ts index ed611c121f..8f5c12af4e 100644 --- a/api.planx.uk/send/createSendEvents.ts +++ b/api.planx.uk/send/createSendEvents.ts @@ -1,10 +1,13 @@ import { NextFunction, Request, Response } from "express"; -import { createScheduledEvent } from "../hasura/metadata"; +import { + ScheduledEventResponse, + createScheduledEvent, +} from "../hasura/metadata"; interface CombinedResponse { - bops?: Record; - uniform?: Record; - email?: Record; + bops?: ScheduledEventResponse; + uniform?: ScheduledEventResponse; + email?: ScheduledEventResponse; } // Create "One-off Scheduled Events" in Hasura from Send component for selected destinations From 409d2ca1dafee7a764fd6e15d8213551e07056bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 3 Oct 2023 10:39:20 +0100 Subject: [PATCH 10/14] fix: Tidy up --- .../createPaymentSendEvents.test.ts | 17 +++++++--- .../webhooks/_old/lowcalSessionEvents.test.ts | 27 ++++++++++----- api.planx.uk/modules/webhooks/docs.yaml | 18 +++++----- .../webhooks/paymentRequestEvents/schema.ts | 6 ++-- .../webhooks/sendNotification/index.test.ts | 34 +------------------ 5 files changed, 45 insertions(+), 57 deletions(-) diff --git a/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts b/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts index d9eac45774..cb75bf84b1 100644 --- a/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts +++ b/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts @@ -9,6 +9,11 @@ const mockedCreateScheduledEvent = createScheduledEvent as jest.MockedFunction< typeof createScheduledEvent >; +const mockScheduledEventResponse = { + message: "success", + event_id: "abc123", +} as const; + describe("Create payment send events webhook", () => { const ENDPOINT = "/webhooks/hasura/create-payment-send-events"; @@ -83,7 +88,7 @@ describe("Create payment send events webhook", () => { }); it("returns a 200 on successful event setup", async () => { - mockedCreateScheduledEvent.mockResolvedValue("test-event-id"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await supertest(app) .post(ENDPOINT) @@ -91,13 +96,15 @@ describe("Create payment send events webhook", () => { .send({ payload: { sessionId: "123" } }) .expect(200) .then((response) => { - expect(response.body).toMatchObject({ email: "test-event-id" }); + expect(response.body).toMatchObject({ + email: mockScheduledEventResponse, + }); }); }); it("passes the correct arguments along to createScheduledEvent", async () => { const body = { createdAt: new Date(), payload: { sessionId: "123" } }; - mockedCreateScheduledEvent.mockResolvedValue("test-event-id"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await supertest(app) .post(ENDPOINT) @@ -105,7 +112,9 @@ describe("Create payment send events webhook", () => { .send(body) .expect(200) .then((response) => { - expect(response.body).toMatchObject({ email: "test-event-id" }); + expect(response.body).toMatchObject({ + email: mockScheduledEventResponse, + }); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; diff --git a/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts b/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts index 0cddfe272b..677b8c2b3f 100644 --- a/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts +++ b/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts @@ -9,6 +9,11 @@ const mockedCreateScheduledEvent = createScheduledEvent as jest.MockedFunction< typeof createScheduledEvent >; +const mockScheduledEventResponse = { + message: "success", + event_id: "abc123", +} as const; + describe("Create reminder event webhook", () => { const ENDPOINT = "/webhooks/hasura/create-reminder-event"; @@ -41,7 +46,7 @@ describe("Create reminder event webhook", () => { it("returns a 200 on successful event setup", async () => { const body = { createdAt: new Date(), payload: { sessionId: "123" } }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -50,20 +55,26 @@ describe("Create reminder event webhook", () => { .then((response) => { // it's queued up x2 reminders for 7 days and 1 day from expiry expect(response.body).toHaveLength(2); - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); }); it("passes the correct arguments along to createScheduledEvent", async () => { const body = { createdAt: new Date(), payload: { sessionId: "123" } }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(200) .then((response) => { - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; @@ -129,27 +140,27 @@ describe("Create expiry event webhook", () => { it("returns a 200 on successful event setup", async () => { const body = { createdAt: new Date(), payload: { sessionId: "123" } }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(200) .then((response) => { - expect(response.body).toBe("test"); + expect(response.body).toStrictEqual(mockScheduledEventResponse); }); }); it("passes the correct arguments along to createScheduledEvent", async () => { const body = { createdAt: new Date(), payload: { sessionId: "123" } }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(200) .then((response) => { - expect(response.body).toBe("test"); + expect(response.body).toStrictEqual(mockScheduledEventResponse); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; expect(mockArgs.webhook).toBe("{{HASURA_PLANX_API_URL}}/send-email/expiry"); diff --git a/api.planx.uk/modules/webhooks/docs.yaml b/api.planx.uk/modules/webhooks/docs.yaml index da352b665a..158a8a71b7 100644 --- a/api.planx.uk/modules/webhooks/docs.yaml +++ b/api.planx.uk/modules/webhooks/docs.yaml @@ -128,14 +128,16 @@ components: content: application/json: schema: - type: object - properties: - message: - type: string - enum: ["success"] - event_id: - type: string - description: Internal Hasura ID of the generated event + type: array + items: + type: object + properties: + message: + type: string + enum: ["success"] + event_id: + type: string + description: Internal Hasura ID of the generated event paths: /webhooks/hasura/sendSlackNotification: post: diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts b/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts index 4ad55670e0..293f64a541 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts +++ b/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts @@ -1,8 +1,6 @@ import { z } from "zod"; import { ValidatedRequestHandler } from "../../../shared/middleware/validate"; - -// TODO: Make this better -type Response = [any, any] | any[]; +import { ScheduledEventResponse } from "../../../hasura/metadata"; export const createPaymentEventSchema = z.object({ body: z.object({ @@ -19,5 +17,5 @@ export type CreatePaymentEvent = z.infer< export type CreatePaymentEventController = ValidatedRequestHandler< typeof createPaymentEventSchema, - Response + ScheduledEventResponse[] >; diff --git a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts b/api.planx.uk/modules/webhooks/sendNotification/index.test.ts index 2eaa93685a..9ecf4244cd 100644 --- a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts +++ b/api.planx.uk/modules/webhooks/sendNotification/index.test.ts @@ -210,6 +210,7 @@ describe("Send Slack notifications endpoint", () => { expect(mockAdmin.session.find).toHaveBeenCalledTimes(1); expect(response.body.message).toBe("Posted to Slack"); expect(response.body.data).toMatch(/abc123/); + }); }); it("adds a status to the Slack message for a disability exemption", async () => { @@ -259,39 +260,6 @@ describe("Send Slack notifications endpoint", () => { }); }); - it("adds an exemption status if there's no fee for the session", async () => { - process.env.APP_ENVIRONMENT = "production"; - mockAdmin.session.find = jest - .fn() - .mockResolvedValue(mockSessionWithoutFee); - - await post(ENDPOINT) - .query({ type: "uniform-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(200) - .then((response) => { - expect(response.body.data).toMatch(/abc123/); - expect(response.body.data).toMatch(/test-council/); - expect(response.body.data).toMatch(/[Exempt]/); - }); - }); - - it("handles missing sessions", async () => { - process.env.APP_ENVIRONMENT = "production"; - mockAdmin.session.find = jest.fn().mockResolvedValueOnce(null); - - await post(ENDPOINT) - .query({ type: "uniform-submission" }) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(500) - .then((response) => { - expect(mockAdmin.session.find).toHaveBeenCalledTimes(1); - expect(response.body.error).toMatch(/Failed to send/); - }); - }); - it("returns error when Slack fails", async () => { process.env.APP_ENVIRONMENT = "production"; mockSend.mockRejectedValue("Fail!"); From 3665eef8c9764e4a8bf0542ddc96674b21efd54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 3 Oct 2023 11:27:41 +0100 Subject: [PATCH 11/14] feat: Session reminder and expiry events --- .../webhooks/_old/lowcalSessionEvents.ts | 76 ------------------- api.planx.uk/modules/webhooks/controller.ts | 35 +++++++++ api.planx.uk/modules/webhooks/docs.yaml | 45 ++++++++++- .../index.test.ts} | 26 ++++--- .../webhooks/lowcalSessionEvents/schema.ts | 21 +++++ .../webhooks/lowcalSessionEvents/service.ts | 44 +++++++++++ api.planx.uk/modules/webhooks/routes.ts | 19 +++-- 7 files changed, 173 insertions(+), 93 deletions(-) delete mode 100644 api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts rename api.planx.uk/modules/webhooks/{_old/lowcalSessionEvents.test.ts => lowcalSessionEvents/index.test.ts} (89%) create mode 100644 api.planx.uk/modules/webhooks/lowcalSessionEvents/schema.ts create mode 100644 api.planx.uk/modules/webhooks/lowcalSessionEvents/service.ts diff --git a/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts b/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts deleted file mode 100644 index 1d50affdc3..0000000000 --- a/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { addDays } from "date-fns"; -import { Request, Response, NextFunction } from "express"; - -import { createScheduledEvent } from "../../../hasura/metadata"; -import { - DAYS_UNTIL_EXPIRY, - REMINDER_DAYS_FROM_EXPIRY, -} from "../../../saveAndReturn/utils"; - -/** - * Create "reminder" events for a lowcal_session record - */ -const createReminderEvent = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { createdAt, payload } = req.body; - if (!createdAt || !payload) - return next({ - status: 400, - message: "Required value missing", - }); - const response = await Promise.all( - REMINDER_DAYS_FROM_EXPIRY.map((day: number) => - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/reminder", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY - day), - payload: payload, - comment: `reminder_${payload.sessionId}_${day}day`, - }), - ), - ); - res.json(response); - } catch (error) { - return next({ - error, - message: `Failed to create reminder event. Error: ${error}`, - }); - } -}; - -/** - * Create an "expiry" event for a lowcal_session record - */ -const createExpiryEvent = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { createdAt, payload } = req.body; - if (!createdAt || !payload) - return next({ - status: 400, - message: "Required value missing", - }); - const response = await createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/expiry", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY), - payload: payload, - comment: `expiry_${payload.sessionId}`, - }); - res.json(response); - } catch (error) { - return next({ - error, - message: `Failed to create expiry event. Error: ${ - (error as Error).message - }`, - }); - } -}; - -export { createReminderEvent, createExpiryEvent }; diff --git a/api.planx.uk/modules/webhooks/controller.ts b/api.planx.uk/modules/webhooks/controller.ts index 26b51612f3..c4e8e33bc2 100644 --- a/api.planx.uk/modules/webhooks/controller.ts +++ b/api.planx.uk/modules/webhooks/controller.ts @@ -1,4 +1,9 @@ import { ServerError } from "../../errors"; +import { CreateSessionEventController } from "./lowcalSessionEvents/schema"; +import { + createSessionExpiryEvent, + createSessionReminderEvent, +} from "./lowcalSessionEvents/service"; import { CreatePaymentEventController } from "./paymentRequestEvents/schema"; import { createPaymentExpiryEvents, @@ -80,3 +85,33 @@ export const createPaymentExpiryEventsController: CreatePaymentEventController = ); } }; + +export const createSessionReminderEventController: CreateSessionEventController = + async (req, res, next) => { + try { + const response = await createSessionReminderEvent(req.body); + res.json(response); + } catch (error) { + return next( + new ServerError({ + message: "Failed to create session reminder event", + cause: error, + }), + ); + } + }; + +export const createSessionExpiryEventController: CreateSessionEventController = + async (req, res, next) => { + try { + const response = await createSessionExpiryEvent(req.body); + res.json(response); + } catch (error) { + return next( + new ServerError({ + message: "Failed to create session expiry event", + cause: error, + }), + ); + } + }; diff --git a/api.planx.uk/modules/webhooks/docs.yaml b/api.planx.uk/modules/webhooks/docs.yaml index 158a8a71b7..40a89bd745 100644 --- a/api.planx.uk/modules/webhooks/docs.yaml +++ b/api.planx.uk/modules/webhooks/docs.yaml @@ -110,6 +110,19 @@ components: properties: paymentRequestId: type: string + CreateSessionEvent: + type: object + properties: + createdAt: + required: true + type: string + format: date-time + payload: + required: true + type: object + properties: + sessionId: + type: string responses: SlackNotificationSuccessMessage: content: @@ -204,4 +217,34 @@ paths: "200": $ref: "#/components/responses/ScheduledEvent" "500": - $ref: "#/components/responses/ErrorMessage" \ No newline at end of file + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-reminder-event: + post: + tags: ["webhooks"] + summary: Create session reminder event + description: Setup events which will trigger session reminder emails to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSessionEvent" + responses: + "200": + $ref: "#/components/responses/ScheduledEvent" + "500": + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-expiry-event: + post: + tags: ["webhooks"] + summary: Create session expiry event + description: Setup events which will trigger session expiry email to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSessionEvent" + responses: + "200": + $ref: "#/components/responses/ScheduledEvent" + "500": + $ref: "#/components/responses/ErrorMessage" diff --git a/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts b/api.planx.uk/modules/webhooks/lowcalSessionEvents/index.test.ts similarity index 89% rename from api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts rename to api.planx.uk/modules/webhooks/lowcalSessionEvents/index.test.ts index 677b8c2b3f..86bfdadc4b 100644 --- a/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts +++ b/api.planx.uk/modules/webhooks/lowcalSessionEvents/index.test.ts @@ -38,9 +38,10 @@ describe("Create reminder event webhook", () => { .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(400) - .then((response) => - expect(response.body.error).toEqual("Required value missing"), - ); + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); } }); @@ -103,7 +104,9 @@ describe("Create reminder event webhook", () => { .send(body) .expect(500) .then((response) => { - expect(response.body.error).toMatch(/Failed to create reminder event/); + expect(response.body.error).toMatch( + /Failed to create session reminder event/, + ); }); }); }); @@ -132,9 +135,10 @@ describe("Create expiry event webhook", () => { .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(400) - .then((response) => - expect(response.body.error).toEqual("Required value missing"), - ); + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); } }); @@ -147,7 +151,7 @@ describe("Create expiry event webhook", () => { .send(body) .expect(200) .then((response) => { - expect(response.body).toStrictEqual(mockScheduledEventResponse); + expect(response.body).toStrictEqual([mockScheduledEventResponse]); }); }); @@ -160,7 +164,7 @@ describe("Create expiry event webhook", () => { .send(body) .expect(200) .then((response) => { - expect(response.body).toStrictEqual(mockScheduledEventResponse); + expect(response.body).toStrictEqual([mockScheduledEventResponse]); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; expect(mockArgs.webhook).toBe("{{HASURA_PLANX_API_URL}}/send-email/expiry"); @@ -177,7 +181,9 @@ describe("Create expiry event webhook", () => { .send(body) .expect(500) .then((response) => { - expect(response.body.error).toMatch(/Failed to create expiry event/); + expect(response.body.error).toMatch( + /Failed to create session expiry event/, + ); }); }); }); diff --git a/api.planx.uk/modules/webhooks/lowcalSessionEvents/schema.ts b/api.planx.uk/modules/webhooks/lowcalSessionEvents/schema.ts new file mode 100644 index 0000000000..deed195cf8 --- /dev/null +++ b/api.planx.uk/modules/webhooks/lowcalSessionEvents/schema.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; +import { ValidatedRequestHandler } from "../../../shared/middleware/validate"; +import { ScheduledEventResponse } from "../../../hasura/metadata"; + +export const createSessionEventSchema = z.object({ + body: z.object({ + createdAt: z.string().transform((val) => new Date(val)), + payload: z.object({ + sessionId: z.string(), + }), + }), +}); + +export type CreateSessionEvent = z.infer< + typeof createSessionEventSchema +>["body"]; + +export type CreateSessionEventController = ValidatedRequestHandler< + typeof createSessionEventSchema, + ScheduledEventResponse[] +>; diff --git a/api.planx.uk/modules/webhooks/lowcalSessionEvents/service.ts b/api.planx.uk/modules/webhooks/lowcalSessionEvents/service.ts new file mode 100644 index 0000000000..357655864a --- /dev/null +++ b/api.planx.uk/modules/webhooks/lowcalSessionEvents/service.ts @@ -0,0 +1,44 @@ +import { addDays } from "date-fns"; + +import { createScheduledEvent } from "../../../hasura/metadata"; +import { + DAYS_UNTIL_EXPIRY, + REMINDER_DAYS_FROM_EXPIRY, +} from "../../../saveAndReturn/utils"; +import { CreateSessionEvent } from "./schema"; + +/** + * Create "reminder" events for a lowcal_session record + */ +export const createSessionReminderEvent = async ({ + createdAt, + payload, +}: CreateSessionEvent) => { + const response = await Promise.all( + REMINDER_DAYS_FROM_EXPIRY.map((day: number) => + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/reminder", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY - day), + payload: payload, + comment: `reminder_${payload.sessionId}_${day}day`, + }), + ), + ); + return response; +}; + +/** + * Create an "expiry" event for a lowcal_session record + */ +export const createSessionExpiryEvent = async ({ + createdAt, + payload, +}: CreateSessionEvent) => { + const response = await createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/expiry", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY), + payload: payload, + comment: `expiry_${payload.sessionId}`, + }); + return [response]; +}; diff --git a/api.planx.uk/modules/webhooks/routes.ts b/api.planx.uk/modules/webhooks/routes.ts index ce24eaf123..dff88ba2ea 100644 --- a/api.planx.uk/modules/webhooks/routes.ts +++ b/api.planx.uk/modules/webhooks/routes.ts @@ -2,19 +2,18 @@ import { Router } from "express"; import { useHasuraAuth } from "../auth/middleware"; import { createPaymentSendEvents } from "../../inviteToPay/createPaymentSendEvents"; import { sanitiseApplicationData } from "./_old/sanitiseApplicationData"; -import { - createExpiryEvent, - createReminderEvent, -} from "./_old/lowcalSessionEvents"; import { validate } from "../../shared/middleware/validate"; import { createPaymentExpiryEventsController, createPaymentInvitationEventsController, createPaymentReminderEventsController, + createSessionExpiryEventController, + createSessionReminderEventController, sendSlackNotificationController, } from "./controller"; import { sendSlackNotificationSchema } from "./sendNotification/schema"; import { createPaymentEventSchema } from "./paymentRequestEvents/schema"; +import { createSessionEventSchema } from "./lowcalSessionEvents/schema"; const router = Router(); @@ -39,11 +38,19 @@ router.post( validate(sendSlackNotificationSchema), sendSlackNotificationController, ); +router.post( + "/hasura/create-reminder-event", + validate(createSessionEventSchema), + createSessionReminderEventController, +); +router.post( + "/hasura/create-expiry-event", + validate(createSessionEventSchema), + createSessionExpiryEventController, +); // TODO: Convert these routes to the new API module structure router.post("/hasura/create-payment-send-events", createPaymentSendEvents); -router.post("/hasura/create-reminder-event", createReminderEvent); -router.post("/hasura/create-expiry-event", createExpiryEvent); router.post("/hasura/sanitise-application-data", sanitiseApplicationData); export default router; From d94b65ea9c028e328f8e1e1974585c55514bd5ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 3 Oct 2023 11:36:47 +0100 Subject: [PATCH 12/14] refactor: Move to /service folder --- api.planx.uk/modules/webhooks/controller.ts | 12 ++++++------ api.planx.uk/modules/webhooks/routes.ts | 6 +++--- .../{ => service}/lowcalSessionEvents/index.test.ts | 6 +++--- .../lowcalSessionEvents/index.ts} | 4 ++-- .../{ => service}/lowcalSessionEvents/schema.ts | 4 ++-- .../{ => service}/paymentRequestEvents/index.test.ts | 6 +++--- .../paymentRequestEvents/index.ts} | 4 ++-- .../{ => service}/paymentRequestEvents/schema.ts | 4 ++-- .../{ => service}/sendNotification/index.test.ts | 6 +++--- .../service.ts => service/sendNotification/index.ts} | 2 +- .../{ => service}/sendNotification/schema.ts | 0 .../webhooks/{ => service}/sendNotification/types.ts | 2 +- 12 files changed, 28 insertions(+), 28 deletions(-) rename api.planx.uk/modules/webhooks/{ => service}/lowcalSessionEvents/index.test.ts (97%) rename api.planx.uk/modules/webhooks/{lowcalSessionEvents/service.ts => service/lowcalSessionEvents/index.ts} (91%) rename api.planx.uk/modules/webhooks/{ => service}/lowcalSessionEvents/schema.ts (74%) rename api.planx.uk/modules/webhooks/{ => service}/paymentRequestEvents/index.test.ts (98%) rename api.planx.uk/modules/webhooks/{paymentRequestEvents/service.ts => service/paymentRequestEvents/index.ts} (96%) rename api.planx.uk/modules/webhooks/{ => service}/paymentRequestEvents/schema.ts (75%) rename api.planx.uk/modules/webhooks/{ => service}/sendNotification/index.test.ts (98%) rename api.planx.uk/modules/webhooks/{sendNotification/service.ts => service/sendNotification/index.ts} (97%) rename api.planx.uk/modules/webhooks/{ => service}/sendNotification/schema.ts (100%) rename api.planx.uk/modules/webhooks/{ => service}/sendNotification/types.ts (92%) diff --git a/api.planx.uk/modules/webhooks/controller.ts b/api.planx.uk/modules/webhooks/controller.ts index c4e8e33bc2..cfcb0b6ec8 100644 --- a/api.planx.uk/modules/webhooks/controller.ts +++ b/api.planx.uk/modules/webhooks/controller.ts @@ -1,17 +1,17 @@ import { ServerError } from "../../errors"; -import { CreateSessionEventController } from "./lowcalSessionEvents/schema"; +import { CreateSessionEventController } from "./service/lowcalSessionEvents/schema"; import { createSessionExpiryEvent, createSessionReminderEvent, -} from "./lowcalSessionEvents/service"; -import { CreatePaymentEventController } from "./paymentRequestEvents/schema"; +} from "./service/lowcalSessionEvents"; +import { SendSlackNotification } from "./service/sendNotification/types"; +import { sendSlackNotification } from "./service/sendNotification"; +import { CreatePaymentEventController } from "./service/paymentRequestEvents/schema"; import { createPaymentExpiryEvents, createPaymentInvitationEvents, createPaymentReminderEvents, -} from "./paymentRequestEvents/service"; -import { sendSlackNotification } from "./sendNotification/service"; -import { SendSlackNotification } from "./sendNotification/types"; +} from "./service/paymentRequestEvents"; export const sendSlackNotificationController: SendSlackNotification = async ( req, diff --git a/api.planx.uk/modules/webhooks/routes.ts b/api.planx.uk/modules/webhooks/routes.ts index dff88ba2ea..f7f286f185 100644 --- a/api.planx.uk/modules/webhooks/routes.ts +++ b/api.planx.uk/modules/webhooks/routes.ts @@ -11,9 +11,9 @@ import { createSessionReminderEventController, sendSlackNotificationController, } from "./controller"; -import { sendSlackNotificationSchema } from "./sendNotification/schema"; -import { createPaymentEventSchema } from "./paymentRequestEvents/schema"; -import { createSessionEventSchema } from "./lowcalSessionEvents/schema"; +import { sendSlackNotificationSchema } from "./service/sendNotification/schema"; +import { createPaymentEventSchema } from "./service/paymentRequestEvents/schema"; +import { createSessionEventSchema } from "./service/lowcalSessionEvents/schema"; const router = Router(); diff --git a/api.planx.uk/modules/webhooks/lowcalSessionEvents/index.test.ts b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.test.ts similarity index 97% rename from api.planx.uk/modules/webhooks/lowcalSessionEvents/index.test.ts rename to api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.test.ts index 86bfdadc4b..8f557a4686 100644 --- a/api.planx.uk/modules/webhooks/lowcalSessionEvents/index.test.ts +++ b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.test.ts @@ -1,10 +1,10 @@ import supertest from "supertest"; -import app from "../../../server"; -import { createScheduledEvent } from "../../../hasura/metadata"; +import app from "../../../../server"; +import { createScheduledEvent } from "../../../../hasura/metadata"; const { post } = supertest(app); -jest.mock("../../../hasura/metadata"); +jest.mock("../../../../hasura/metadata"); const mockedCreateScheduledEvent = createScheduledEvent as jest.MockedFunction< typeof createScheduledEvent >; diff --git a/api.planx.uk/modules/webhooks/lowcalSessionEvents/service.ts b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.ts similarity index 91% rename from api.planx.uk/modules/webhooks/lowcalSessionEvents/service.ts rename to api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.ts index 357655864a..b8ec4cf072 100644 --- a/api.planx.uk/modules/webhooks/lowcalSessionEvents/service.ts +++ b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.ts @@ -1,10 +1,10 @@ import { addDays } from "date-fns"; -import { createScheduledEvent } from "../../../hasura/metadata"; +import { createScheduledEvent } from "../../../../hasura/metadata"; import { DAYS_UNTIL_EXPIRY, REMINDER_DAYS_FROM_EXPIRY, -} from "../../../saveAndReturn/utils"; +} from "../../../../saveAndReturn/utils"; import { CreateSessionEvent } from "./schema"; /** diff --git a/api.planx.uk/modules/webhooks/lowcalSessionEvents/schema.ts b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts similarity index 74% rename from api.planx.uk/modules/webhooks/lowcalSessionEvents/schema.ts rename to api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts index deed195cf8..53e73f0b06 100644 --- a/api.planx.uk/modules/webhooks/lowcalSessionEvents/schema.ts +++ b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { ValidatedRequestHandler } from "../../../shared/middleware/validate"; -import { ScheduledEventResponse } from "../../../hasura/metadata"; +import { ValidatedRequestHandler } from "../../../../shared/middleware/validate"; +import { ScheduledEventResponse } from "../../../../hasura/metadata"; export const createSessionEventSchema = z.object({ body: z.object({ diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.test.ts similarity index 98% rename from api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts rename to api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.test.ts index e12216ea99..20beeefd98 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/index.test.ts +++ b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.test.ts @@ -1,11 +1,11 @@ import supertest from "supertest"; -import app from "../../../server"; -import { createScheduledEvent } from "../../../hasura/metadata"; +import app from "../../../../server"; +import { createScheduledEvent } from "../../../../hasura/metadata"; import { CreatePaymentEvent } from "./schema"; const { post } = supertest(app); -jest.mock("../../../hasura/metadata"); +jest.mock("../../../../hasura/metadata"); const mockedCreateScheduledEvent = createScheduledEvent as jest.MockedFunction< typeof createScheduledEvent >; diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.ts similarity index 96% rename from api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts rename to api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.ts index d60822d036..9679c7931e 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/service.ts +++ b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.ts @@ -1,10 +1,10 @@ import { addDays } from "date-fns"; -import { createScheduledEvent } from "../../../hasura/metadata"; +import { createScheduledEvent } from "../../../../hasura/metadata"; import { DAYS_UNTIL_EXPIRY, REMINDER_DAYS_FROM_EXPIRY, -} from "../../../saveAndReturn/utils"; +} from "../../../../saveAndReturn/utils"; import { CreatePaymentEvent } from "./schema"; /** diff --git a/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts similarity index 75% rename from api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts rename to api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts index 293f64a541..5b7a48404c 100644 --- a/api.planx.uk/modules/webhooks/paymentRequestEvents/schema.ts +++ b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { ValidatedRequestHandler } from "../../../shared/middleware/validate"; -import { ScheduledEventResponse } from "../../../hasura/metadata"; +import { ValidatedRequestHandler } from "../../../../shared/middleware/validate"; +import { ScheduledEventResponse } from "../../../../hasura/metadata"; export const createPaymentEventSchema = z.object({ body: z.object({ diff --git a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts b/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts similarity index 98% rename from api.planx.uk/modules/webhooks/sendNotification/index.test.ts rename to api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts index 9ecf4244cd..d3a4d25a54 100644 --- a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts @@ -1,8 +1,8 @@ import supertest from "supertest"; -import app from "../../../server"; +import app from "../../../../server"; import SlackNotify from "slack-notify"; import { BOPSBody, EmailBody, UniformBody } from "./types"; -import { $admin } from "../../../client"; +import { $admin } from "../../../../client"; import { CoreDomainClient } from "@opensystemslab/planx-core"; const mockSessionWithFee = { @@ -35,7 +35,7 @@ const mockSessionWithResubmissionExemption = { }, }; -jest.mock("../../../client"); +jest.mock("../../../../client"); const mockAdmin = jest.mocked($admin); const mockSend = jest.fn(); diff --git a/api.planx.uk/modules/webhooks/sendNotification/service.ts b/api.planx.uk/modules/webhooks/service/sendNotification/index.ts similarity index 97% rename from api.planx.uk/modules/webhooks/sendNotification/service.ts rename to api.planx.uk/modules/webhooks/service/sendNotification/index.ts index be8678a0e9..e0dfd36947 100644 --- a/api.planx.uk/modules/webhooks/sendNotification/service.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification/index.ts @@ -7,7 +7,7 @@ import { EventType, UniformEventData, } from "./types"; -import { $admin } from "../../../client"; +import { $admin } from "../../../../client"; export const sendSlackNotification = async ( data: EventData, diff --git a/api.planx.uk/modules/webhooks/sendNotification/schema.ts b/api.planx.uk/modules/webhooks/service/sendNotification/schema.ts similarity index 100% rename from api.planx.uk/modules/webhooks/sendNotification/schema.ts rename to api.planx.uk/modules/webhooks/service/sendNotification/schema.ts diff --git a/api.planx.uk/modules/webhooks/sendNotification/types.ts b/api.planx.uk/modules/webhooks/service/sendNotification/types.ts similarity index 92% rename from api.planx.uk/modules/webhooks/sendNotification/types.ts rename to api.planx.uk/modules/webhooks/service/sendNotification/types.ts index cd1e293bf9..1575f45429 100644 --- a/api.planx.uk/modules/webhooks/sendNotification/types.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification/types.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { ValidatedRequestHandler } from "../../../shared/middleware/validate"; +import { ValidatedRequestHandler } from "../../../../shared/middleware/validate"; import { bopsSubmissionSchema, emailSubmissionSchema, From 80da1b40fbe32b086936cddbf29788ed0a0b5b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 3 Oct 2023 16:24:47 +0100 Subject: [PATCH 13/14] feat: Sanitise application data endpoint --- api.planx.uk/modules/webhooks/controller.ts | 18 +++++++++++ api.planx.uk/modules/webhooks/docs.yaml | 26 +++++++++++++++ api.planx.uk/modules/webhooks/routes.ts | 9 ++++-- .../sanitiseApplicationData/index.test.ts | 6 ++-- .../sanitiseApplicationData/index.ts | 32 +++++-------------- .../sanitiseApplicationData/mocks/queries.ts | 0 .../operations.test.ts | 0 .../sanitiseApplicationData/operations.ts | 0 .../sanitiseApplicationData/types.ts} | 8 +++++ 9 files changed, 70 insertions(+), 29 deletions(-) rename api.planx.uk/modules/webhooks/{_old => service}/sanitiseApplicationData/index.test.ts (96%) rename api.planx.uk/modules/webhooks/{_old => service}/sanitiseApplicationData/index.ts (61%) rename api.planx.uk/modules/webhooks/{_old => service}/sanitiseApplicationData/mocks/queries.ts (100%) rename api.planx.uk/modules/webhooks/{_old => service}/sanitiseApplicationData/operations.test.ts (100%) rename api.planx.uk/modules/webhooks/{_old => service}/sanitiseApplicationData/operations.ts (100%) rename api.planx.uk/modules/webhooks/{_old/sanitiseApplicationData/types.d.ts => service/sanitiseApplicationData/types.ts} (53%) diff --git a/api.planx.uk/modules/webhooks/controller.ts b/api.planx.uk/modules/webhooks/controller.ts index cfcb0b6ec8..1621159778 100644 --- a/api.planx.uk/modules/webhooks/controller.ts +++ b/api.planx.uk/modules/webhooks/controller.ts @@ -12,6 +12,8 @@ import { createPaymentInvitationEvents, createPaymentReminderEvents, } from "./service/paymentRequestEvents"; +import { SanitiseApplicationData } from "./service/sanitiseApplicationData/types"; +import { sanitiseApplicationData } from "./service/sanitiseApplicationData"; export const sendSlackNotificationController: SendSlackNotification = async ( req, @@ -115,3 +117,19 @@ export const createSessionExpiryEventController: CreateSessionEventController = ); } }; + +export const sanitiseApplicationDataController: SanitiseApplicationData = + async (_req, res, next) => { + try { + const { operationFailed, results } = await sanitiseApplicationData(); + if (operationFailed) res.status(500); + return res.json(results); + } catch (error) { + return next( + new ServerError({ + message: "Failed to sanitise application data", + cause: error, + }), + ); + } + }; diff --git a/api.planx.uk/modules/webhooks/docs.yaml b/api.planx.uk/modules/webhooks/docs.yaml index 40a89bd745..5d8a2d0d2f 100644 --- a/api.planx.uk/modules/webhooks/docs.yaml +++ b/api.planx.uk/modules/webhooks/docs.yaml @@ -151,6 +151,22 @@ components: event_id: type: string description: Internal Hasura ID of the generated event + OperationResult: + type: object + properties: + operationName: + type: string + status: + type: string + enum: + - success + - failure + count: + type: integer + required: false + errorMessage: + type: string + required: false paths: /webhooks/hasura/sendSlackNotification: post: @@ -248,3 +264,13 @@ paths: $ref: "#/components/responses/ScheduledEvent" "500": $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/sanitise-application-data: + post: + tags: ["webhooks"] + summary: Sanitise application data + description: Called by Hasura cron job "sanitise_application_data" on a nightly basis + responses: + "200": + $ref: "#/components/responses/OperationResult" + "500": + $ref: "#/components/responses/OperationResult" diff --git a/api.planx.uk/modules/webhooks/routes.ts b/api.planx.uk/modules/webhooks/routes.ts index f7f286f185..dc7c1bbbcd 100644 --- a/api.planx.uk/modules/webhooks/routes.ts +++ b/api.planx.uk/modules/webhooks/routes.ts @@ -1,7 +1,6 @@ import { Router } from "express"; import { useHasuraAuth } from "../auth/middleware"; import { createPaymentSendEvents } from "../../inviteToPay/createPaymentSendEvents"; -import { sanitiseApplicationData } from "./_old/sanitiseApplicationData"; import { validate } from "../../shared/middleware/validate"; import { createPaymentExpiryEventsController, @@ -9,6 +8,7 @@ import { createPaymentReminderEventsController, createSessionExpiryEventController, createSessionReminderEventController, + sanitiseApplicationDataController, sendSlackNotificationController, } from "./controller"; import { sendSlackNotificationSchema } from "./service/sendNotification/schema"; @@ -48,9 +48,12 @@ router.post( validate(createSessionEventSchema), createSessionExpiryEventController, ); +router.post( + "/hasura/sanitise-application-data", + sanitiseApplicationDataController, +); -// TODO: Convert these routes to the new API module structure +// TODO: Convert to the new API module structure router.post("/hasura/create-payment-send-events", createPaymentSendEvents); -router.post("/hasura/sanitise-application-data", sanitiseApplicationData); export default router; diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.test.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.test.ts similarity index 96% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.test.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.test.ts index 96da6ca4b3..a4930c9178 100644 --- a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.test.ts +++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.test.ts @@ -30,13 +30,15 @@ describe("Sanitise application data webhook", () => { it("returns a 500 if an unhandled error is thrown whilst running operations", async () => { const mockOperationHandler = jest.spyOn(operations, "operationHandler"); - mockOperationHandler.mockRejectedValueOnce(new Error("Unhandled error!")); + mockOperationHandler.mockRejectedValueOnce("Unhandled error!"); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .expect(500) .then((response) => - expect(response.body.error).toMatch(/Unhandled error!/), + expect(response.body.error).toMatch( + /Failed to sanitise application data/, + ), ); }); diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.ts similarity index 61% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.ts index 9454104edf..af9cecf7c3 100644 --- a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.ts +++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.ts @@ -9,35 +9,19 @@ import { getFormattedEnvironment } from "../../../../helpers"; * Called by Hasura cron job `sanitise_application_data` on a nightly basis * See hasura.planx.uk/metadata/cron_triggers.yaml */ -export const sanitiseApplicationData = async ( - _req: Request, - res: Response, - next: NextFunction, -): Promise => { +export const sanitiseApplicationData = async () => { const operations = getOperations(); const results: OperationResult[] = []; - try { - for (const operation of operations) { - const result = await operationHandler(operation); - results.push(result); - } - } catch (error) { - // Unhandled error, flag with Airbrake - return next({ - error, - message: `Failed to sanitise application data. ${ - (error as Error).message - }`, - }); - } - const operationFailed = results.find((result) => result.status === "failure"); - if (operationFailed) { - await postToSlack(results); - res.status(500); + for (const operation of operations) { + const result = await operationHandler(operation); + results.push(result); } - return res.json(results); + const operationFailed = results.some((result) => result.status === "failure"); + if (operationFailed) await postToSlack(results); + + return { operationFailed, results }; }; export const postToSlack = async (results: OperationResult[]) => { diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/mocks/queries.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/mocks/queries.ts similarity index 100% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/mocks/queries.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/mocks/queries.ts diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.test.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts similarity index 100% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.test.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.ts similarity index 100% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.ts diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/types.d.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/types.ts similarity index 53% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/types.d.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/types.ts index 55bd0d8bc5..88b9df2804 100644 --- a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/types.d.ts +++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/types.ts @@ -1,3 +1,6 @@ +import { z } from "zod"; +import { ValidatedRequestHandler } from "../../../../shared/middleware/validate"; + export interface OperationResult { operationName: string; status: "processing" | "success" | "failure"; @@ -8,3 +11,8 @@ export interface OperationResult { export type QueryResult = string[]; export type Operation = () => Promise; + +export type SanitiseApplicationData = ValidatedRequestHandler< + z.ZodUndefined, + OperationResult[] +>; From f7f3701ea0eade606ed7a5801ecd3b47ef408fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 3 Oct 2023 17:04:11 +0100 Subject: [PATCH 14/14] fix: Tidy up swagger docs --- api.planx.uk/modules/webhooks/docs.yaml | 132 ++++++++++++++---------- 1 file changed, 76 insertions(+), 56 deletions(-) diff --git a/api.planx.uk/modules/webhooks/docs.yaml b/api.planx.uk/modules/webhooks/docs.yaml index 5d8a2d0d2f..b200378244 100644 --- a/api.planx.uk/modules/webhooks/docs.yaml +++ b/api.planx.uk/modules/webhooks/docs.yaml @@ -151,22 +151,42 @@ components: event_id: type: string description: Internal Hasura ID of the generated event - OperationResult: - type: object - properties: - operationName: - type: string - status: - type: string - enum: - - success - - failure - count: - type: integer - required: false - errorMessage: - type: string - required: false + OperationResultSuccess: + content: + application/json: + schema: + type: array + items: + type: object + properties: + operationName: + type: string + status: + type: string + enum: + - success + count: + type: integer + OperationResultFailure: + content: + application/json: + schema: + type: array + items: + type: object + properties: + operationName: + type: string + status: + type: string + enum: + - success + - failure + count: + type: integer + errorMessage: + type: string + required: false paths: /webhooks/hasura/sendSlackNotification: post: @@ -234,43 +254,43 @@ paths: $ref: "#/components/responses/ScheduledEvent" "500": $ref: "#/components/responses/ErrorMessage" - /webhooks/hasura/create-reminder-event: - post: - tags: ["webhooks"] - summary: Create session reminder event - description: Setup events which will trigger session reminder emails to be generated - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CreateSessionEvent" - responses: - "200": - $ref: "#/components/responses/ScheduledEvent" - "500": - $ref: "#/components/responses/ErrorMessage" - /webhooks/hasura/create-expiry-event: - post: - tags: ["webhooks"] - summary: Create session expiry event - description: Setup events which will trigger session expiry email to be generated - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CreateSessionEvent" - responses: - "200": - $ref: "#/components/responses/ScheduledEvent" - "500": - $ref: "#/components/responses/ErrorMessage" - /webhooks/hasura/sanitise-application-data: - post: - tags: ["webhooks"] - summary: Sanitise application data - description: Called by Hasura cron job "sanitise_application_data" on a nightly basis - responses: - "200": - $ref: "#/components/responses/OperationResult" - "500": - $ref: "#/components/responses/OperationResult" + /webhooks/hasura/create-reminder-event: + post: + tags: ["webhooks"] + summary: Create session reminder event + description: Setup events which will trigger session reminder emails to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSessionEvent" + responses: + "200": + $ref: "#/components/responses/ScheduledEvent" + "500": + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-expiry-event: + post: + tags: ["webhooks"] + summary: Create session expiry event + description: Setup events which will trigger session expiry email to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSessionEvent" + responses: + "200": + $ref: "#/components/responses/ScheduledEvent" + "500": + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/sanitise-application-data: + post: + tags: ["webhooks"] + summary: Sanitise application data + description: Called by Hasura cron job "sanitise_application_data" on a nightly basis + responses: + "200": + $ref: "#/components/responses/OperationResultSuccess" + "500": + $ref: "#/components/responses/OperationResultFailure"