Skip to content

Commit

Permalink
feat: send ODP Schema payload to BOPS /api/v2/planning_applications
Browse files Browse the repository at this point in the history
… on staging only (#2529)
  • Loading branch information
jessicamcinchak authored Dec 19, 2023
1 parent eb6e3e0 commit b2cff6c
Show file tree
Hide file tree
Showing 8 changed files with 306 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
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ const createPaymentSendEvents = async (
comment: `bops_submission_${payload.sessionId}`,
});
combinedResponse[Destination.BOPS] = bopsEvent;

const isProduction = process.env.APP_ENVIRONMENT === "production";
if (!isProduction) {
const bopsV2Event = await createScheduledEvent({
webhook: `{{HASURA_PLANX_API_URL}}/bops-v2/${teamSlug}`,
schedule_at: new Date(now.getTime() + 30 * 1000),
payload: eventPayload,
comment: `bops_v2_submission_${payload.sessionId}`,
});
combinedResponse["bops_v2"] = bopsV2Event;
}
}

if (destinations.includes(Destination.Email)) {
Expand Down
112 changes: 110 additions & 2 deletions api.planx.uk/modules/send/bops/bops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import supertest from "supertest";
import { queryMock } from "../../../tests/graphqlQueryMock";
import app from "../../../server";
import { expectedPayload } from "../../../tests/mocks/bopsMocks";
import { expectedPlanningPermissionPayload } from "../../../tests/mocks/digitalPlanningDataMocks";

jest.mock("../../saveAndReturn/service/utils", () => ({
markSessionAsSubmitted: jest.fn(),
Expand All @@ -22,6 +23,10 @@ jest.mock("@opensystemslab/planx-core", () => {
exportData: expectedPayload,
redactedExportData: expectedPayload,
});
this.export.digitalPlanningDataPayload = () =>
jest.fn().mockResolvedValue({
exportData: expectedPlanningPermissionPayload,
});
}
},
};
Expand All @@ -36,7 +41,10 @@ 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 +110,10 @@ 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 +130,100 @@ 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("successfully 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",
});
});
});
});
148 changes: 144 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,139 @@ 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 +289,17 @@ interface FindApplication {
*/
async function checkBOPSAuditTable(
sessionId: string,
version: "v1" | "v2",
): 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 +308,11 @@ async function checkBOPSAuditTable(
`,
{
session_id: sessionId,
search_string: searchString,
},
);

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

export { sendToBOPS };
export { sendToBOPS, sendToBOPSV2 };
11 changes: 11 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,17 @@ const createSendEvents: CreateSendEventsController = async (
comment: `bops_submission_${sessionId}`,
});
combinedResponse["bops"] = bopsEvent;

const isProduction = process.env.APP_ENVIRONMENT === "production";
if (!isProduction) {
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
Loading

0 comments on commit b2cff6c

Please sign in to comment.