Skip to content

Commit

Permalink
add endpoint to submit odp schema to bops
Browse files Browse the repository at this point in the history
  • Loading branch information
jessicamcinchak committed Dec 3, 2023
1 parent c5c5214 commit 0b9c036
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 9 deletions.
1 change: 1 addition & 0 deletions api.planx.uk/lib/hasura/metadata/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ScheduledEvent {

export interface CombinedResponse {
bops?: ScheduledEventResponse;
bops_v2?: ScheduledEventResponse;
uniform?: ScheduledEventResponse;
email?: ScheduledEventResponse;
}
Expand Down
97 changes: 95 additions & 2 deletions api.planx.uk/modules/send/bops/bops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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)
Expand All @@ -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",
});
});
});
});
142 changes: 138 additions & 4 deletions api.planx.uk/modules/send/bops/bops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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<CreateBopsApplication>(
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<string, string>;
Expand All @@ -155,12 +283,17 @@ interface FindApplication {
*/
async function checkBOPSAuditTable(
sessionId: string,
version: string,
): Promise<Record<string, string>> {
const searchString = `%/api/${version}/planning_applications`;
const application = await $api.client.request<FindApplication>(
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
Expand All @@ -169,10 +302,11 @@ async function checkBOPSAuditTable(
`,
{
session_id: sessionId,
search_string: searchString,
},
);

return application?.bopsApplications[0]?.response;
}

export { sendToBOPS };
export { sendToBOPS, sendToBOPSV2 };
8 changes: 8 additions & 0 deletions api.planx.uk/modules/send/createSendEvents/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion api.planx.uk/modules/send/routes.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
Expand Down
12 changes: 10 additions & 2 deletions editor.planx.uk/src/@planx/components/Send/Public.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,17 @@ const SendEvents: React.FC<Props> = ({
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 (
Expand Down

0 comments on commit 0b9c036

Please sign in to comment.