From 0b9c0362f27ece29b46cad6dc4c44ee413892e10 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Sun, 3 Dec 2023 12:26:32 +0100 Subject: [PATCH] add endpoint to submit odp schema to bops --- api.planx.uk/lib/hasura/metadata/index.ts | 1 + api.planx.uk/modules/send/bops/bops.test.ts | 97 +++++++++++- api.planx.uk/modules/send/bops/bops.ts | 142 +++++++++++++++++- .../send/createSendEvents/controller.ts | 8 + api.planx.uk/modules/send/routes.ts | 3 +- .../src/@planx/components/Send/Public.tsx | 12 +- 6 files changed, 254 insertions(+), 9 deletions(-) diff --git a/api.planx.uk/lib/hasura/metadata/index.ts b/api.planx.uk/lib/hasura/metadata/index.ts index dcc8d11ffc..e9a7b7eac5 100644 --- a/api.planx.uk/lib/hasura/metadata/index.ts +++ b/api.planx.uk/lib/hasura/metadata/index.ts @@ -11,6 +11,7 @@ interface ScheduledEvent { export interface CombinedResponse { bops?: ScheduledEventResponse; + bops_v2?: ScheduledEventResponse; uniform?: ScheduledEventResponse; email?: ScheduledEventResponse; } diff --git a/api.planx.uk/modules/send/bops/bops.test.ts b/api.planx.uk/modules/send/bops/bops.test.ts index 4f44b0bbe3..a65eb91065 100644 --- a/api.planx.uk/modules/send/bops/bops.test.ts +++ b/api.planx.uk/modules/send/bops/bops.test.ts @@ -36,7 +36,7 @@ describe(`sending an application to BOPS`, () => { data: { bopsApplications: [], }, - variables: { session_id: "123" }, + variables: { session_id: "123", search_string: "%/api/v1/planning_applications" }, }); queryMock.mockQuery({ @@ -102,7 +102,7 @@ describe(`sending an application to BOPS`, () => { { response: { message: "Application created", id: "bops_app_id" } }, ], }, - variables: { session_id: "previously_submitted_app" }, + variables: { session_id: "previously_submitted_app", search_string: "%/api/v1/planning_applications" }, }); await supertest(app) @@ -119,3 +119,96 @@ describe(`sending an application to BOPS`, () => { }); }); }); + +describe(`sending an application to BOPS v2`, () => { + beforeEach(() => { + queryMock.mockQuery({ + name: "FindApplication", + data: { + bopsApplications: [], + }, + variables: { session_id: "123", search_string: "%/api/v2/planning_applications" }, + }); + + queryMock.mockQuery({ + name: "CreateBopsApplication", + matchOnVariables: false, + data: { + insertBopsApplication: { id: 22 }, + }, + }); + }); + + it.todo("throws an error if payload is invalid"); + + it.skip("proxies request and returns hasura id", async () => { + nock(`${submissionURL}/api/v2/planning_applications`).post("").reply(200, { + application: "0000123", + }); + + await supertest(app) + .post("/bops-v2/southwark") + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send({ payload: { sessionId: "123" } }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ + application: { id: 22, bopsResponse: { application: "0000123" } }, + }); + }); + }); + + it("requires auth", async () => { + await supertest(app) + .post("/bops-v2/southwark") + .send({ payload: { sessionId: "123" } }) + .expect(401); + }); + + it("throws an error if payload is missing", async () => { + await supertest(app) + .post("/bops-v2/southwark") + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send({ payload: null }) + .expect(400) + .then((res) => { + expect(res.body.error).toMatch(/Missing application/); + }); + }); + + it("throws an error if team is unsupported", async () => { + await supertest(app) + .post("/bops-v2/unsupported-team") + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send({ payload: { sessionId: "123" } }) + .expect(400) + .then((res) => { + expect(res.body.error).toMatch(/not enabled for this local authority/); + }); + }); + + it("does not re-send an application which has already been submitted", async () => { + queryMock.mockQuery({ + name: "FindApplication", + data: { + bopsApplications: [ + { response: { message: "Application created", id: "bops_app_id" } }, + ], + }, + variables: { session_id: "previously_submitted_app", search_string: "%/api/v2/planning_applications" }, + }); + + await supertest(app) + .post("/bops-v2/southwark") + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send({ payload: { sessionId: "previously_submitted_app" } }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ + sessionId: "previously_submitted_app", + 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 61014b50a3..03aa4f04a9 100644 --- a/api.planx.uk/modules/send/bops/bops.ts +++ b/api.planx.uk/modules/send/bops/bops.ts @@ -17,6 +17,7 @@ interface CreateBopsApplication { bopsId: string; }; } + 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; @@ -30,7 +31,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { } // confirm that this session has not already been successfully submitted before proceeding - const submittedApp = await checkBOPSAuditTable(payload?.sessionId); + const submittedApp = await checkBOPSAuditTable(payload?.sessionId, "v1"); if (submittedApp?.message === "Application created") { return res.status(200).send({ sessionId: payload?.sessionId, @@ -144,6 +145,133 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { } }; +const sendToBOPSV2 = 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`, + }), + ); + } + + // confirm that this session has not already been successfully submitted before proceeding + const submittedApp = await checkBOPSAuditTable(payload?.sessionId, "v2"); + if (submittedApp?.message === "Application created") { + return res.status(200).send({ + sessionId: payload?.sessionId, + bopsId: submittedApp?.id, + message: `Skipping send, already successfully submitted`, + }); + } + + // 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 bopsSubmissionURLEnvName = `BOPS_SUBMISSION_URL_${localAuthority.toUpperCase()}`; + const bopsSubmissionURL = process.env[bopsSubmissionURLEnvName]; + const isSupported = Boolean(bopsSubmissionURL); + if (!isSupported) { + return next( + new ServerError({ + status: 400, + message: `Back-office Planning System (BOPS) is not enabled for this local authority (${localAuthority})`, + }), + ); + } + const target = `${bopsSubmissionURL}/api/v2/planning_applications`; + const exportData = await $api.export.digitalPlanningDataPayload(payload?.sessionId); + + try { + const bopsResponse = await axios({ + method: "POST", + url: target, + adapter: "http", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.BOPS_API_TOKEN}`, + }, + data: exportData, + }) + .then(async (res: AxiosResponse<{ id: string }>) => { + // Mark session as submitted so that reminder and expiry emails are not triggered + markSessionAsSubmitted(payload?.sessionId); + + const applicationId = await $api.client.request( + gql` + mutation CreateBopsApplication( + $bops_id: String = "" + $destination_url: String! + $request: jsonb! + $req_headers: jsonb = {} + $response: jsonb = {} + $response_headers: jsonb = {} + $session_id: String! + ) { + insertBopsApplication: insert_bops_applications_one( + object: { + bops_id: $bops_id + destination_url: $destination_url + request: $request + req_headers: $req_headers + response: $response + response_headers: $response_headers + session_id: $session_id + } + ) { + id + bopsId: bops_id + } + } + `, + { + bops_id: res.data.id, + destination_url: target, + request: exportData, + response: res.data, + response_headers: res.headers, + session_id: payload?.sessionId, + }, + ); + + return { + application: { + ...applicationId.insertBopsApplication, + bopsResponse: res.data, + }, + }; + }) + .catch((error) => { + if (error.response) { + throw new Error( + `Sending to BOPS v2 failed (${localAuthority}):\n${JSON.stringify( + error.response.data, + null, + 2, + )}`, + ); + } else { + // re-throw other errors + throw new Error( + `Sending to BOPS v2 failed (${localAuthority}):\n${error}`, + ); + } + }); + res.send(bopsResponse); + } catch (err) { + next( + new ServerError({ + status: 500, + message: `Sending to BOPS v2 failed (${localAuthority})`, + cause: err, + }), + ); + } +}; + interface FindApplication { bopsApplications: { response: Record; @@ -155,12 +283,17 @@ interface FindApplication { */ async function checkBOPSAuditTable( sessionId: string, + version: string, ): Promise> { + const searchString = `%/api/${version}/planning_applications`; const application = await $api.client.request( gql` - query FindApplication($session_id: String = "") { + query FindApplication($session_id: String = "", $search_string: String) { bopsApplications: bops_applications( - where: { session_id: { _eq: $session_id } } + where: { + session_id: { _eq: $session_id }, + destination_url: {_like: $search_string } + } order_by: { created_at: desc } ) { response @@ -169,10 +302,11 @@ async function checkBOPSAuditTable( `, { session_id: sessionId, + search_string: searchString, }, ); return application?.bopsApplications[0]?.response; } -export { sendToBOPS }; +export { sendToBOPS, sendToBOPSV2 }; diff --git a/api.planx.uk/modules/send/createSendEvents/controller.ts b/api.planx.uk/modules/send/createSendEvents/controller.ts index 54f7af0ed7..d4ace3277f 100644 --- a/api.planx.uk/modules/send/createSendEvents/controller.ts +++ b/api.planx.uk/modules/send/createSendEvents/controller.ts @@ -35,6 +35,14 @@ const createSendEvents: CreateSendEventsController = async ( comment: `bops_submission_${sessionId}`, }); combinedResponse["bops"] = bopsEvent; + + const bopsV2Event = await createScheduledEvent({ + webhook: `{{HASURA_PLANX_API_URL}}/bops-v2/${bops.localAuthority}`, + schedule_at: new Date(now.getTime() + 45 * 1000), + payload: bops.body, + comment: `bops_v2_submission_${sessionId}`, + }); + combinedResponse["bops_v2"] = bopsV2Event; } if (uniform) { diff --git a/api.planx.uk/modules/send/routes.ts b/api.planx.uk/modules/send/routes.ts index 73f5974263..f17462070c 100644 --- a/api.planx.uk/modules/send/routes.ts +++ b/api.planx.uk/modules/send/routes.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import { createSendEvents } from "./createSendEvents/controller"; import { useHasuraAuth } from "../auth/middleware"; -import { sendToBOPS } from "./bops/bops"; +import { sendToBOPS, sendToBOPSV2 } from "./bops/bops"; import { sendToUniform } from "./uniform/uniform"; import { sendToEmail } from "./email"; import { validate } from "../../shared/middleware/validate"; @@ -16,6 +16,7 @@ router.post( createSendEvents, ); router.post("/bops/:localAuthority", useHasuraAuth, sendToBOPS); +router.post("/bops-v2/:localAuthority", useHasuraAuth, sendToBOPSV2); router.post("/uniform/:localAuthority", useHasuraAuth, sendToUniform); router.post("/email-submission/:localAuthority", useHasuraAuth, sendToEmail); router.get("/download-application-files/:sessionId", downloadApplicationFiles); diff --git a/editor.planx.uk/src/@planx/components/Send/Public.tsx b/editor.planx.uk/src/@planx/components/Send/Public.tsx index 898b1672f4..780cd47d04 100644 --- a/editor.planx.uk/src/@planx/components/Send/Public.tsx +++ b/editor.planx.uk/src/@planx/components/Send/Public.tsx @@ -75,9 +75,17 @@ const SendEvents: React.FC = ({ isReady && props.handleSubmit ) { - props.handleSubmit( - makeData(props, request.value.bops?.event_id, "bopsSendEventId"), + const v1 = makeData( + props, + request.value.bops?.event_id, + "bopsSendEventId", + ); + const v2 = makeData( + props, + request.value.bops_v2?.event_id, + "bopsV2SendEventId", ); + props.handleSubmit({ ...v1, ...v2 }); } if (