diff --git a/api.planx.uk/modules/send/email/index.ts b/api.planx.uk/modules/send/email/index.ts index 13f1066bd4..48e5d5f680 100644 --- a/api.planx.uk/modules/send/email/index.ts +++ b/api.planx.uk/modules/send/email/index.ts @@ -1,8 +1,7 @@ import type { NextFunction, Request, Response } from "express"; -import capitalize from "lodash/capitalize"; -import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils"; import { sendEmail } from "../../../lib/notify"; import { EmailSubmissionNotifyConfig } from "../../../types"; +import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils"; import { getSessionEmailDetailsById, getTeamEmailSettings, diff --git a/api.planx.uk/modules/send/s3/index.ts b/api.planx.uk/modules/send/s3/index.ts index 39aed61780..93e7311cf8 100644 --- a/api.planx.uk/modules/send/s3/index.ts +++ b/api.planx.uk/modules/send/s3/index.ts @@ -1,6 +1,7 @@ -import axios from "axios"; +import axios, { AxiosRequestConfig } from "axios"; import type { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; + import { $api } from "../../../client"; import { Passport } from "../../../types"; import { uploadPrivateFile } from "../../file/service/uploadFile"; @@ -60,8 +61,8 @@ const sendToS3 = async (req: Request, res: Response, next: NextFunction) => { `${sessionId}.json`, ); - // Send a notification with the file URL to the Power Automate webook - const webhookRequest = { + // Send a notification with the file URL to the Power Automate webhook + const webhookRequest: AxiosRequestConfig = { method: "POST", url: powerAutomateWebhookURL, adapter: "http", @@ -76,11 +77,8 @@ const sendToS3 = async (req: Request, res: Response, next: NextFunction) => { payload: doValidation ? "Validated ODP Schema" : "Discretionary", }, }; - let webhookResponseStatus: number | undefined; - await axios(webhookRequest) + const webhookResponse = await axios(webhookRequest) .then(async (res) => { - webhookResponseStatus = res.status; - // Mark session as submitted so that reminder and expiry emails are not triggered markSessionAsSubmitted(sessionId); @@ -114,9 +112,8 @@ const sendToS3 = async (req: Request, res: Response, next: NextFunction) => { ); return { - application: { - ...applicationId.insertS3Application, - }, + id: applicationId.insertS3Application?.id, + axiosResponse: res, }; }) .catch((error) => { @@ -125,10 +122,11 @@ const sendToS3 = async (req: Request, res: Response, next: NextFunction) => { ); }); - return res.status(200).send({ + res.status(200).send({ message: `Successfully uploaded submission to S3: ${fileUrl}`, payload: doValidation ? "Validated ODP Schema" : "Discretionary", - webhookResponse: webhookResponseStatus, + webhookResponse: webhookResponse.axiosResponse.status, + auditEntryId: webhookResponse.id, }); } catch (error) { return next({ diff --git a/api.planx.uk/modules/webhooks/docs.yaml b/api.planx.uk/modules/webhooks/docs.yaml index 320afb9f27..b9e0e26050 100644 --- a/api.planx.uk/modules/webhooks/docs.yaml +++ b/api.planx.uk/modules/webhooks/docs.yaml @@ -92,11 +92,33 @@ components: type: string required: - body + S3Submission: + type: object + properties: + body: + type: object + properties: + event: + type: object + properties: + data: + type: object + properties: + new: + type: object + properties: + session_id: + type: string + team_slug: + type: string + required: + - body SendSlackNotification: oneOf: - $ref: "#/components/schemas/BopsSubmission" - $ref: "#/components/schemas/UniformSubmission" - $ref: "#/components/schemas/EmailSubmission" + - $ref: "#/components/schemas/S3Submission" CreatePaymentEvent: type: object properties: diff --git a/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts b/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts index 18da1e4c94..433f8dc21b 100644 --- a/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts @@ -1,7 +1,7 @@ import supertest from "supertest"; import app from "../../../../server"; import SlackNotify from "slack-notify"; -import { BOPSBody, EmailBody, UniformBody } from "./types"; +import { BOPSBody, EmailBody, S3Body, UniformBody } from "./types"; import { $api } from "../../../../client"; import { CoreDomainClient } from "@opensystemslab/planx-core"; @@ -340,4 +340,63 @@ describe("Send Slack notifications endpoint", () => { }); }); }); + + describe("S3 notifications", () => { + const body: S3Body = { + event: { + data: { + new: { + session_id: "s3-pa-123", + team_slug: "test-team", + }, + }, + }, + }; + + it("skips the staging environment", async () => { + process.env.APP_ENVIRONMENT = "staging"; + await post(ENDPOINT) + .query({ type: "s3-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"; + + await post(ENDPOINT) + .query({ type: "s3-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(/s3-pa-123/); + expect(response.body.data).toMatch(/test-team/); + }); + }); + + it("returns error when Slack fails", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockSend.mockRejectedValue("Fail!"); + + await post(ENDPOINT) + .query({ type: "s3-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/index.ts b/api.planx.uk/modules/webhooks/service/sendNotification/index.ts index 8d70f6a84b..c35bc225cf 100644 --- a/api.planx.uk/modules/webhooks/service/sendNotification/index.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification/index.ts @@ -5,6 +5,7 @@ import { EmailEventData, EventData, EventType, + S3EventData, UniformEventData, } from "./types"; import { $api } from "../../../../client"; @@ -41,6 +42,11 @@ const getMessageForEventType = (data: EventData, type: EventType) => { const { request, session_id, team_slug } = data as EmailEventData; return `New email submission "${request.personalisation.serviceName}" *${session_id}* [${team_slug}]`; } + + if (type === "s3-submission") { + const { session_id, team_slug } = data as S3EventData; + return `New S3 + Power Automate submission *${session_id}* [${team_slug}]`; + } }; const getSessionIdFromEvent = (data: EventData, type: EventType) => @@ -48,6 +54,7 @@ const getSessionIdFromEvent = (data: EventData, type: EventType) => "bops-submission": (data as BOPSEventData).session_id, "uniform-submission": (data as UniformEventData).payload?.sessionId, "email-submission": (data as EmailEventData).session_id, + "s3-submission": (data as S3EventData).session_id, })[type]; const getExemptionStatusesForSession = async (sessionId: string) => { diff --git a/api.planx.uk/modules/webhooks/service/sendNotification/schema.ts b/api.planx.uk/modules/webhooks/service/sendNotification/schema.ts index 545a4fb220..0ecf85ccf5 100644 --- a/api.planx.uk/modules/webhooks/service/sendNotification/schema.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification/schema.ts @@ -59,8 +59,25 @@ export const emailSubmissionSchema = z.object({ }), }); +export const s3SubmissionSchema = z.object({ + body: z.object({ + event: z.object({ + data: z.object({ + new: z.object({ + session_id: z.string(), + team_slug: z.string(), + }), + }), + }), + }), + query: z.object({ + type: z.literal("s3-submission"), + }), +}); + export const sendSlackNotificationSchema = z.union([ bopsSubmissionSchema, uniformSubmissionSchema, emailSubmissionSchema, + s3SubmissionSchema, ]); diff --git a/api.planx.uk/modules/webhooks/service/sendNotification/types.ts b/api.planx.uk/modules/webhooks/service/sendNotification/types.ts index 1575f45429..5373cebb5f 100644 --- a/api.planx.uk/modules/webhooks/service/sendNotification/types.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification/types.ts @@ -3,6 +3,7 @@ import { ValidatedRequestHandler } from "../../../../shared/middleware/validate" import { bopsSubmissionSchema, emailSubmissionSchema, + s3SubmissionSchema, sendSlackNotificationSchema, uniformSubmissionSchema, } from "./schema"; @@ -25,7 +26,14 @@ 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 S3Body = z.infer["body"]; +export type S3EventData = S3Body["event"]["data"]["new"]; + +export type EventData = + | BOPSEventData + | UniformEventData + | EmailEventData + | S3EventData; export type SendSlackNotification = ValidatedRequestHandler< typeof sendSlackNotificationSchema, diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index 0242083d56..09c260266e 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -1444,6 +1444,27 @@ filter: {} check: null comment: "" + event_triggers: + - name: setup_s3_applications_notifications + definition: + enable_manual: false + insert: + columns: '*' + retry_conf: + interval_sec: 30 + num_retries: 1 + timeout_sec: 60 + webhook: '{{HASURA_PLANX_API_URL}}' + headers: + - name: authorization + value_from_env: HASURA_PLANX_API_KEY + request_transform: + method: POST + query_params: + type: s3-submission + template_engine: Kriti + url: '{{$base_url}}/webhooks/hasura/send-slack-notification' + version: 2 - table: name: sessions schema: public