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] 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;