diff --git a/api.planx.uk/modules/send/bops/bops.test.ts b/api.planx.uk/modules/send/bops/bops.test.ts index 3342e08b19..76dfe8f22e 100644 --- a/api.planx.uk/modules/send/bops/bops.test.ts +++ b/api.planx.uk/modules/send/bops/bops.test.ts @@ -37,7 +37,7 @@ describe(`sending an application to BOPS (v2)`, () => { bopsApplications: [], }, variables: { - session_id: "123", + session_id: "37e3649b-4854-4249-a5c3-7937c1b952b9", search_string: "%/api/v2/planning_applications", }, }); @@ -93,7 +93,7 @@ describe(`sending an application to BOPS (v2)`, () => { await supertest(app) .post("/bops/southwark") .set({ Authorization: process.env.HASURA_PLANX_API_KEY! }) - .send({ payload: { sessionId: "123" } }) + .send({ payload: { sessionId: "37e3649b-4854-4249-a5c3-7937c1b952b9" } }) .expect(200) .then((res) => { expect(res.body).toEqual({ @@ -108,7 +108,7 @@ describe(`sending an application to BOPS (v2)`, () => { it("requires auth", async () => { await supertest(app) .post("/bops/southwark") - .send({ payload: { sessionId: "123" } }) + .send({ payload: { sessionId: "37e3649b-4854-4249-a5c3-7937c1b952b9" } }) .expect(401); }); @@ -119,7 +119,8 @@ describe(`sending an application to BOPS (v2)`, () => { .send({ payload: null }) .expect(400) .then((res) => { - expect(res.body.error).toMatch(/Missing application/); + expect(res.body).toHaveProperty("issues"); + expect(res.body).toHaveProperty("name", "ZodError"); }); }); @@ -127,7 +128,7 @@ describe(`sending an application to BOPS (v2)`, () => { await supertest(app) .post("/bops/unsupported-team") .set({ Authorization: process.env.HASURA_PLANX_API_KEY! }) - .send({ payload: { sessionId: "123" } }) + .send({ payload: { sessionId: "37e3649b-4854-4249-a5c3-7937c1b952b9" } }) .expect(500) .then((res) => { expect(res.body.error).toMatch( @@ -137,6 +138,9 @@ describe(`sending an application to BOPS (v2)`, () => { }); it("does not re-send an application which has already been submitted", async () => { + const previouslySubmittedApplicationID = + "07e4dd0d-ec3c-48f5-adc7-aee90116f6c7"; + queryMock.mockQuery({ name: "FindApplication", data: { @@ -145,7 +149,7 @@ describe(`sending an application to BOPS (v2)`, () => { ], }, variables: { - session_id: "previously_submitted_app", + session_id: previouslySubmittedApplicationID, search_string: "%/api/v2/planning_applications", }, }); @@ -153,11 +157,11 @@ describe(`sending an application to BOPS (v2)`, () => { await supertest(app) .post("/bops/southwark") .set({ Authorization: process.env.HASURA_PLANX_API_KEY! }) - .send({ payload: { sessionId: "previously_submitted_app" } }) + .send({ payload: { sessionId: previouslySubmittedApplicationID } }) .expect(200) .then((res) => { expect(res.body).toEqual({ - sessionId: "previously_submitted_app", + sessionId: previouslySubmittedApplicationID, bopsId: "bops_app_id", message: "Skipping send, already successfully submitted", }); diff --git a/api.planx.uk/modules/send/bops/bops.ts b/api.planx.uk/modules/send/bops/bops.ts index 334d9f06df..e706711fb7 100644 --- a/api.planx.uk/modules/send/bops/bops.ts +++ b/api.planx.uk/modules/send/bops/bops.ts @@ -1,16 +1,10 @@ import type { AxiosResponse } from "axios"; import axios from "axios"; import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils.js"; -import type { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; import { $api } from "../../../client/index.js"; import { ServerError } from "../../../errors/index.js"; - -interface SendToBOPSRequest { - payload: { - sessionId: string; - }; -} +import type { SendIntegrationController } from "../types.js"; interface CreateBopsApplication { insertBopsApplication: { @@ -19,23 +13,16 @@ interface CreateBopsApplication { }; } -const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { - // `/bops/:localAuthority` is only called via Hasura's scheduled event webhook now, so body is wrapped in a "payload" key - const { payload }: SendToBOPSRequest = req.body; - if (!payload) { - return next( - new ServerError({ - status: 400, - message: `Missing application payload data to send to BOPS`, - }), - ); - } +const sendToBOPS: SendIntegrationController = async (_req, res, next) => { + const { + payload: { sessionId }, + } = res.locals.parsedReq.body; // confirm that this session has not already been successfully submitted before proceeding - const submittedApp = await checkBOPSAuditTable(payload?.sessionId, "v2"); + const submittedApp = await checkBOPSAuditTable(sessionId, "v2"); if (submittedApp?.message === "Application created") { return res.status(200).send({ - sessionId: payload?.sessionId, + sessionId, bopsId: submittedApp?.id, message: `Skipping send, already successfully submitted`, }); @@ -44,7 +31,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { // confirm this local authority (aka team) is supported by BOPS before creating the proxy // a local or staging API instance should send to the BOPS staging endpoint // production should send to the BOPS production endpoint - const localAuthority = req.params.localAuthority; + const localAuthority = res.locals.parsedReq.params.localAuthority; const env = process.env.APP_ENVIRONMENT === "production" ? "production" : "staging"; @@ -54,9 +41,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { encryptionKey: process.env.ENCRYPTION_KEY!, env, }); - const exportData = await $api.export.digitalPlanningDataPayload( - payload?.sessionId, - ); + const exportData = await $api.export.digitalPlanningDataPayload(sessionId); const bopsResponse = await axios({ method: "POST", @@ -70,7 +55,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { }) .then(async (res: AxiosResponse<{ id: string }>) => { // Mark session as submitted so that reminder and expiry emails are not triggered - markSessionAsSubmitted(payload?.sessionId); + markSessionAsSubmitted(sessionId); const applicationId = await $api.client.request( gql` @@ -105,7 +90,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { request: exportData, response: res.data, response_headers: res.headers, - session_id: payload?.sessionId, + session_id: sessionId, }, ); @@ -119,14 +104,14 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { .catch((error) => { if (error.response) { throw new Error( - `Sending to BOPS v2 failed (${[localAuthority, payload?.sessionId] + `Sending to BOPS v2 failed (${[localAuthority, sessionId] .filter(Boolean) .join(" - ")}):\n${JSON.stringify(error.response.data, null, 2)}`, ); } else { // re-throw other errors throw new Error( - `Sending to BOPS v2 failed (${[localAuthority, payload?.sessionId] + `Sending to BOPS v2 failed (${[localAuthority, sessionId] .filter(Boolean) .join(" - ")}):\n${error}`, ); @@ -137,10 +122,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { next( new ServerError({ status: 500, - message: `Sending to BOPS v2 failed (${[ - localAuthority, - payload?.sessionId, - ] + message: `Sending to BOPS v2 failed (${[localAuthority, sessionId] .filter(Boolean) .join(" - ")}):\n${err}`, cause: err, diff --git a/api.planx.uk/modules/send/email/index.test.ts b/api.planx.uk/modules/send/email/index.test.ts index d5b9fcad84..f5e17e5231 100644 --- a/api.planx.uk/modules/send/email/index.test.ts +++ b/api.planx.uk/modules/send/email/index.test.ts @@ -64,7 +64,7 @@ describe(`sending an application by email to a planning office`, () => { data: { session: { data: {} }, }, - variables: { id: "123" }, + variables: { id: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" }, matchOnVariables: true, }); @@ -77,16 +77,16 @@ describe(`sending an application by email to a planning office`, () => { flow: { slug: "test-flow", name: "Test Flow" }, }, }, - variables: { id: "123" }, + variables: { id: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" }, }); queryMock.mockQuery({ name: "MarkSessionAsSubmitted", matchOnVariables: false, data: { - session: { id: "123" }, + session: { id: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" }, }, - variables: { sessionId: "123" }, + variables: { sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" }, }); queryMock.mockQuery({ @@ -96,7 +96,7 @@ describe(`sending an application by email to a planning office`, () => { application: { id: 1 }, }, variables: { - sessionId: "123", + sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49", teamSlug: "southwark", recipient: "planning.office.example@council.gov.uk", request: { @@ -116,7 +116,7 @@ describe(`sending an application by email to a planning office`, () => { await supertest(app) .post("/email-submission/southwark") .set({ Authorization: process.env.HASURA_PLANX_API_KEY! }) - .send({ payload: { sessionId: "123" } }) + .send({ payload: { sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" } }) .expect(200) .then((res) => { expect(res.body).toEqual({ @@ -130,7 +130,7 @@ describe(`sending an application by email to a planning office`, () => { it("fails without authorization header", async () => { await supertest(app) .post("/email-submission/southwark") - .send({ payload: { sessionId: "123" } }) + .send({ payload: { sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" } }) .expect(401); }); @@ -138,12 +138,13 @@ describe(`sending an application by email to a planning office`, () => { await supertest(app) .post("/email-submission/southwark") .set({ Authorization: process.env.HASURA_PLANX_API_KEY! }) - .send({ payload: { somethingElse: "123" } }) + .send({ + payload: { somethingElse: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" }, + }) .expect(400) .then((res) => { - expect(res.body).toEqual({ - error: "Missing application payload data to send to email", - }); + expect(res.body).toHaveProperty("issues"); + expect(res.body).toHaveProperty("name", "ZodError"); }); }); @@ -167,7 +168,7 @@ describe(`sending an application by email to a planning office`, () => { await supertest(app) .post("/email-submission/other-council") .set({ Authorization: process.env.HASURA_PLANX_API_KEY! }) - .send({ payload: { sessionId: "123" } }) + .send({ payload: { sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" } }) .expect(400) .then((res) => { expect(res.body).toEqual({ @@ -184,13 +185,13 @@ describe(`sending an application by email to a planning office`, () => { data: { session: null, }, - variables: { id: "123" }, + variables: { id: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" }, }); await supertest(app) .post("/email-submission/other-council") .set({ Authorization: process.env.HASURA_PLANX_API_KEY! }) - .send({ payload: { sessionId: "123" } }) + .send({ payload: { sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" } }) .expect(500) .then((res) => { expect(res.body.error).toMatch(/Cannot find session/); @@ -221,7 +222,7 @@ describe(`downloading application data received by email`, () => { data: { session: { data: { passport: { test: "dummy data" } } }, }, - variables: { id: "123" }, + variables: { id: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" }, }); }); @@ -274,12 +275,12 @@ describe(`downloading application data received by email`, () => { it("calls addTemplateFilesToZip()", async () => { await supertest(app) .get( - "/download-application-files/123?email=planning.office.example@council.gov.uk&localAuthority=southwark", + "/download-application-files/33d373d4-fff2-4ef7-a5f2-2a36e39ccc49?email=planning.office.example@council.gov.uk&localAuthority=southwark", ) .expect(200) .then((_res) => { expect(mockBuildSubmissionExportZip).toHaveBeenCalledWith({ - sessionId: "123", + sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49", includeDigitalPlanningJSON: true, }); }); diff --git a/api.planx.uk/modules/send/email/index.ts b/api.planx.uk/modules/send/email/index.ts index ce7a28dc95..5e5e08f990 100644 --- a/api.planx.uk/modules/send/email/index.ts +++ b/api.planx.uk/modules/send/email/index.ts @@ -1,30 +1,24 @@ -import type { NextFunction, Request, Response } from "express"; import { sendEmail } from "../../../lib/notify/index.js"; import type { EmailSubmissionNotifyConfig } from "../../../types.js"; import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils.js"; +import type { SendIntegrationController } from "../types.js"; import { getSessionEmailDetailsById, getTeamEmailSettings, insertAuditEntry, } from "./service.js"; -export async function sendToEmail( - req: Request, - res: Response, - next: NextFunction, -) { +export const sendToEmail: SendIntegrationController = async ( + req, + res, + next, +) => { req.setTimeout(120 * 1000); // Temporary bump to address submission timeouts - // `/email-submission/:localAuthority` is only called via Hasura's scheduled event webhook, so body is wrapped in a "payload" key - const { payload } = req.body; - const localAuthority = req.params.localAuthority; - - if (!payload?.sessionId) { - return next({ - status: 400, - message: `Missing application payload data to send to email`, - }); - } + const { + payload: { sessionId }, + } = res.locals.parsedReq.body; + const localAuthority = res.locals.parsedReq.params.localAuthority; try { // Confirm this local authority (aka team) has an email configured in teams.submission_email @@ -37,16 +31,16 @@ export async function sendToEmail( } // Get the applicant email and flow slug associated with the session - const { email, flow } = await getSessionEmailDetailsById(payload.sessionId); + const { email, flow } = await getSessionEmailDetailsById(sessionId); const flowName = flow.name; // Prepare email template const config: EmailSubmissionNotifyConfig = { personalisation: { serviceName: flowName, - sessionId: payload.sessionId, + sessionId, applicantEmail: email, - downloadLink: `${process.env.API_URL_EXT}/download-application-files/${payload.sessionId}?email=${teamSettings.submissionEmail}&localAuthority=${localAuthority}`, + downloadLink: `${process.env.API_URL_EXT}/download-application-files/${sessionId}?email=${teamSettings.submissionEmail}&localAuthority=${localAuthority}`, ...teamSettings, }, }; @@ -59,11 +53,11 @@ export async function sendToEmail( ); // Mark session as submitted so that reminder and expiry emails are not triggered - markSessionAsSubmitted(payload.sessionId); + markSessionAsSubmitted(sessionId); // Create audit table entry, which triggers a Slack notification on `insert` if production insertAuditEntry( - payload.sessionId, + sessionId, localAuthority, teamSettings.submissionEmail, config, @@ -83,4 +77,4 @@ export async function sendToEmail( }`, }); } -} +}; diff --git a/api.planx.uk/modules/send/idox/nexus.test.ts b/api.planx.uk/modules/send/idox/nexus.test.ts index c5c0872de2..b5157f36e5 100644 --- a/api.planx.uk/modules/send/idox/nexus.test.ts +++ b/api.planx.uk/modules/send/idox/nexus.test.ts @@ -16,9 +16,8 @@ describe(`sending an application to Idox Nexus`, () => { .send({ payload: { somethingElse: "123" } }) .expect(400) .then((res) => { - expect(res.body).toEqual({ - error: "Missing application data to send to Idox Nexus", - }); + expect(res.body).toHaveProperty("issues"); + expect(res.body).toHaveProperty("name", "ZodError"); }); }); }); diff --git a/api.planx.uk/modules/send/idox/nexus.ts b/api.planx.uk/modules/send/idox/nexus.ts index 758e1a41b1..7851537562 100644 --- a/api.planx.uk/modules/send/idox/nexus.ts +++ b/api.planx.uk/modules/send/idox/nexus.ts @@ -1,6 +1,5 @@ import type { AxiosRequestConfig } from "axios"; import axios, { isAxiosError } from "axios"; -import type { NextFunction, Request, Response } from "express"; import FormData from "form-data"; import fs from "fs"; import { gql } from "graphql-request"; @@ -9,6 +8,10 @@ import { Buffer } from "node:buffer"; import { $api } from "../../../client/index.js"; import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils.js"; import { buildSubmissionExportZip } from "../utils/exportZip.js"; +import type { + SendIntegrationController, + SendIntegrationPayload, +} from "../types.js"; interface IdoxNexusClient { clientId: string; @@ -40,15 +43,11 @@ interface UniformApplication { created_at: string; } -interface SendToIdoxNexusPayload { - sessionId: string; -} - -export async function sendToIdoxNexus( - req: Request, - res: Response, - next: NextFunction, -) { +export const sendToIdoxNexus: SendIntegrationController = async ( + req, + res, + next, +) => { /** * Submits application data to Idox's Submission API (aka Nexus) * @@ -58,21 +57,14 @@ export async function sendToIdoxNexus( */ req.setTimeout(120 * 1000); // Temporary bump to address submission timeouts - // `/idox/:localAuthority` is only called via Hasura's scheduled event webhook now, so body is wrapped in a "payload" key - const payload: SendToIdoxNexusPayload = req.body.payload; - if (!payload?.sessionId) { - return next({ - status: 400, - message: "Missing application data to send to Idox Nexus", - }); - } - + const { payload } = res.locals.parsedReq.body; // localAuthority is only parsed for audit record, not client-specific - const localAuthority = req.params.localAuthority; + const localAuthority = res.locals.parsedReq.params.localAuthority; + const idoxNexusClient = getIdoxNexusClient(); // confirm that this session has not already been successfully submitted before proceeding - const submittedApp = await checkUniformAuditTable(payload?.sessionId); + const submittedApp = await checkUniformAuditTable(payload.sessionId); const _isAlreadySubmitted = submittedApp?.submissionStatus === "PENDING" && submittedApp?.canDownload; // if (isAlreadySubmitted) { @@ -135,7 +127,7 @@ export async function sendToIdoxNexus( }); // Mark session as submitted so that reminder and expiry emails are not triggered - markSessionAsSubmitted(payload?.sessionId); + markSessionAsSubmitted(payload.sessionId); return res.status(200).send({ message: `Successfully created an Idox Nexus submission (${randomOrgId} - ${randomOrg})`, @@ -151,7 +143,7 @@ export async function sendToIdoxNexus( message: `Failed to send to Idox Nexus (${payload.sessionId}): ${errorMessage}`, }); } -} +}; /** * Query the Uniform audit table to see if we already have an application for this session @@ -377,7 +369,7 @@ const createUniformApplicationAuditRecord = async ({ submissionDetails, }: { idoxSubmissionId: string; - payload: SendToIdoxNexusPayload; + payload: SendIntegrationPayload; localAuthority: string; submissionDetails: UniformSubmissionResponse; }): Promise => { diff --git a/api.planx.uk/modules/send/routes.ts b/api.planx.uk/modules/send/routes.ts index bcec19196b..33ab4c8ce3 100644 --- a/api.planx.uk/modules/send/routes.ts +++ b/api.planx.uk/modules/send/routes.ts @@ -9,6 +9,7 @@ import { combinedEventsPayloadSchema } from "./createSendEvents/types.js"; import { downloadApplicationFiles } from "./downloadApplicationFiles/index.js"; import { sendToS3 } from "./s3/index.js"; import { sendToIdoxNexus } from "./idox/nexus.js"; +import { sendIntegrationSchema } from "./types.js"; const router = Router(); @@ -17,11 +18,37 @@ router.post( validate(combinedEventsPayloadSchema), createSendEvents, ); -router.post("/bops/:localAuthority", useHasuraAuth, sendToBOPS); -router.post("/uniform/:localAuthority", useHasuraAuth, sendToUniform); -router.post("/idox/:localAuthority", useHasuraAuth, sendToIdoxNexus); -router.post("/email-submission/:localAuthority", useHasuraAuth, sendToEmail); +router.post( + "/bops/:localAuthority", + useHasuraAuth, + validate(sendIntegrationSchema), + sendToBOPS, +); +router.post( + "/uniform/:localAuthority", + useHasuraAuth, + validate(sendIntegrationSchema), + sendToUniform, +); +router.post( + "/idox/:localAuthority", + useHasuraAuth, + validate(sendIntegrationSchema), + sendToIdoxNexus, +); +router.post( + "/email-submission/:localAuthority", + useHasuraAuth, + validate(sendIntegrationSchema), + sendToEmail, +); +router.post( + "/upload-submission/:localAuthority", + useHasuraAuth, + validate(sendIntegrationSchema), + sendToS3, +); + router.get("/download-application-files/:sessionId", downloadApplicationFiles); -router.post("/upload-submission/:localAuthority", useHasuraAuth, sendToS3); export default router; diff --git a/api.planx.uk/modules/send/s3/index.test.ts b/api.planx.uk/modules/send/s3/index.test.ts index 01baa00632..5570fb7205 100644 --- a/api.planx.uk/modules/send/s3/index.test.ts +++ b/api.planx.uk/modules/send/s3/index.test.ts @@ -77,7 +77,8 @@ describe(`uploading an application to S3`, () => { .send({ payload: null }) .expect(400) .then((res) => { - expect(res.body.error).toMatch(/Missing application payload/); + expect(res.body).toHaveProperty("issues"); + expect(res.body).toHaveProperty("name", "ZodError"); }); }); @@ -85,7 +86,9 @@ describe(`uploading an application to S3`, () => { await supertest(app) .post("/upload-submission/unsupported-team") .set({ Authorization: process.env.HASURA_PLANX_API_KEY! }) - .send({ payload: { sessionId: "123" } }) + .send({ + payload: { sessionId: "3188f052-a032-4755-be63-72b0ba497eb6" }, + }) .expect(500) .then((res) => { expect(res.body.error).toMatch( diff --git a/api.planx.uk/modules/send/s3/index.ts b/api.planx.uk/modules/send/s3/index.ts index ccd2d2ccd9..6f7186ae8b 100644 --- a/api.planx.uk/modules/send/s3/index.ts +++ b/api.planx.uk/modules/send/s3/index.ts @@ -1,6 +1,5 @@ import type { AxiosRequestConfig } from "axios"; import axios from "axios"; -import type { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; import { $api } from "../../../client/index.js"; @@ -8,6 +7,7 @@ import type { Passport } from "../../../types.js"; import { uploadPrivateFile } from "../../file/service/uploadFile.js"; import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils.js"; import { isApplicationTypeSupported } from "../utils/helpers.js"; +import type { SendIntegrationController } from "../types.js"; interface CreateS3Application { insertS3Application: { @@ -15,23 +15,15 @@ interface CreateS3Application { }; } -const sendToS3 = async (req: Request, res: Response, next: NextFunction) => { - // `/upload-submission/:localAuthority` is only called via Hasura's scheduled event webhook, so body is wrapped in a "payload" key - const { payload } = req.body; - const localAuthority = req.params.localAuthority; +const sendToS3: SendIntegrationController = async (_req, res, next) => { + const { + payload: { sessionId }, + } = res.locals.parsedReq.body; + const localAuthority = res.locals.parsedReq.params.localAuthority; const env = process.env.APP_ENVIRONMENT === "production" ? "production" : "staging"; - if (!payload?.sessionId) { - return next({ - status: 400, - message: `Missing application payload data to send to email`, - }); - } - try { - const { sessionId } = payload; - // Fetch integration credentials for this team const { powerAutomateWebhookURL, powerAutomateAPIKey } = await $api.team.getIntegrations({ diff --git a/api.planx.uk/modules/send/types.ts b/api.planx.uk/modules/send/types.ts new file mode 100644 index 0000000000..fbf2694d4c --- /dev/null +++ b/api.planx.uk/modules/send/types.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; +import type { ValidatedRequestHandler } from "../../shared/middleware/validate.js"; + +const payloadSchema = z.object({ + sessionId: z.string().uuid(), +}); + +export type SendIntegrationPayload = z.infer; + +export const sendIntegrationSchema = z.object({ + body: z.object({ + payload: payloadSchema, + }), + params: z.object({ + localAuthority: z.string(), + }), +}); + +export type SendIntegrationController = ValidatedRequestHandler< + typeof sendIntegrationSchema, + unknown +>; diff --git a/api.planx.uk/modules/send/uniform/uniform.test.ts b/api.planx.uk/modules/send/uniform/uniform.test.ts index 32ffea8812..e698ac31f2 100644 --- a/api.planx.uk/modules/send/uniform/uniform.test.ts +++ b/api.planx.uk/modules/send/uniform/uniform.test.ts @@ -16,9 +16,8 @@ describe(`sending an application to uniform`, () => { .send({ payload: { somethingElse: "123" } }) .expect(400) .then((res) => { - expect(res.body).toEqual({ - error: "Missing application data to send to Uniform", - }); + expect(res.body).toHaveProperty("issues"); + expect(res.body).toHaveProperty("name", "ZodError"); }); }); }); diff --git a/api.planx.uk/modules/send/uniform/uniform.ts b/api.planx.uk/modules/send/uniform/uniform.ts index 795f69cd8c..557ec35f06 100644 --- a/api.planx.uk/modules/send/uniform/uniform.ts +++ b/api.planx.uk/modules/send/uniform/uniform.ts @@ -1,6 +1,5 @@ import type { AxiosRequestConfig } from "axios"; import axios, { isAxiosError } from "axios"; -import type { NextFunction, Request, Response } from "express"; import FormData from "form-data"; import fs from "fs"; import { gql } from "graphql-request"; @@ -9,6 +8,10 @@ import { Buffer } from "node:buffer"; import { $api } from "../../../client/index.js"; import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils.js"; import { buildSubmissionExportZip } from "../utils/exportZip.js"; +import type { + SendIntegrationController, + SendIntegrationPayload, +} from "../types.js"; interface UniformClient { clientId: string; @@ -40,15 +43,11 @@ interface UniformApplication { created_at: string; } -interface SendToUniformPayload { - sessionId: string; -} - -export async function sendToUniform( - req: Request, - res: Response, - next: NextFunction, -) { +export const sendToUniform: SendIntegrationController = async ( + req, + res, + next, +) => { /** * Submits application data to Uniform * @@ -58,16 +57,9 @@ export async function sendToUniform( */ req.setTimeout(120 * 1000); // Temporary bump to address submission timeouts - // `/uniform/:localAuthority` is only called via Hasura's scheduled event webhook now, so body is wrapped in a "payload" key - const payload: SendToUniformPayload = req.body.payload; - if (!payload?.sessionId) { - return next({ - status: 400, - message: "Missing application data to send to Uniform", - }); - } + const { payload } = res.locals.parsedReq.body; - const localAuthority = req.params.localAuthority; + const localAuthority = res.locals.parsedReq.params.localAuthority; const uniformClient = getUniformClient(localAuthority); if (!uniformClient) { @@ -78,7 +70,7 @@ export async function sendToUniform( } // confirm that this session has not already been successfully submitted before proceeding - const submittedApp = await checkUniformAuditTable(payload?.sessionId); + const submittedApp = await checkUniformAuditTable(payload.sessionId); const isAlreadySubmitted = submittedApp?.submissionStatus === "PENDING" && submittedApp?.canDownload; if (isAlreadySubmitted) { @@ -128,7 +120,7 @@ export async function sendToUniform( }); // Mark session as submitted so that reminder and expiry emails are not triggered - markSessionAsSubmitted(payload?.sessionId); + markSessionAsSubmitted(payload.sessionId); return res.status(200).send({ message: `Successfully created a Uniform submission`, @@ -144,7 +136,7 @@ export async function sendToUniform( message: `Failed to send to Uniform (${localAuthority}): ${errorMessage}`, }); } -} +}; /** * Query the Uniform audit table to see if we already have an application for this session @@ -369,7 +361,7 @@ const createUniformApplicationAuditRecord = async ({ submissionDetails, }: { idoxSubmissionId: string; - payload: SendToUniformPayload; + payload: SendIntegrationPayload; localAuthority: string; submissionDetails: UniformSubmissionResponse; }): Promise => {