diff --git a/.env.example b/.env.example index 369418ea13..fd2f0b1203 100644 --- a/.env.example +++ b/.env.example @@ -98,5 +98,8 @@ UNIFORM_CLIENT_AYLESBURY_VALE=👻 UNIFORM_CLIENT_CHILTERN=👻 UNIFORM_CLIENT_WYCOMBE=👻 +## Forthcoming Idox Nexus integration +IDOX_NEXUS_CLIENT=👻 + ## End-to-end test team (borrows Lambeth's details) GOV_UK_PAY_SECRET_E2E=👻 diff --git a/api.planx.uk/lib/hasura/metadata/index.ts b/api.planx.uk/lib/hasura/metadata/index.ts index 41b2fc8d19..b6e5fb4aaa 100644 --- a/api.planx.uk/lib/hasura/metadata/index.ts +++ b/api.planx.uk/lib/hasura/metadata/index.ts @@ -12,6 +12,7 @@ interface ScheduledEvent { export interface CombinedResponse { bops?: ScheduledEventResponse; uniform?: ScheduledEventResponse; + idox?: ScheduledEventResponse; email?: ScheduledEventResponse; s3?: ScheduledEventResponse; } diff --git a/api.planx.uk/modules/send/createSendEvents/controller.ts b/api.planx.uk/modules/send/createSendEvents/controller.ts index f0ea313c39..9cfd7c892f 100644 --- a/api.planx.uk/modules/send/createSendEvents/controller.ts +++ b/api.planx.uk/modules/send/createSendEvents/controller.ts @@ -10,7 +10,7 @@ const createSendEvents: CreateSendEventsController = async ( res, next, ) => { - const { email, uniform, bops, s3 } = res.locals.parsedReq.body; + const { email, uniform, bops, s3, idox } = res.locals.parsedReq.body; const { sessionId } = res.locals.parsedReq.params; try { @@ -47,6 +47,16 @@ const createSendEvents: CreateSendEventsController = async ( combinedResponse["uniform"] = uniformEvent; } + if (idox) { + const idoxEvent = await createScheduledEvent({ + webhook: `{{HASURA_PLANX_API_URL}}/idox/${idox.localAuthority}`, + schedule_at: new Date(now.getTime() + 60 * 1000), + payload: idox.body, + comment: `idox_nexus_submission_${sessionId}`, + }); + combinedResponse["idox"] = idoxEvent; + } + if (s3) { const s3Event = await createScheduledEvent({ webhook: `{{HASURA_PLANX_API_URL}}/upload-submission/${s3.localAuthority}`, diff --git a/api.planx.uk/modules/send/createSendEvents/types.ts b/api.planx.uk/modules/send/createSendEvents/types.ts index 98b51bdf13..e5151dadb8 100644 --- a/api.planx.uk/modules/send/createSendEvents/types.ts +++ b/api.planx.uk/modules/send/createSendEvents/types.ts @@ -15,6 +15,7 @@ export const combinedEventsPayloadSchema = z.object({ bops: eventSchema.optional(), uniform: eventSchema.optional(), s3: eventSchema.optional(), + idox: eventSchema.optional(), }), params: z.object({ sessionId: z.string().uuid(), diff --git a/api.planx.uk/modules/send/idox/nexus.test.ts b/api.planx.uk/modules/send/idox/nexus.test.ts new file mode 100644 index 0000000000..e8e2bf793e --- /dev/null +++ b/api.planx.uk/modules/send/idox/nexus.test.ts @@ -0,0 +1,24 @@ +import supertest from "supertest"; +import app from "../../../server"; + +describe(`sending an application to Idox Nexus`, () => { + it("fails without authorization header", async () => { + await supertest(app) + .post("/idox/southwark") + .send({ payload: { sessionId: "123" } }) + .expect(401); + }); + + it("errors if the payload body does not include a sessionId", async () => { + await supertest(app) + .post("/idox/southwark") + .set({ Authorization: process.env.HASURA_PLANX_API_KEY! }) + .send({ payload: { somethingElse: "123" } }) + .expect(400) + .then((res) => { + expect(res.body).toEqual({ + error: "Missing application data to send to Idox Nexus", + }); + }); + }); +}); diff --git a/api.planx.uk/modules/send/idox/nexus.ts b/api.planx.uk/modules/send/idox/nexus.ts new file mode 100644 index 0000000000..4adb6ad97f --- /dev/null +++ b/api.planx.uk/modules/send/idox/nexus.ts @@ -0,0 +1,408 @@ +import axios, { AxiosRequestConfig, isAxiosError } from "axios"; +import { NextFunction, Request, Response } from "express"; +import FormData from "form-data"; +import fs from "fs"; +import { gql } from "graphql-request"; +import jwt from "jsonwebtoken"; +import { Buffer } from "node:buffer"; +import { $api } from "../../../client"; +import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils"; +import { buildSubmissionExportZip } from "../utils/exportZip"; + +interface UniformClient { + clientId: string; + clientSecret: string; +} + +interface UniformSubmissionResponse { + submissionStatus?: string; + canDownload?: boolean; + submissionId?: string; +} + +interface RawUniformAuthResponse { + access_token: string; +} + +interface UniformAuthResponse { + token: string; + organisation: string; + organisationId: string; +} + +interface UniformApplication { + id: string; + idox_submission_id: string; + submission_reference: string; + destination: string; + response: UniformSubmissionResponse; + created_at: string; +} + +interface SendToUniformPayload { + sessionId: string; +} + +export async function sendToIdoxNexus( + req: Request, + res: Response, + next: NextFunction, +) { + /** + * Submits application data to Uniform + * + * first, create a zip folder containing an XML (Idox's schema), CSV (our format), and any user-uploaded files + * then, make requests to Uniform's "Submission API" to authenticate, create a submission, and attach the zip to the submission + * finally, insert a record into uniform_applications for future auditing + */ + 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 Idox Nexus", + }); + } + + // localAuthority is only parsed for audit record, not client-specific + const localAuthority = req.params.localAuthority; + const uniformClient = getUniformClient(); + + // confirm that this session has not already been successfully submitted before proceeding + const submittedApp = await checkUniformAuditTable(payload?.sessionId); + const isAlreadySubmitted = + submittedApp?.submissionStatus === "PENDING" && submittedApp?.canDownload; + if (isAlreadySubmitted) { + return res.status(200).send({ + sessionId: payload?.sessionId, + idoxSubmissionId: submittedApp?.submissionId, + message: `Skipping send, already successfully submitted`, + }); + } + + try { + // Request 1/4 - Authenticate + const { token, organisation, organisationId } = + await authenticate(uniformClient); + + // 2/4 - Create a submission + const idoxSubmissionId = await createSubmission( + token, + organisation, + organisationId, + payload.sessionId, + ); + + // 3/4 - Create & attach the zip + const zip = await buildSubmissionExportZip({ + sessionId: payload.sessionId, + onlyDigitalPlanningJSON: true, + }); + + const attachmentAdded = await attachArchive( + token, + idoxSubmissionId, + zip.filename, + ); + + // clean-up zip file + zip.remove(); + + // 4/4 - Get submission details and create audit record + const submissionDetails = await retrieveSubmission(token, idoxSubmissionId); + + const applicationAuditRecord = await createUniformApplicationAuditRecord({ + idoxSubmissionId, + submissionDetails, + payload, + localAuthority, + }); + + // Mark session as submitted so that reminder and expiry emails are not triggered + markSessionAsSubmitted(payload?.sessionId); + + return res.status(200).send({ + message: `Successfully created an Idox Nexus submission`, + zipAttached: attachmentAdded, + application: applicationAuditRecord, + }); + } catch (error) { + const errorMessage = isAxiosError(error) + ? JSON.stringify(error.response?.data) + : (error as Error).message; + return next({ + error, + 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 + */ +async function checkUniformAuditTable( + sessionId: string, +): Promise { + const application: Record<"uniform_applications", UniformApplication[]> = + await $api.client.request( + gql` + query FindApplication($submission_reference: String = "") { + uniform_applications( + where: { submission_reference: { _eq: $submission_reference } } + order_by: { created_at: desc } + ) { + response + } + } + `, + { + submission_reference: sessionId, + }, + ); + + return application?.uniform_applications[0]?.response; +} + +/** + * Logs in to the Idox Submission API using a username/password + * and returns an access token + */ +async function authenticate({ + clientId, + clientSecret, +}: UniformClient): Promise { + const authString = Buffer.from(`${clientId}:${clientSecret}`).toString( + "base64", + ); + + const authConfig: AxiosRequestConfig = { + method: "POST", + url: process.env.UNIFORM_TOKEN_URL!, + headers: { + Authorization: `Basic ${authString}`, + "Content-type": "application/x-www-form-urlencoded", + }, + data: new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + grant_type: "client_credentials", + }), + }; + + const response = await axios.request(authConfig); + + if (!response.data.access_token) { + throw Error("Failed to authenticate to Uniform - no access token returned"); + } + + // Decode access_token to get "organisation-name" & "organisation-id" + const decodedAccessToken = jwt.decode(response.data.access_token) as any; + const organisation = decodedAccessToken?.["organisation-name"]; + const organisationId = decodedAccessToken?.["organisation-id"]; + + if (!organisation || !organisationId) { + throw Error( + "Failed to authenticate to Uniform - failed to decode organisation details from access_token", + ); + } + + const uniformAuthResponse: UniformAuthResponse = { + token: response.data.access_token, + organisation: organisation, + organisationId: organisationId, + }; + + return uniformAuthResponse; +} + +/** + * Creates a submission (submissionReference is unique value provided by RIPA & must match XML ) + * and returns a submissionId parsed from the resource link + */ +async function createSubmission( + token: string, + organisation: string, + organisationId: string, + sessionId = "TEST", +): Promise { + const createSubmissionEndpoint = `${process.env + .UNIFORM_SUBMISSION_URL!}/secure/submission`; + + const isStaging = ["mock-server", "staging"].some((hostname) => + createSubmissionEndpoint.includes(hostname), + ); + + const createSubmissionConfig: AxiosRequestConfig = { + url: createSubmissionEndpoint, + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-type": "application/json", + }, + data: JSON.stringify({ + entity: "dc", + module: "dc", + organisation: organisation, + organisationId: organisationId, + submissionReference: sessionId, + description: isStaging + ? "Staging submission from PlanX" + : "Production submission from PlanX", + submissionProcessorType: "API", + }), + }; + + const response = await axios.request(createSubmissionConfig); + // successful submission returns 201 Created without body + if (response.status !== 201) + throw Error("Failed to authenticate to Idox Nexus"); + + // parse & return the submissionId + const resourceLink = response.headers.location; + const submissionId = resourceLink.split("/").pop(); + if (!submissionId) + throw Error("Authenticated to Idox Nexus, but failed to create submission"); + + return submissionId; +} + +/** + * Uploads and attaches a zip folder to an existing submission + */ +async function attachArchive( + token: string, + submissionId: string, + zipPath: string, +): Promise { + if (!fs.existsSync(zipPath)) { + console.log( + `Zip does not exist, cannot attach to idox_submission_id ${submissionId}`, + ); + return false; + } + + const attachArchiveEndpoint = `${process.env + .UNIFORM_SUBMISSION_URL!}/secure/submission/${submissionId}/archive`; + + const formData = new FormData(); + formData.append("file", fs.createReadStream(zipPath)); + + const attachArchiveConfig: AxiosRequestConfig = { + url: attachArchiveEndpoint, + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + data: formData, + // Restrict to 1GB + maxBodyLength: 1e9, + maxContentLength: 1e9, + }; + + const response = await axios.request(attachArchiveConfig); + // successful upload returns 204 No Content without body + const isSuccess = response.status === 204; + + // Temp additional logging to debug failures + console.log("*** Uniform attachArchive response ***"); + console.log({ status: response.status }); + console.log(JSON.stringify(response.data, null, 2)); + console.log("******"); + + return isSuccess; +} + +/** + * Gets details about an existing submission to store for auditing purposes + * since neither createSubmission nor attachArchive requests return a meaningful response body + */ +async function retrieveSubmission( + token: string, + submissionId: string, +): Promise { + const getSubmissionEndpoint = `${process.env + .UNIFORM_SUBMISSION_URL!}/secure/submission/${submissionId}`; + + const getSubmissionConfig: AxiosRequestConfig = { + url: getSubmissionEndpoint, + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + const response = await axios.request(getSubmissionConfig); + return response.data; +} + +/** + * Get id and secret of Idox Nexus client + */ +const getUniformClient = (): UniformClient => { + const client = process.env["IDOX_NEXUS_CLIENT"]; + + if (!client) throw Error(`Unable to find Idox Nexus client`); + + const [clientId, clientSecret] = client.split(":"); + return { clientId, clientSecret }; +}; + +const createUniformApplicationAuditRecord = async ({ + idoxSubmissionId, + payload, + localAuthority, + submissionDetails, +}: { + idoxSubmissionId: string; + payload: SendToUniformPayload; + localAuthority: string; + submissionDetails: UniformSubmissionResponse; +}): Promise => { + const xml = await $api.export.oneAppPayload(payload?.sessionId); + + const application: Record< + "insert_uniform_applications_one", + UniformApplication + > = await $api.client.request( + gql` + mutation CreateUniformApplication( + $idox_submission_id: String = "" + $submission_reference: String = "" + $destination: String = "" + $response: jsonb = "" + $payload: jsonb = "" + $xml: xml = "" + ) { + insert_uniform_applications_one( + object: { + idox_submission_id: $idox_submission_id + submission_reference: $submission_reference + destination: $destination + response: $response + payload: $payload + xml: $xml + } + ) { + id + idox_submission_id + submission_reference + destination + response + created_at + } + } + `, + { + idox_submission_id: idoxSubmissionId, + submission_reference: payload?.sessionId, + destination: localAuthority, + response: submissionDetails, + payload, + xml, + }, + ); + + return application.insert_uniform_applications_one; +}; diff --git a/api.planx.uk/modules/send/routes.ts b/api.planx.uk/modules/send/routes.ts index 37e2eb31da..5447f7ad3d 100644 --- a/api.planx.uk/modules/send/routes.ts +++ b/api.planx.uk/modules/send/routes.ts @@ -8,6 +8,7 @@ import { validate } from "../../shared/middleware/validate"; import { combinedEventsPayloadSchema } from "./createSendEvents/types"; import { downloadApplicationFiles } from "./downloadApplicationFiles"; import { sendToS3 } from "./s3"; +import { sendToIdoxNexus } from "./idox/nexus"; const router = Router(); @@ -18,6 +19,7 @@ router.post( ); 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.get("/download-application-files/:sessionId", downloadApplicationFiles); router.post("/upload-submission/:localAuthority", useHasuraAuth, sendToS3); diff --git a/api.planx.uk/modules/send/utils/exportZip.ts b/api.planx.uk/modules/send/utils/exportZip.ts index 2d15f79c2f..98843e53f2 100644 --- a/api.planx.uk/modules/send/utils/exportZip.ts +++ b/api.planx.uk/modules/send/utils/exportZip.ts @@ -22,10 +22,12 @@ export async function buildSubmissionExportZip({ sessionId, includeOneAppXML = false, includeDigitalPlanningJSON = false, + onlyDigitalPlanningJSON = false, }: { sessionId: string; includeOneAppXML?: boolean; includeDigitalPlanningJSON?: boolean; + onlyDigitalPlanningJSON?: boolean; }): Promise { // fetch session data const sessionData = await $api.session.find(sessionId); @@ -41,7 +43,7 @@ export async function buildSubmissionExportZip({ const zip = new ExportZip(sessionId, flowSlug); // add OneApp XML to the zip - if (includeOneAppXML) { + if (includeOneAppXML && !onlyDigitalPlanningJSON) { try { const xml = await $api.export.oneAppPayload(sessionId); const xmlStream = str(xml.trim()); @@ -57,7 +59,7 @@ export async function buildSubmissionExportZip({ } // add ODP Schema JSON to the zip, skipping validation if an unsupported application type - if (includeDigitalPlanningJSON) { + if (includeDigitalPlanningJSON || onlyDigitalPlanningJSON) { try { const doValidation = isApplicationTypeSupported(passport); const schema = doValidation @@ -75,115 +77,117 @@ export async function buildSubmissionExportZip({ } } - // add remote files on S3 to the zip - const files = new Passport(passport).files; - if (files.length) { - for (const file of files) { - // Ensure unique filename by combining original filename and S3 folder name, which is a nanoid - // Uniform requires all uploaded files to be present in the zip, even if they are duplicates - // Must match unique filename in editor.planx.uk/src/@planx/components/Send/uniform/xml.ts - const uniqueFilename = decodeURIComponent( - file.url.split("/").slice(-2).join("-"), - ); - await zip.addRemoteFile({ url: file.url, name: uniqueFilename }); + if (!onlyDigitalPlanningJSON) { + // add remote user-uploaded files on S3 to the zip + const files = new Passport(passport).files; + if (files.length) { + for (const file of files) { + // Ensure unique filename by combining original filename and S3 folder name, which is a nanoid + // Uniform requires all uploaded files to be present in the zip, even if they are duplicates + // Must match unique filename in editor.planx.uk/src/@planx/components/Send/uniform/xml.ts + const uniqueFilename = decodeURIComponent( + file.url.split("/").slice(-2).join("-"), + ); + await zip.addRemoteFile({ url: file.url, name: uniqueFilename }); + } } - } - // generate csv data - const responses = await $api.export.csvData(sessionId); - const redactedResponses = await $api.export.csvDataRedacted(sessionId); - - // write csv to the zip - try { - const csvStream = stringify(responses, { - columns: ["question", "responses", "metadata"], - header: true, - }); - await zip.addStream({ - name: "application.csv", - stream: csvStream, - }); - } catch (error) { - throw new Error( - `Failed to generate CSV for ${sessionId} zip. Error - ${error}`, - ); - } + // generate csv data + const responses = await $api.export.csvData(sessionId); + const redactedResponses = await $api.export.csvDataRedacted(sessionId); - // add template files to zip - const templateNames = - await $api.getDocumentTemplateNamesForSession(sessionId); - for (const templateName of templateNames || []) { + // write csv to the zip try { - const isTemplateSupported = hasRequiredDataForTemplate({ - passport, - templateName, + const csvStream = stringify(responses, { + columns: ["question", "responses", "metadata"], + header: true, + }); + await zip.addStream({ + name: "application.csv", + stream: csvStream, }); - if (isTemplateSupported) { - const templateStream = generateDocxTemplateStream({ + } catch (error) { + throw new Error( + `Failed to generate CSV for ${sessionId} zip. Error - ${error}`, + ); + } + + // add template files to zip + const templateNames = + await $api.getDocumentTemplateNamesForSession(sessionId); + for (const templateName of templateNames || []) { + try { + const isTemplateSupported = hasRequiredDataForTemplate({ passport, templateName, }); - await zip.addStream({ - name: `${templateName}.doc`, - stream: templateStream, - }); + if (isTemplateSupported) { + const templateStream = generateDocxTemplateStream({ + passport, + templateName, + }); + await zip.addStream({ + name: `${templateName}.doc`, + stream: templateStream, + }); + } + } catch (error) { + console.log( + `Template "${templateName}" could not be generated so has been skipped. Error - ${error}`, + ); + continue; } - } catch (error) { - console.log( - `Template "${templateName}" could not be generated so has been skipped. Error - ${error}`, - ); - continue; } - } - const boundingBox = passport.data["property.boundary.site.buffered"]; - const userAction = passport.data?.["drawBoundary.action"]; - // generate and add an HTML overview document for the submission to zip - const overviewHTML = generateApplicationHTML({ - planXExportData: responses as PlanXExportData[], - boundingBox, - userAction, - }); - await zip.addFile({ - name: "Overview.htm", - buffer: Buffer.from(overviewHTML), - }); - - // generate and add an HTML overview document for the submission to zip - const redactedOverviewHTML = generateApplicationHTML({ - planXExportData: redactedResponses as PlanXExportData[], - boundingBox, - userAction, - }); - await zip.addFile({ - name: "RedactedOverview.htm", - buffer: Buffer.from(redactedOverviewHTML), - }); - - // add an optional GeoJSON file to zip - const geojson: GeoJSON.Feature | undefined = - passport?.data?.["property.boundary.site"]; - if (geojson) { - if (userAction) { - geojson["properties"] ??= {}; - geojson["properties"]["planx_user_action"] = userAction; - } - const geoBuff = Buffer.from(JSON.stringify(geojson, null, 2)); - zip.addFile({ - name: "LocationPlanGeoJSON.geojson", - buffer: geoBuff, + const boundingBox = passport.data["property.boundary.site.buffered"]; + const userAction = passport.data?.["drawBoundary.action"]; + // generate and add an HTML overview document for the submission to zip + const overviewHTML = generateApplicationHTML({ + planXExportData: responses as PlanXExportData[], + boundingBox, + userAction, + }); + await zip.addFile({ + name: "Overview.htm", + buffer: Buffer.from(overviewHTML), }); - // generate and add an HTML boundary document for the submission to zip - const boundaryHTML = generateMapHTML({ - geojson, + // generate and add an HTML overview document for the submission to zip + const redactedOverviewHTML = generateApplicationHTML({ + planXExportData: redactedResponses as PlanXExportData[], boundingBox, userAction, }); await zip.addFile({ - name: "LocationPlan.htm", - buffer: Buffer.from(boundaryHTML), + name: "RedactedOverview.htm", + buffer: Buffer.from(redactedOverviewHTML), }); + + // add an optional GeoJSON file to zip + const geojson: GeoJSON.Feature | undefined = + passport?.data?.["property.boundary.site"]; + if (geojson) { + if (userAction) { + geojson["properties"] ??= {}; + geojson["properties"]["planx_user_action"] = userAction; + } + const geoBuff = Buffer.from(JSON.stringify(geojson, null, 2)); + zip.addFile({ + name: "LocationPlanGeoJSON.geojson", + buffer: geoBuff, + }); + + // generate and add an HTML boundary document for the submission to zip + const boundaryHTML = generateMapHTML({ + geojson, + boundingBox, + userAction, + }); + await zip.addFile({ + name: "LocationPlan.htm", + buffer: Buffer.from(boundaryHTML), + }); + } } // write the zip diff --git a/editor.planx.uk/src/@planx/components/Send/Editor.tsx b/editor.planx.uk/src/@planx/components/Send/Editor.tsx index 9e1816982b..bfc3f208ca 100644 --- a/editor.planx.uk/src/@planx/components/Send/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/Send/Editor.tsx @@ -57,12 +57,17 @@ const SendComponent: React.FC = (props) => { }, ]; - // Show S3 option on staging only + // Show S3 & Nexus options on staging only if (process.env.REACT_APP_ENV !== "production") { options.push({ value: Destination.S3, label: "Upload to AWS S3 bucket", }); + + options.push({ + value: Destination.Idox, + label: "Idox Nexus", + }); } const changeCheckbox = diff --git a/editor.planx.uk/src/@planx/components/Send/Public.tsx b/editor.planx.uk/src/@planx/components/Send/Public.tsx index 8b8be0b35f..f6c1b872bf 100644 --- a/editor.planx.uk/src/@planx/components/Send/Public.tsx +++ b/editor.planx.uk/src/@planx/components/Send/Public.tsx @@ -96,6 +96,16 @@ const CreateSendEvents: React.FC = ({ ); } + if ( + destinations.includes(Destination.Idox) && + isReady && + props.handleSubmit + ) { + props.handleSubmit( + makeData(props, request.value.idox?.event_id, "idoxSendEventId") + ); + } + if ( destinations.includes(Destination.Email) && isReady && diff --git a/editor.planx.uk/src/@planx/components/Send/model.ts b/editor.planx.uk/src/@planx/components/Send/model.ts index d5f9fd5a16..cd1082b4a5 100644 --- a/editor.planx.uk/src/@planx/components/Send/model.ts +++ b/editor.planx.uk/src/@planx/components/Send/model.ts @@ -4,6 +4,7 @@ import { MoreInformation, parseMoreInformation } from "../shared"; export enum Destination { BOPS = "bops", Uniform = "uniform", + Idox = "idox", Email = "email", S3 = "s3", } @@ -79,6 +80,13 @@ export function getCombinedEventsPayload({ }; } + if (destinations.includes(Destination.Idox)) { + combinedEventsPayload[Destination.Idox] = { + localAuthority: teamSlug, + body: { sessionId }, + }; + } + if (destinations.includes(Destination.S3)) { combinedEventsPayload[Destination.S3] = { localAuthority: teamSlug,